From 82e0ca2423e6eed65f65542eb5922c4dd69c2d15 Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Wed, 18 Jan 2017 13:19:19 -0800 Subject: [PATCH 001/142] create users tablet button --- scripts/system/users.js | 1229 +-------------------------------------- 1 file changed, 9 insertions(+), 1220 deletions(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index 8c52240aa9..a403dd7978 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -2,9 +2,8 @@ // // users.js -// examples // -// Created by David Rowe on 9 Mar 2015. +// Created by Faye Li on 18 Jan 2017. // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,1226 +11,16 @@ // (function() { // BEGIN LOCAL_SCOPE - -// resolve these paths immediately -var MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); -var BASE_URL = Script.resolvePath("assets/images/tools/"); - -var PopUpMenu = function (properties) { - var value = properties.value, - promptOverlay, - valueOverlay, - buttonOverlay, - optionOverlays = [], - isDisplayingOptions = false, - OPTION_MARGIN = 4, - - MIN_MAX_BUTTON_SVG_WIDTH = 17.1, - MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, - MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH; - - function positionDisplayOptions() { - var y, - i; - - y = properties.y - (properties.values.length - 1) * properties.lineHeight - OPTION_MARGIN; - - for (i = 0; i < properties.values.length; i += 1) { - Overlays.editOverlay(optionOverlays[i], { - y: y - }); - y += properties.lineHeight; - } - } - - function showDisplayOptions() { - var i, - yOffScreen = Controller.getViewportDimensions().y; - - for (i = 0; i < properties.values.length; i += 1) { - optionOverlays[i] = Overlays.addOverlay("text", { - x: properties.x + properties.promptWidth, - y: yOffScreen, - width: properties.width - properties.promptWidth, - height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders - topMargin: OPTION_MARGIN, - leftMargin: OPTION_MARGIN, - color: properties.optionColor, - alpha: properties.optionAlpha, - backgroundColor: properties.popupBackgroundColor, - backgroundAlpha: properties.popupBackgroundAlpha, - text: properties.displayValues[i], - font: properties.font, - visible: true - }); - } - - positionDisplayOptions(); - - isDisplayingOptions = true; - } - - function deleteDisplayOptions() { - var i; - - for (i = 0; i < optionOverlays.length; i += 1) { - Overlays.deleteOverlay(optionOverlays[i]); - } - - isDisplayingOptions = false; - } - - function handleClick(overlay) { - var clicked = false, - i; - - if (overlay === valueOverlay || overlay === buttonOverlay) { - showDisplayOptions(); - return true; - } - - if (isDisplayingOptions) { - for (i = 0; i < optionOverlays.length; i += 1) { - if (overlay === optionOverlays[i]) { - value = properties.values[i]; - Overlays.editOverlay(valueOverlay, { - text: properties.displayValues[i] - }); - clicked = true; - } - } - - deleteDisplayOptions(); - } - - return clicked; - } - - function updatePosition(x, y) { - properties.x = x; - properties.y = y; - Overlays.editOverlay(promptOverlay, { - x: x, - y: y - }); - Overlays.editOverlay(valueOverlay, { - x: x + properties.promptWidth, - y: y - OPTION_MARGIN - }); - Overlays.editOverlay(buttonOverlay, { - x: x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, - y: y - OPTION_MARGIN + 1 - }); - if (isDisplayingOptions) { - positionDisplayOptions(); - } - } - - function setVisible(visible) { - Overlays.editOverlay(promptOverlay, { - visible: visible - }); - Overlays.editOverlay(valueOverlay, { - visible: visible - }); - Overlays.editOverlay(buttonOverlay, { - visible: visible - }); - } - - function tearDown() { - Overlays.deleteOverlay(promptOverlay); - Overlays.deleteOverlay(valueOverlay); - Overlays.deleteOverlay(buttonOverlay); - if (isDisplayingOptions) { - deleteDisplayOptions(); - } - } - - function getValue() { - return value; - } - - function setValue(newValue) { - var index; - - index = properties.values.indexOf(newValue); - if (index !== -1) { - value = newValue; - Overlays.editOverlay(valueOverlay, { - text: properties.displayValues[index] - }); - } - } - - promptOverlay = Overlays.addOverlay("text", { - x: properties.x, - y: properties.y, - width: properties.promptWidth, - height: properties.textHeight, - topMargin: 0, - leftMargin: 0, - color: properties.promptColor, - alpha: properties.promptAlpha, - backgroundColor: properties.promptBackgroundColor, - backgroundAlpha: properties.promptBackgroundAlpha, - text: properties.prompt, - font: properties.font, - visible: properties.visible + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + // TODO: work with Alan to make new icon art + icon: "icons/tablet-icons/people-i.svg", + text: "Users" }); - valueOverlay = Overlays.addOverlay("text", { - x: properties.x + properties.promptWidth, - y: properties.y, - width: properties.width - properties.promptWidth, - height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders - topMargin: OPTION_MARGIN, - leftMargin: OPTION_MARGIN, - color: properties.optionColor, - alpha: properties.optionAlpha, - backgroundColor: properties.optionBackgroundColor, - backgroundAlpha: properties.optionBackgroundAlpha, - text: properties.displayValues[properties.values.indexOf(value)], - font: properties.font, - visible: properties.visible - }); - - buttonOverlay = Overlays.addOverlay("image", { - x: properties.x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, - y: properties.y, - width: MIN_MAX_BUTTON_WIDTH, - height: MIN_MAX_BUTTON_HEIGHT, - imageURL: MIN_MAX_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: MIN_MAX_BUTTON_SVG_WIDTH, - height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 - }, - //color: properties.buttonColor, - alpha: properties.buttonAlpha, - visible: properties.visible - }); - - return { - updatePosition: updatePosition, - setVisible: setVisible, - handleClick: handleClick, - tearDown: tearDown, - getValue: getValue, - setValue: setValue - }; -}; - -var usersWindow = (function () { - - var WINDOW_WIDTH = 260, - WINDOW_MARGIN = 12, - WINDOW_BASE_MARGIN = 24, // A little less is needed in order look correct - WINDOW_FONT = { - size: 12 - }, - WINDOW_FOREGROUND_COLOR = { - red: 240, - green: 240, - blue: 240 - }, - WINDOW_FOREGROUND_ALPHA = 0.95, - WINDOW_HEADING_COLOR = { - red: 180, - green: 180, - blue: 180 - }, - WINDOW_HEADING_ALPHA = 0.95, - WINDOW_BACKGROUND_COLOR = { - red: 80, - green: 80, - blue: 80 - }, - WINDOW_BACKGROUND_ALPHA = 0.8, - windowPane, - windowHeading, - - // Margin on the left and right side of the window to keep - // it from getting too close to the edge of the screen which - // is unclickable. - WINDOW_MARGIN_X = 20, - - // Window border is similar to that of edit.js. - WINDOW_MARGIN_HALF = WINDOW_MARGIN / 2, - WINDOW_BORDER_WIDTH = WINDOW_WIDTH + 2 * WINDOW_MARGIN_HALF, - WINDOW_BORDER_TOP_MARGIN = 2 * WINDOW_MARGIN_HALF, - WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_MARGIN_HALF, - WINDOW_BORDER_LEFT_MARGIN = WINDOW_MARGIN_HALF, - WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, - WINDOW_BORDER_ALPHA = 0.5, - windowBorder, - - MIN_MAX_BUTTON_SVG = BASE_URL + "min-max-toggle.svg", - MIN_MAX_BUTTON_SVG_WIDTH = 17.1, - MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, - MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH, - MIN_MAX_BUTTON_COLOR = { - red: 255, - green: 255, - blue: 255 - }, - MIN_MAX_BUTTON_ALPHA = 0.9, - minimizeButton, - SCROLLBAR_BACKGROUND_WIDTH = 12, - SCROLLBAR_BACKGROUND_COLOR = { - red: 70, - green: 70, - blue: 70 - }, - SCROLLBAR_BACKGROUND_ALPHA = 0.8, - scrollbarBackground, - SCROLLBAR_BAR_MIN_HEIGHT = 5, - SCROLLBAR_BAR_COLOR = { - red: 170, - green: 170, - blue: 170 - }, - SCROLLBAR_BAR_ALPHA = 0.8, - SCROLLBAR_BAR_SELECTED_ALPHA = 0.95, - scrollbarBar, - scrollbarBackgroundHeight, - scrollbarBarHeight, - FRIENDS_BUTTON_SPACER = 6, // Space before add/remove friends button - FRIENDS_BUTTON_SVG = BASE_URL + "add-remove-friends.svg", - FRIENDS_BUTTON_SVG_WIDTH = 107, - FRIENDS_BUTTON_SVG_HEIGHT = 27, - FRIENDS_BUTTON_WIDTH = FRIENDS_BUTTON_SVG_WIDTH, - FRIENDS_BUTTON_HEIGHT = FRIENDS_BUTTON_SVG_HEIGHT, - FRIENDS_BUTTON_COLOR = { - red: 225, - green: 225, - blue: 225 - }, - FRIENDS_BUTTON_ALPHA = 0.95, - FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends", - FRIENDS_WINDOW_WIDTH = 290, - FRIENDS_WINDOW_HEIGHT = 500, - FRIENDS_WINDOW_TITLE = "Add/Remove Friends", - friendsButton, - friendsWindow, - - OPTION_BACKGROUND_COLOR = { - red: 60, - green: 60, - blue: 60 - }, - OPTION_BACKGROUND_ALPHA = 0.1, - - DISPLAY_SPACER = 12, // Space before display control - DISPLAY_PROMPT = "Show me:", - DISPLAY_PROMPT_WIDTH = 60, - DISPLAY_EVERYONE = "everyone", - DISPLAY_FRIENDS = "friends", - DISPLAY_VALUES = [DISPLAY_EVERYONE, DISPLAY_FRIENDS], - DISPLAY_DISPLAY_VALUES = DISPLAY_VALUES, - DISPLAY_OPTIONS_BACKGROUND_COLOR = { - red: 120, - green: 120, - blue: 120 - }, - DISPLAY_OPTIONS_BACKGROUND_ALPHA = 0.9, - displayControl, - - VISIBILITY_SPACER = 6, // Space before visibility control - VISIBILITY_PROMPT = "Visible to:", - VISIBILITY_PROMPT_WIDTH = 60, - VISIBILITY_ALL = "all", - VISIBILITY_FRIENDS = "friends", - VISIBILITY_NONE = "none", - VISIBILITY_VALUES = [VISIBILITY_ALL, VISIBILITY_FRIENDS, VISIBILITY_NONE], - VISIBILITY_DISPLAY_VALUES = ["everyone", "friends", "no one"], - visibilityControl, - - windowHeight, - windowBorderHeight, - windowTextHeight, - windowLineSpacing, - windowLineHeight, // = windowTextHeight + windowLineSpacing - windowMinimumHeight, - - usersOnline, // Raw users data - linesOfUsers = [], // Array of indexes pointing into usersOnline - numUsersToDisplay = 0, - firstUserToDisplay = 0, - - API_URL = "https://metaverse.highfidelity.com/api/v1/users?status=online", - API_FRIENDS_FILTER = "&filter=friends", - HTTP_GET_TIMEOUT = 60000, // ms = 1 minute - usersRequest, - processUsers, - pollUsersTimedOut, - usersTimer = null, - USERS_UPDATE_TIMEOUT = 5000, // ms = 5s - - showMe, - myVisibility, - - MENU_NAME = "View", - MENU_ITEM = "Users Online", - MENU_ITEM_OVERLAYS = "Overlays", - MENU_ITEM_AFTER = MENU_ITEM_OVERLAYS, - - SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", - SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", - SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", - SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", - // +ve x, y values are offset from left, top of screen; -ve from right, bottom. - - isLoggedIn = false, - isVisible = true, - isMinimized = false, - isBorderVisible = false, - - viewport, - isMirrorDisplay = false, - isFullscreenMirror = false, - - windowPosition = {}, // Bottom left corner of window pane. - isMovingWindow = false, - movingClickOffset = { x: 0, y: 0 }, - - isUsingScrollbars = false, - isMovingScrollbar = false, - scrollbarBackgroundPosition = {}, - scrollbarBarPosition = {}, - scrollbarBarClickedAt, // 0.0 .. 1.0 - scrollbarValue = 0.0; // 0.0 .. 1.0 - - function isWindowDisabled() { - return !Menu.isOptionChecked(MENU_ITEM) || !Menu.isOptionChecked(MENU_ITEM_OVERLAYS); + function cleanup() { + tablet.removeButton(button); } - function isValueTrue(value) { - // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after - // Being written if refresh script. - return value === true || value === "true"; - } - - function calculateWindowHeight() { - var AUDIO_METER_HEIGHT = 52, - MIRROR_HEIGHT = 220, - nonUsersHeight, - maxWindowHeight; - - if (isMinimized) { - windowHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; - windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; - return; - } - - // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight - + (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) - + DISPLAY_SPACER - + windowLineHeight + VISIBILITY_SPACER - + windowLineHeight + WINDOW_BASE_MARGIN; - - // Limit window to height of viewport above window position minus VU meter and mirror if displayed - windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; - maxWindowHeight = windowPosition.y - AUDIO_METER_HEIGHT; - if (isMirrorDisplay && !isFullscreenMirror) { - maxWindowHeight -= MIRROR_HEIGHT; - } - windowHeight = Math.max(Math.min(windowHeight, maxWindowHeight), nonUsersHeight); - windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; - - // Corresponding number of users to actually display - numUsersToDisplay = Math.max(Math.round((windowHeight - nonUsersHeight) / windowLineHeight), 0); - isUsingScrollbars = 0 < numUsersToDisplay && numUsersToDisplay < linesOfUsers.length; - if (isUsingScrollbars) { - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - } else { - firstUserToDisplay = 0; - scrollbarValue = 0.0; - } - } - - function saturateWindowPosition() { - windowPosition.x = Math.max(WINDOW_MARGIN_X, Math.min(viewport.x - WINDOW_WIDTH - WINDOW_MARGIN_X, windowPosition.x)); - windowPosition.y = Math.max(windowMinimumHeight, Math.min(viewport.y, windowPosition.y)); - } - - function updateOverlayPositions() { - // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. - var windowLeft = windowPosition.x, - windowTop = windowPosition.y - windowHeight, - x, - y; - - Overlays.editOverlay(windowBorder, { - x: windowPosition.x - WINDOW_BORDER_LEFT_MARGIN, - y: windowTop - WINDOW_BORDER_TOP_MARGIN - }); - Overlays.editOverlay(windowPane, { - x: windowLeft, - y: windowTop - }); - Overlays.editOverlay(windowHeading, { - x: windowLeft + WINDOW_MARGIN, - y: windowTop + WINDOW_MARGIN - }); - - Overlays.editOverlay(minimizeButton, { - x: windowLeft + WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, - y: windowTop + WINDOW_MARGIN - }); - - scrollbarBackgroundPosition.x = windowLeft + WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH; - scrollbarBackgroundPosition.y = windowTop + WINDOW_MARGIN + windowTextHeight; - Overlays.editOverlay(scrollbarBackground, { - x: scrollbarBackgroundPosition.x, - y: scrollbarBackgroundPosition.y - }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 - + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); - Overlays.editOverlay(scrollbarBar, { - x: scrollbarBackgroundPosition.x + 1, - y: scrollbarBarPosition.y - }); - - - x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - - DISPLAY_SPACER - - windowLineHeight - VISIBILITY_SPACER - - windowLineHeight - WINDOW_BASE_MARGIN; - if (shouldShowFriendsButton()) { - y -= FRIENDS_BUTTON_HEIGHT; - Overlays.editOverlay(friendsButton, { - x: x, - y: y - }); - y += FRIENDS_BUTTON_HEIGHT; - } - - y += DISPLAY_SPACER; - displayControl.updatePosition(x, y); - - y += windowLineHeight + VISIBILITY_SPACER; - visibilityControl.updatePosition(x, y); - } - - function updateUsersDisplay() { - var displayText = "", - user, - userText, - textWidth, - maxTextWidth, - ellipsisWidth, - reducedTextWidth, - i; - - if (!isMinimized) { - maxTextWidth = WINDOW_WIDTH - (isUsingScrollbars ? SCROLLBAR_BACKGROUND_WIDTH : 0) - 2 * WINDOW_MARGIN; - ellipsisWidth = Overlays.textSize(windowPane, "...").width; - reducedTextWidth = maxTextWidth - ellipsisWidth; - - for (i = 0; i < numUsersToDisplay; i += 1) { - user = usersOnline[linesOfUsers[firstUserToDisplay + i]]; - userText = user.text; - textWidth = user.textWidth; - - if (textWidth > maxTextWidth) { - // Trim and append "..." to fit window width - maxTextWidth = maxTextWidth - Overlays.textSize(windowPane, "...").width; - while (textWidth > reducedTextWidth) { - userText = userText.slice(0, -1); - textWidth = Overlays.textSize(windowPane, userText).width; - } - userText += "..."; - } - - displayText += "\n" + userText; - } - - displayText = displayText.slice(1); // Remove leading "\n". - - scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; - Overlays.editOverlay(scrollbarBackground, { - height: scrollbarBackgroundHeight, - visible: isLoggedIn && isUsingScrollbars - }); - scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, - SCROLLBAR_BAR_MIN_HEIGHT); - Overlays.editOverlay(scrollbarBar, { - height: scrollbarBarHeight, - visible: isLoggedIn && isUsingScrollbars - }); - } - - Overlays.editOverlay(windowBorder, { - height: windowBorderHeight - }); - - Overlays.editOverlay(windowPane, { - height: windowHeight, - text: displayText - }); - - Overlays.editOverlay(windowHeading, { - text: isLoggedIn ? (linesOfUsers.length > 0 ? "Users online" : "No users online") : "Users online - log in to view" - }); - } - - function shouldShowFriendsButton() { - return isVisible && isLoggedIn && !isMinimized; - } - - function updateOverlayVisibility() { - Overlays.editOverlay(windowBorder, { - visible: isVisible && isBorderVisible - }); - Overlays.editOverlay(windowPane, { - visible: isVisible - }); - Overlays.editOverlay(windowHeading, { - visible: isVisible - }); - Overlays.editOverlay(minimizeButton, { - visible: isVisible - }); - Overlays.editOverlay(scrollbarBackground, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(scrollbarBar, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(friendsButton, { - visible: shouldShowFriendsButton() - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - - function checkLoggedIn() { - var wasLoggedIn = isLoggedIn; - - isLoggedIn = Account.isLoggedIn(); - if (isLoggedIn !== wasLoggedIn) { - if (wasLoggedIn) { - setMinimized(true); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - } - - updateOverlayVisibility(); - } - } - - function pollUsers() { - var url = API_URL; - - if (showMe === DISPLAY_FRIENDS) { - url += API_FRIENDS_FILTER; - } - - usersRequest = new XMLHttpRequest(); - usersRequest.open("GET", url, true); - usersRequest.timeout = HTTP_GET_TIMEOUT; - usersRequest.ontimeout = pollUsersTimedOut; - usersRequest.onreadystatechange = processUsers; - usersRequest.send(); - } - - processUsers = function () { - var response, - myUsername, - user, - userText, - i; - - if (usersRequest.readyState === usersRequest.DONE) { - if (usersRequest.status === 200) { - response = JSON.parse(usersRequest.responseText); - if (response.status !== "success") { - print("Error: Request for users status returned status = " + response.status); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - if (!response.hasOwnProperty("data") || !response.data.hasOwnProperty("users")) { - print("Error: Request for users status returned invalid data"); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - - usersOnline = response.data.users; - myUsername = GlobalServices.username; - linesOfUsers = []; - for (i = 0; i < usersOnline.length; i += 1) { - user = usersOnline[i]; - if (user.username !== myUsername && user.online) { - userText = user.username; - if (user.location.root) { - userText += " @ " + user.location.root.name; - } - - usersOnline[i].text = userText; - usersOnline[i].textWidth = Overlays.textSize(windowPane, userText).width; - - linesOfUsers.push(i); - } - } - - checkLoggedIn(); - calculateWindowHeight(); - updateUsersDisplay(); - updateOverlayPositions(); - - } else { - print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - - usersTimer = Script.setTimeout(pollUsers, USERS_UPDATE_TIMEOUT); // Update after finished processing. - } - }; - - pollUsersTimedOut = function () { - print("Error: Request for users status timed out"); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - }; - - function setVisible(visible) { - isVisible = visible; - - if (isVisible) { - if (usersTimer === null) { - pollUsers(); - } - } else { - Script.clearTimeout(usersTimer); - usersTimer = null; - } - - updateOverlayVisibility(); - } - - function setMinimized(minimized) { - isMinimized = minimized; - Overlays.editOverlay(minimizeButton, { - subImage: { - y: isMinimized ? MIN_MAX_BUTTON_SVG_HEIGHT / 2 : 0 - } - }); - updateOverlayVisibility(); - Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); - } - - function onMenuItemEvent(event) { - if (event === MENU_ITEM) { - setVisible(Menu.isOptionChecked(MENU_ITEM)); - } - } - - function onFindableByChanged(event) { - if (VISIBILITY_VALUES.indexOf(event) !== -1) { - myVisibility = event; - visibilityControl.setValue(event); - Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); - } else { - print("Error: Unrecognized onFindableByChanged value: " + event); - } - } - - function onMousePressEvent(event) { - var clickedOverlay, - numLinesBefore, - overlayX, - overlayY, - minY, - maxY, - lineClicked, - userClicked, - delta; - - if (!isVisible || isWindowDisabled()) { - return; - } - - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - - if (displayControl.handleClick(clickedOverlay)) { - if (usersTimer !== null) { - Script.clearTimeout(usersTimer); - usersTimer = null; - } - pollUsers(); - showMe = displayControl.getValue(); - Settings.setValue(SETTING_USERS_SHOW_ME, showMe); - return; - } - - if (visibilityControl.handleClick(clickedOverlay)) { - myVisibility = visibilityControl.getValue(); - GlobalServices.findableBy = myVisibility; - Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); - return; - } - - if (clickedOverlay === windowPane) { - - overlayX = event.x - windowPosition.x - WINDOW_MARGIN; - overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; - - numLinesBefore = Math.round(overlayY / windowLineHeight); - minY = numLinesBefore * windowLineHeight; - maxY = minY + windowTextHeight; - - lineClicked = -1; - if (minY <= overlayY && overlayY <= maxY) { - lineClicked = numLinesBefore; - } - - userClicked = firstUserToDisplay + lineClicked; - - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX - && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { - //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); - location.goToUser(usersOnline[linesOfUsers[userClicked]].username); - } - - return; - } - - if (clickedOverlay === minimizeButton) { - setMinimized(!isMinimized); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - return; - } - - if (clickedOverlay === scrollbarBar) { - scrollbarBarClickedAt = (event.y - scrollbarBarPosition.y) / scrollbarBarHeight; - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_SELECTED_ALPHA - }); - isMovingScrollbar = true; - return; - } - - if (clickedOverlay === scrollbarBackground) { - delta = scrollbarBarHeight / (scrollbarBackgroundHeight - scrollbarBarHeight); - - if (event.y < scrollbarBarPosition.y) { - scrollbarValue = Math.max(scrollbarValue - delta, 0.0); - } else { - scrollbarValue = Math.min(scrollbarValue + delta, 1.0); - } - - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - updateOverlayPositions(); - updateUsersDisplay(); - return; - } - - if (clickedOverlay === friendsButton) { - if (!friendsWindow) { - friendsWindow = new OverlayWebWindow({ - title: FRIENDS_WINDOW_TITLE, - width: FRIENDS_WINDOW_WIDTH, - height: FRIENDS_WINDOW_HEIGHT, - visible: false - }); - } - friendsWindow.setURL(FRIENDS_WINDOW_URL); - friendsWindow.setVisible(true); - friendsWindow.raise(); - return; - } - - if (clickedOverlay === windowBorder) { - movingClickOffset = { - x: event.x - windowPosition.x, - y: event.y - windowPosition.y - }; - - isMovingWindow = true; - } - } - - function onMouseMoveEvent(event) { - var isVisible; - - if (!isLoggedIn || isWindowDisabled()) { - return; - } - - if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x - && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN - && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y - && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) - / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); - scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - updateOverlayPositions(); - updateUsersDisplay(); - } else { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; - } - } - - if (isMovingWindow) { - windowPosition = { - x: event.x - movingClickOffset.x, - y: event.y - movingClickOffset.y - }; - - saturateWindowPosition(); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - - } else { - - isVisible = isBorderVisible; - if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x - && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH - && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y - && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; - } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH - && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; - } - if (isVisible !== isBorderVisible) { - isBorderVisible = isVisible; - Overlays.editOverlay(windowBorder, { - visible: isBorderVisible - }); - } - } - } - - function onMouseReleaseEvent() { - var offset = {}; - - if (isWindowDisabled()) { - return; - } - - if (isMovingScrollbar) { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; - } - - if (isMovingWindow) { - // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) - ? windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) - ? windowPosition.y : windowPosition.y - viewport.y; - Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); - isMovingWindow = false; - } - } - - function onScriptUpdate() { - var oldViewport = viewport, - oldIsMirrorDisplay = isMirrorDisplay, - oldIsFullscreenMirror = isFullscreenMirror, - MIRROR_MENU_ITEM = "Mirror", - FULLSCREEN_MIRROR_MENU_ITEM = "Fullscreen Mirror"; - - if (isWindowDisabled()) { - return; - } - - viewport = Controller.getViewportDimensions(); - isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); - isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay - || isFullscreenMirror !== oldIsFullscreenMirror) { - calculateWindowHeight(); - updateUsersDisplay(); - } - - if (viewport.y !== oldViewport.y) { - if (windowPosition.y > oldViewport.y / 2) { - // Maintain position w.r.t. bottom of window. - windowPosition.y = viewport.y - (oldViewport.y - windowPosition.y); - } - } - - if (viewport.x !== oldViewport.x) { - if (windowPosition.x + (WINDOW_WIDTH / 2) > oldViewport.x / 2) { - // Maintain position w.r.t. right of window. - windowPosition.x = viewport.x - (oldViewport.x - windowPosition.x); - } - } - - updateOverlayPositions(); - } - - function setUp() { - var textSizeOverlay, - offsetSetting, - offset = {}, - hmdViewport; - - textSizeOverlay = Overlays.addOverlay("text", { - font: WINDOW_FONT, - visible: false - }); - windowTextHeight = Math.floor(Overlays.textSize(textSizeOverlay, "1").height); - windowLineSpacing = Math.floor(Overlays.textSize(textSizeOverlay, "1\n2").height - 2 * windowTextHeight); - windowLineHeight = windowTextHeight + windowLineSpacing; - windowMinimumHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; - Overlays.deleteOverlay(textSizeOverlay); - - viewport = Controller.getViewportDimensions(); - - offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); - if (offsetSetting !== "") { - offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); - } - if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { - windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; - windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; - } else { - hmdViewport = Controller.getRecommendedOverlayRect(); - windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. - }; - } - - saturateWindowPosition(); - calculateWindowHeight(); - - windowBorder = Overlays.addOverlay("rectangle", { - x: 0, - y: viewport.y, // Start up off-screen - width: WINDOW_BORDER_WIDTH, - height: windowBorderHeight, - radius: WINDOW_BORDER_RADIUS, - color: WINDOW_BORDER_COLOR, - alpha: WINDOW_BORDER_ALPHA, - visible: false - }); - - windowPane = Overlays.addOverlay("text", { - x: 0, - y: viewport.y, - width: WINDOW_WIDTH, - height: windowHeight, - topMargin: WINDOW_MARGIN + windowLineHeight, - leftMargin: WINDOW_MARGIN, - color: WINDOW_FOREGROUND_COLOR, - alpha: WINDOW_FOREGROUND_ALPHA, - backgroundColor: WINDOW_BACKGROUND_COLOR, - backgroundAlpha: WINDOW_BACKGROUND_ALPHA, - text: "", - font: WINDOW_FONT, - visible: false - }); - - windowHeading = Overlays.addOverlay("text", { - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 2 * WINDOW_MARGIN, - height: windowTextHeight, - topMargin: 0, - leftMargin: 0, - color: WINDOW_HEADING_COLOR, - alpha: WINDOW_HEADING_ALPHA, - backgroundAlpha: 0.0, - text: "Users online", - font: WINDOW_FONT, - visible: false - }); - - minimizeButton = Overlays.addOverlay("image", { - x: 0, - y: viewport.y, - width: MIN_MAX_BUTTON_WIDTH, - height: MIN_MAX_BUTTON_HEIGHT, - imageURL: MIN_MAX_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: MIN_MAX_BUTTON_SVG_WIDTH, - height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 - }, - color: MIN_MAX_BUTTON_COLOR, - alpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - scrollbarBackgroundPosition = { - x: 0, - y: viewport.y - }; - scrollbarBackground = Overlays.addOverlay("text", { - x: 0, - y: scrollbarBackgroundPosition.y, - width: SCROLLBAR_BACKGROUND_WIDTH, - height: windowTextHeight, - backgroundColor: SCROLLBAR_BACKGROUND_COLOR, - backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, - text: "", - visible: false - }); - - scrollbarBarPosition = { - x: 0, - y: viewport.y - }; - scrollbarBar = Overlays.addOverlay("text", { - x: 0, - y: scrollbarBarPosition.y, - width: SCROLLBAR_BACKGROUND_WIDTH - 2, - height: windowTextHeight, - backgroundColor: SCROLLBAR_BAR_COLOR, - backgroundAlpha: SCROLLBAR_BAR_ALPHA, - text: "", - visible: false - }); - - friendsButton = Overlays.addOverlay("image", { - x: 0, - y: viewport.y, - width: FRIENDS_BUTTON_WIDTH, - height: FRIENDS_BUTTON_HEIGHT, - imageURL: FRIENDS_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: FRIENDS_BUTTON_SVG_WIDTH, - height: FRIENDS_BUTTON_SVG_HEIGHT - }, - color: FRIENDS_BUTTON_COLOR, - alpha: FRIENDS_BUTTON_ALPHA, - visible: false - }); - - showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); - if (DISPLAY_VALUES.indexOf(showMe) === -1) { - showMe = DISPLAY_EVERYONE; - } - - displayControl = new PopUpMenu({ - prompt: DISPLAY_PROMPT, - value: showMe, - values: DISPLAY_VALUES, - displayValues: DISPLAY_DISPLAY_VALUES, - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, - promptWidth: DISPLAY_PROMPT_WIDTH, - lineHeight: windowLineHeight, - textHeight: windowTextHeight, - font: WINDOW_FONT, - promptColor: WINDOW_HEADING_COLOR, - promptAlpha: WINDOW_HEADING_ALPHA, - promptBackgroundColor: WINDOW_BACKGROUND_COLOR, - promptBackgroundAlpha: 0.0, - optionColor: WINDOW_FOREGROUND_COLOR, - optionAlpha: WINDOW_FOREGROUND_ALPHA, - optionBackgroundColor: OPTION_BACKGROUND_COLOR, - optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, - popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, - popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, - buttonColor: MIN_MAX_BUTTON_COLOR, - buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); - if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { - myVisibility = VISIBILITY_FRIENDS; - } - GlobalServices.findableBy = myVisibility; - - visibilityControl = new PopUpMenu({ - prompt: VISIBILITY_PROMPT, - value: myVisibility, - values: VISIBILITY_VALUES, - displayValues: VISIBILITY_DISPLAY_VALUES, - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, - promptWidth: VISIBILITY_PROMPT_WIDTH, - lineHeight: windowLineHeight, - textHeight: windowTextHeight, - font: WINDOW_FONT, - promptColor: WINDOW_HEADING_COLOR, - promptAlpha: WINDOW_HEADING_ALPHA, - promptBackgroundColor: WINDOW_BACKGROUND_COLOR, - promptBackgroundAlpha: 0.0, - optionColor: WINDOW_FOREGROUND_COLOR, - optionAlpha: WINDOW_FOREGROUND_ALPHA, - optionBackgroundColor: OPTION_BACKGROUND_COLOR, - optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, - popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, - popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, - buttonColor: MIN_MAX_BUTTON_COLOR, - buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - Controller.mousePressEvent.connect(onMousePressEvent); - Controller.mouseMoveEvent.connect(onMouseMoveEvent); - Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); - - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: MENU_ITEM, - afterItem: MENU_ITEM_AFTER, - isCheckable: true, - isChecked: isVisible - }); - Menu.menuItemEvent.connect(onMenuItemEvent); - - GlobalServices.findableByChanged.connect(onFindableByChanged); - - Script.update.connect(onScriptUpdate); - - pollUsers(); - - // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, true))); - } - - function tearDown() { - Menu.removeMenuItem(MENU_NAME, MENU_ITEM); - - Script.clearTimeout(usersTimer); - Overlays.deleteOverlay(windowBorder); - Overlays.deleteOverlay(windowPane); - Overlays.deleteOverlay(windowHeading); - Overlays.deleteOverlay(minimizeButton); - Overlays.deleteOverlay(scrollbarBackground); - Overlays.deleteOverlay(scrollbarBar); - Overlays.deleteOverlay(friendsButton); - displayControl.tearDown(); - visibilityControl.tearDown(); - } - - setUp(); - Script.scriptEnding.connect(tearDown); -}()); - + Script.scriptEnding.connect(cleanup); }()); // END LOCAL_SCOPE From 9865f134dbe771ce873f86f4dfb1bedfafb94207 Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Wed, 18 Jan 2017 16:09:12 -0800 Subject: [PATCH 002/142] go to users.html on tablet button click --- scripts/system/users.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index a403dd7978..5603cc591c 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -4,13 +4,14 @@ // users.js // // Created by Faye Li on 18 Jan 2017. -// Copyright 2015 High Fidelity, Inc. +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function() { // BEGIN LOCAL_SCOPE + var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ // TODO: work with Alan to make new icon art @@ -18,7 +19,14 @@ text: "Users" }); + function onClicked() { + tablet.gotoWebScreen(USERS_URL); + } + + button.clicked.connect(onClicked); + function cleanup() { + button.clicked.disconnect(onClicked); tablet.removeButton(button); } From 75b1eba84e076b2a7f7f8c1f55dd5a429965ce70 Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Wed, 18 Jan 2017 16:10:42 -0800 Subject: [PATCH 003/142] initial commit for users.html --- scripts/system/html/users.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/system/html/users.html diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html new file mode 100644 index 0000000000..cbe69efd92 --- /dev/null +++ b/scripts/system/html/users.html @@ -0,0 +1,16 @@ + + + + + +

Hello Users

+ + \ No newline at end of file From 5ad1cc6f4d538dc9a04ea7c8f938c6d45b9f1893 Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Wed, 18 Jan 2017 18:03:16 -0800 Subject: [PATCH 004/142] end of day commit --- scripts/system/html/users.html | 20 ++++++++++++++++++++ scripts/system/users.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index cbe69efd92..de6a03b702 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -12,5 +12,25 @@

Hello Users

+ +
+ + \ No newline at end of file diff --git a/scripts/system/users.js b/scripts/system/users.js index 5603cc591c..76745ac86e 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -11,7 +11,7 @@ // (function() { // BEGIN LOCAL_SCOPE - var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; + var USERS_URL = Script.resolvePath("html/users.html"); var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ // TODO: work with Alan to make new icon art From 10f512306ecb2ca9b0d4c987741499f6136009a7 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Thu, 19 Jan 2017 10:58:29 -0800 Subject: [PATCH 005/142] successfully polling all online users --- scripts/system/html/users.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index de6a03b702..75e79c47e8 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -23,7 +23,8 @@ $.ajax({ url: METAVERSE_API_URL, success: function(response) { - $("#dev-div").html(response); + $("#dev-div").html(JSON.stringify(response.data)); + console.log(response); } }); } From 53404caccf43d4a1c17a65a0eb0957a132186d08 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Thu, 19 Jan 2017 12:15:27 -0800 Subject: [PATCH 006/142] top bar css --- scripts/system/html/users.html | 38 +++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 75e79c47e8..4192f4b33f 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -9,11 +9,43 @@ --> + Users Online + + + -

Hello Users

- -
+
+
Users Online
+
+
+ +
+
From bbed26c8fa79e8b1a11ac7550a50916b7e6c60d5 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Thu, 19 Jan 2017 14:34:28 -0800 Subject: [PATCH 010/142] display all online users --- scripts/system/html/users.html | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index be657f35fe..b6cf8fb553 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -12,7 +12,7 @@ Users Online - + @@ -51,19 +57,28 @@
-
+
+ \ No newline at end of file diff --git a/scripts/system/users.js b/scripts/system/users.js index 76745ac86e..c55bccf961 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -23,7 +23,12 @@ tablet.gotoWebScreen(USERS_URL); } + function onWebEventReceived(event) { + print(event); + } + button.clicked.connect(onClicked); + tablet.webEventReceived.connect(onWebEventReceived); function cleanup() { button.clicked.disconnect(onClicked); From 763028dad4a482e0da294ad74f995ae486c39c81 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Thu, 19 Jan 2017 15:46:02 -0800 Subject: [PATCH 012/142] refresh icon --- scripts/system/html/users.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 703abb9638..79590f6443 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -29,7 +29,7 @@ font-weight: bold; } - .top-bar .container{ + .top-bar .container { display: flex; justify-content: space-between; align-items: center; @@ -38,6 +38,11 @@ height: 100%; } + #refresh-button { + width: 24px; + height: 24px; + } + .main { padding: 30px; } @@ -53,7 +58,7 @@
Users Online
- +
@@ -90,9 +95,9 @@ $(document).ready(function() { $("#dev-div").html("ready"); // Send a ready event to hifi - EventBridge.emitWebEvent("ready"); + // EventBridge.emitWebEvent("ready"); // Listen for events from hifi - EventBridge.scriptEventReceived.connect(onScriptEventReceived); + // EventBridge.scriptEventReceived.connect(onScriptEventReceived); }); From ebe96f9b25dcc4423b088618dfd68bdd0f9a42c4 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Sat, 7 Jan 2017 17:54:45 -0500 Subject: [PATCH 013/142] rm injectors from mixed audio processing --- libraries/audio-client/src/AudioClient.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e3dc11338..342c7b282e 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1177,11 +1177,6 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); } - // mix in active injectors - if (getActiveLocalAudioInjectors().size() > 0) { - mixLocalAudioInjectors(_mixBuffer); - } - // apply stereo reverb bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (hasReverb) { From 24d53ea13c6248d28a66709df3a7216ab64f6972 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Sun, 8 Jan 2017 18:30:57 -0500 Subject: [PATCH 014/142] rm audio output limiting --- libraries/audio-client/src/AudioClient.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 342c7b282e..6477449366 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1172,7 +1172,7 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - // convert network audio to float + // convert network audio (int16_t) to mix audio (float) for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); } @@ -1184,16 +1184,16 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA _listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } + // convert mix audio (float) to network audio (int16_t) + for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { + _scratchBuffer[i] = (int16_t)(_mixBuffer[i] * 32768.0f); + } + + // resample to output sample rate if (_networkToOutputResampler) { - - // resample to output sample rate - _audioLimiter.render(_mixBuffer, _scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); _networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } else { - - // no resampling needed - _audioLimiter.render(_mixBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + memcpy(outputBuffer.data(), _scratchBuffer, AudioConstants::NETWORK_FRAME_BYTES_STEREO); } } From a7ecf41a426dab17fb2c5064477a7dd909b0cdc5 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Sun, 8 Jan 2017 23:24:23 -0500 Subject: [PATCH 015/142] add audio limiting to device callback --- libraries/audio-client/src/AudioClient.cpp | 44 ++++++++++++++++++---- libraries/audio-client/src/AudioClient.h | 5 +++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 6477449366..1e97de8dca 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1386,6 +1386,12 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _loopbackOutputDevice = NULL; delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; + + delete[] _limitMixBuffer; + _limitMixBuffer = NULL; + + delete[] _limitScratchBuffer; + _limitScratchBuffer = NULL; } if (_networkToOutputResampler) { @@ -1436,6 +1442,12 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _limitPeriod = periodSampleSize * 2; + _limitMixBuffer = new float[_limitPeriod]; + _limitScratchBuffer = new int16_t[_limitPeriod]; + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); @@ -1545,6 +1557,8 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + samplesRequested = std::min(samplesRequested, _audio->_limitPeriod); int samplesPopped; int bytesWritten; @@ -1553,14 +1567,30 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - // if required, upmix or downmix to deviceChannelCount - if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); - } else { - lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + int16_t* scratchBuffer = _audio->_limitScratchBuffer; + lastPopOutput.readSamples(scratchBuffer, samplesPopped); + + float* mixBuffer = _audio->_limitMixBuffer; + for (int i = 0; i < samplesPopped; i++) { + mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); } + + int framesPopped = samplesPopped / AudioConstants::STEREO; + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + // limit the audio + _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); + } else { + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + + // upmix or downmix to deviceChannelCount + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; + channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); + } else { + channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + } + } + bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; } else { // nothing on network, don't grab anything from injectors, and just return 0s diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 123da35319..0320b5db2b 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -300,6 +300,11 @@ private: // for local hrtf-ing float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + + // for limiting + int _limitPeriod { 0 }; + float* _limitMixBuffer { NULL }; + int16_t* _limitScratchBuffer { NULL }; AudioLimiter _audioLimiter; // Adds Reverb From 3a0d874bb5557b9815d05f7f071b0e9d7f894f80 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 9 Jan 2017 13:21:20 -0500 Subject: [PATCH 016/142] add injector ring buffer to audio client --- libraries/audio-client/src/AudioClient.cpp | 3 ++- libraries/audio-client/src/AudioClient.h | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e97de8dca..8e73c1a983 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -127,6 +127,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), + _localInjectorsBuffer(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -146,7 +147,7 @@ AudioClient::AudioClient() : _networkToOutputResampler(NULL), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_receivedAudioStream, this), + _audioOutputIODevice(_localInjectorsBuffer, _receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 0320b5db2b..0733bba47c 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -84,8 +84,10 @@ public: class AudioOutputIODevice : public QIODevice { public: - AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : - _receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {}; + AudioOutputIODevice(AudioRingBuffer& localInjectorsBuffer, MixedProcessedAudioStream& receivedAudioStream, + AudioClient* audio) : + _localInjectorsBuffer(localInjectorsBuffer), _receivedAudioStream(receivedAudioStream), + _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } void stop() { close(); } @@ -93,6 +95,7 @@ public: qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: + AudioRingBuffer& _localInjectorsBuffer; MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; @@ -262,6 +265,7 @@ private: QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; + AudioRingBuffer _localInjectorsBuffer; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; From 0f1ec63b177385746d40f2661b30de4449f53df5 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 9 Jan 2017 13:41:10 -0500 Subject: [PATCH 017/142] enable injectors in audio device callback --- libraries/audio-client/src/AudioClient.cpp | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 8e73c1a983..9056ee0bea 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1561,22 +1561,44 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // restrict samplesRequested to the size of our mix/scratch buffers samplesRequested = std::min(samplesRequested, _audio->_limitPeriod); - int samplesPopped; - int bytesWritten; + int16_t* scratchBuffer = _audio->_limitScratchBuffer; + float* mixBuffer = _audio->_limitMixBuffer; - if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable()); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - int16_t* scratchBuffer = _audio->_limitScratchBuffer; - lastPopOutput.readSamples(scratchBuffer, samplesPopped); - - float* mixBuffer = _audio->_limitMixBuffer; - for (int i = 0; i < samplesPopped; i++) { + // convert to mix buffer (float) + for (int i = 0; i < networkSamplesPopped; i++) { mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); } - int framesPopped = samplesPopped / AudioConstants::STEREO; + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped; + if ((injectorSamplesPopped = _localInjectorsBuffer.readSamples(scratchBuffer, samplesRequested)) > 0) { + qCDebug(audiostream, "Read %d samples from injectors (%d available)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable()); + + if (networkSamplesPopped == 0) { + // convert to mix buffer (float) + for (int i = 0; i < injectorSamplesPopped; i++) { + mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); + } + } else { + // add to mix buffer (float) + for (int i = 0; i < injectorSamplesPopped; i++) { + mixBuffer[i] += (float)scratchBuffer[i] * (1/32768.0f); + } + } + } + + int samplesPopped = std::max(networkSamplesPopped, networkSamplesPopped); + int framesPopped = samplesPopped / AudioConstants::STEREO; + int bytesWritten; + if (samplesPopped > 0) { if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { // limit the audio _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); From dee5f6303739e62a6996d1670ef38597c76cf70f Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 10 Jan 2017 18:19:18 -0500 Subject: [PATCH 018/142] rename audio mix/scratch buffers Conflicts: libraries/audio-client/src/AudioClient.cpp libraries/audio-client/src/AudioClient.h --- libraries/audio-client/src/AudioClient.cpp | 30 +++++++++++----------- libraries/audio-client/src/AudioClient.h | 15 ++++++----- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9056ee0bea..63c4139d84 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1175,26 +1175,26 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA // convert network audio (int16_t) to mix audio (float) for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); + _networkMixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); } // apply stereo reverb bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (hasReverb) { updateReverbOptions(); - _listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + _listenerReverb.render(_networkMixBuffer, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } // convert mix audio (float) to network audio (int16_t) for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _scratchBuffer[i] = (int16_t)(_mixBuffer[i] * 32768.0f); + _networkScratchBuffer[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); } // resample to output sample rate if (_networkToOutputResampler) { - _networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + _networkToOutputResampler->render(_networkScratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else { - memcpy(outputBuffer.data(), _scratchBuffer, AudioConstants::NETWORK_FRAME_BYTES_STEREO); + memcpy(outputBuffer.data(), _networkScratchBuffer, AudioConstants::NETWORK_FRAME_BYTES_STEREO); } } @@ -1388,11 +1388,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; - delete[] _limitMixBuffer; - _limitMixBuffer = NULL; + delete _outputMixBuffer; + _outputMixBuffer = NULL; - delete[] _limitScratchBuffer; - _limitScratchBuffer = NULL; + delete _outputScratchBuffer; + _outputScratchBuffer = NULL; } if (_networkToOutputResampler) { @@ -1445,9 +1445,9 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes - _limitPeriod = periodSampleSize * 2; - _limitMixBuffer = new float[_limitPeriod]; - _limitScratchBuffer = new int16_t[_limitPeriod]; + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << @@ -1559,10 +1559,10 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; // restrict samplesRequested to the size of our mix/scratch buffers - samplesRequested = std::min(samplesRequested, _audio->_limitPeriod); + samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); - int16_t* scratchBuffer = _audio->_limitScratchBuffer; - float* mixBuffer = _audio->_limitMixBuffer; + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; int networkSamplesPopped; if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 0733bba47c..e245277d61 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -301,14 +301,15 @@ private: AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; - // for local hrtf-ing - float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + // for network audio (used by network audio threads) + float _networkMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + + // for output audio (used by this thread only) + int _outputPeriod { 0 }; + float* _outputMixBuffer { NULL }; + int16_t* _outputScratchBuffer { NULL }; - // for limiting - int _limitPeriod { 0 }; - float* _limitMixBuffer { NULL }; - int16_t* _limitScratchBuffer { NULL }; AudioLimiter _audioLimiter; // Adds Reverb From 969d776e1f0a824013f2cebd341a1ee8b663c902 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 10 Jan 2017 18:31:17 -0500 Subject: [PATCH 019/142] queue injector audio after device callbacks Conflicts: libraries/audio-client/src/AudioClient.cpp --- libraries/audio-client/src/AudioClient.cpp | 70 +++++++++++++++++++--- libraries/audio-client/src/AudioClient.h | 9 ++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 63c4139d84..5e52953949 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1083,14 +1083,62 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } -void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { +void AudioClient::prepareLocalAudioInjectors() { + if (_outputPeriod == 0) { + return; + } + + int periodSamples = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + int samplesNeeded; + if ((samplesNeeded = periodSamples - _localInjectorsBuffer.samplesAvailable()) > 0) { + while (samplesNeeded > 0) { + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + return; + } + + // reverb + if (_reverb) { + updateReverbOptions(); + _listenerReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + // convert mix audio (float) to network audio (int16_t) + for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { + _localScratchBuffer[i] = (int16_t)(_localMixBuffer[i] * 32768.0f); + } + + // resample to output sample rate + int samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + int16_t* scratchBuffer = _localScratchBuffer; + if (_networkToOutputResampler) { + int frames = _networkToOutputResampler->render(_localScratchBuffer, _outputScratchBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + samples = frames * AudioConstants::STEREO; + scratchBuffer = _outputScratchBuffer; + } + + // write to local injectors' ring buffer + _localInjectorsBuffer.writeSamples(scratchBuffer, samples); + samplesNeeded -= samples; + } + } +} + +bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { QVector injectorsToRemove; // lock the injector vector Lock lock(_injectorsMutex); - for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (_activeLocalAudioInjectors.size() == 0) { + return false; + } + + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); + + for (AudioInjector* injector : _activeLocalAudioInjectors) { if (injector->getLocalBuffer()) { static const int HRTF_DATASET_INDEX = 1; @@ -1099,8 +1147,8 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector - memset(_scratchBuffer, 0, bytesToRead); - if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, bytesToRead)) { + memset(_localScratchBuffer, 0, bytesToRead); + if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) { if (injector->isAmbisonic()) { @@ -1120,7 +1168,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float qz = relativeOrientation.y; // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else if (injector->isStereo()) { @@ -1128,7 +1176,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += (float)_scratchBuffer[i] * (1/32768.0f) * gain; + mixBuffer[i] += (float)_localScratchBuffer[i] * (1/32768.0f) * gain; } } else { @@ -1140,7 +1188,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } @@ -1161,8 +1209,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (AudioInjector* injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; - getActiveLocalAudioInjectors().removeOne(injector); + _activeLocalAudioInjectors.removeOne(injector); } + + return true; } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { @@ -1448,6 +1498,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _outputPeriod = periodSampleSize * 2; _outputMixBuffer = new float[_outputPeriod]; _outputScratchBuffer = new int16_t[_outputPeriod]; + _localInjectorsBuffer.resizeForFrameSize(_outputPeriod); qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << @@ -1595,6 +1646,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } } + // prepare injectors for the next callback + QMetaObject::invokeMethod(_audio, "prepareLocalAudioInjectors", Qt::QueuedConnection); + int samplesPopped = std::max(networkSamplesPopped, networkSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; int bytesWritten; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index e245277d61..322a27d71e 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -132,8 +132,6 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } - void checkDevices(); static const float CALLBACK_ACCELERATOR_RATIO; @@ -174,6 +172,7 @@ public slots: int setOutputBufferSize(int numFrames, bool persist = true); + void prepareLocalAudioInjectors(); bool outputLocalInjector(AudioInjector* injector) override; bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } @@ -221,7 +220,7 @@ protected: private: void outputFormatChanged(); - void mixLocalAudioInjectors(float* mixBuffer); + bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -305,6 +304,10 @@ private: float _networkMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + // for local audio (used by this thread only) + float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + // for output audio (used by this thread only) int _outputPeriod { 0 }; float* _outputMixBuffer { NULL }; From 42f5af7c39a0f9a07f21eed2f6234379c7e89342 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 11:31:44 -0500 Subject: [PATCH 020/142] improve audiostream debug logs --- libraries/audio-client/src/AudioClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 5e52953949..8b89430a76 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1617,7 +1617,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int networkSamplesPopped; if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable()); + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); @@ -1631,7 +1631,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int injectorSamplesPopped; if ((injectorSamplesPopped = _localInjectorsBuffer.readSamples(scratchBuffer, samplesRequested)) > 0) { - qCDebug(audiostream, "Read %d samples from injectors (%d available)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable()); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable(), samplesRequested); if (networkSamplesPopped == 0) { // convert to mix buffer (float) From 61f7f72c5e5dd3a4a6b1c1904ae3aacccbd0e727 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 12:29:04 -0500 Subject: [PATCH 021/142] simplify resampling --- libraries/audio-client/src/AudioClient.cpp | 35 ++++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 8b89430a76..a85a6c4282 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1108,18 +1108,23 @@ void AudioClient::prepareLocalAudioInjectors() { _localScratchBuffer[i] = (int16_t)(_localMixBuffer[i] * 32768.0f); } - // resample to output sample rate - int samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - int16_t* scratchBuffer = _localScratchBuffer; + int samples; if (_networkToOutputResampler) { + // resample to output sample rate int frames = _networkToOutputResampler->render(_localScratchBuffer, _outputScratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer samples = frames * AudioConstants::STEREO; - scratchBuffer = _outputScratchBuffer; + _localInjectorsBuffer.writeSamples(_outputScratchBuffer, samples); + + } else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsBuffer.writeSamples(_localScratchBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } - // write to local injectors' ring buffer - _localInjectorsBuffer.writeSamples(scratchBuffer, samples); samplesNeeded -= samples; } } @@ -1235,16 +1240,20 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA _listenerReverb.render(_networkMixBuffer, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - // convert mix audio (float) to network audio (int16_t) - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _networkScratchBuffer[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); - } - - // resample to output sample rate if (_networkToOutputResampler) { + // convert mix audio (float) to network audio (int16_t) + for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { + _networkScratchBuffer[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); + } + + // resample to output sample rate _networkToOutputResampler->render(_networkScratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } else { - memcpy(outputBuffer.data(), _networkScratchBuffer, AudioConstants::NETWORK_FRAME_BYTES_STEREO); + // convert mix audio (float) to network audio (int16_t) + for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { + outputSamples[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); + } } } From 0f08abfa1421fa17a9f9bb59c89693e4ae5dd893 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 12:29:28 -0500 Subject: [PATCH 022/142] fix bug in audio samples popped check --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index a85a6c4282..e0b36891a7 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1658,7 +1658,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // prepare injectors for the next callback QMetaObject::invokeMethod(_audio, "prepareLocalAudioInjectors", Qt::QueuedConnection); - int samplesPopped = std::max(networkSamplesPopped, networkSamplesPopped); + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; int bytesWritten; if (samplesPopped > 0) { From d1673e554ff289b5723996a117398c68667bea34 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 12:29:45 -0500 Subject: [PATCH 023/142] simplify audio bytesWritten computation --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index e0b36891a7..ddc5bf47e5 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1677,7 +1677,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } } - bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; + bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; } else { // nothing on network, don't grab anything from injectors, and just return 0s // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); From d7085ec6851b7fbf58898f99232d51a8dc7fbba6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 12:39:43 -0500 Subject: [PATCH 024/142] add audio helpers convertToMix/Scratch --- libraries/audio-client/src/AudioClient.cpp | 61 +++------------------- libraries/shared/src/AudioHelpers.h | 42 +++++++++++++++ 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index ddc5bf47e5..92e3b4dcf2 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -46,6 +46,7 @@ #include #include "PositionalAudioStream.h" +#include "AudioHelpers.h" #include "AudioClientLogging.h" #include "AudioLogging.h" @@ -842,36 +843,6 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } -static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -1103,10 +1074,7 @@ void AudioClient::prepareLocalAudioInjectors() { _listenerReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - // convert mix audio (float) to network audio (int16_t) - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _localScratchBuffer[i] = (int16_t)(_localMixBuffer[i] * 32768.0f); - } + convertToScratch(_localScratchBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); int samples; if (_networkToOutputResampler) { @@ -1228,10 +1196,7 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - // convert network audio (int16_t) to mix audio (float) - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _networkMixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); - } + convertToMix(_networkMixBuffer, decodedSamples, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); // apply stereo reverb bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -1241,19 +1206,13 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA } if (_networkToOutputResampler) { - // convert mix audio (float) to network audio (int16_t) - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _networkScratchBuffer[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); - } + convertToScratch(_networkScratchBuffer, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); // resample to output sample rate _networkToOutputResampler->render(_networkScratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else { - // convert mix audio (float) to network audio (int16_t) - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - outputSamples[i] = (int16_t)(_networkMixBuffer[i] * 32768.0f); - } + convertToScratch(outputSamples, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } } @@ -1630,10 +1589,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - // convert to mix buffer (float) - for (int i = 0; i < networkSamplesPopped; i++) { - mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); - } + convertToMix(mixBuffer, scratchBuffer, networkSamplesPopped); samplesRequested = networkSamplesPopped; } @@ -1643,10 +1599,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable(), samplesRequested); if (networkSamplesPopped == 0) { - // convert to mix buffer (float) - for (int i = 0; i < injectorSamplesPopped; i++) { - mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); - } + convertToMix(mixBuffer, scratchBuffer, injectorSamplesPopped); } else { // add to mix buffer (float) for (int i = 0; i < injectorSamplesPopped; i++) { diff --git a/libraries/shared/src/AudioHelpers.h b/libraries/shared/src/AudioHelpers.h index b43764ef5d..b0fcb7248d 100644 --- a/libraries/shared/src/AudioHelpers.h +++ b/libraries/shared/src/AudioHelpers.h @@ -91,4 +91,46 @@ static inline float unpackFloatGainFromByte(uint8_t byte) { return gain; } +static inline void convertToMix(float* mixBuffer, const int16_t* scratchBuffer, int numSamples) { + for (int i = 0; i < numSamples; i++) { + mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); + } +} + +static inline void convertToScratch(int16_t* scratchBuffer, const float* mixBuffer, int numSamples) { + for (int i = 0; i < numSamples; i++) { + scratchBuffer[i] = (int16_t)(mixBuffer[i] * 32768.0f); + } +} + +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + #endif // hifi_AudioHelpers_h From 4c7c7ee3ccfdacc8c6fa3e33194b95de5014b14a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 11 Jan 2017 15:25:31 -0500 Subject: [PATCH 025/142] mv audio injector preparation to own thread --- libraries/audio-client/src/AudioClient.cpp | 134 ++++++++++++++------- libraries/audio-client/src/AudioClient.h | 24 +++- 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 92e3b4dcf2..eb2b0f87e0 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -113,6 +113,10 @@ private: bool _quit { false }; }; +void AudioInjectorsThread::prepare() { + _audio->prepareLocalAudioInjectors(); +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), @@ -146,6 +150,8 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _localToOutputResampler(NULL), + _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsBuffer, _receivedAudioStream, this), @@ -178,6 +184,10 @@ AudioClient::AudioClient() : _checkDevicesThread->setPriority(QThread::LowPriority); _checkDevicesThread->start(); + // start a thread to process local injectors + _localAudioThread.setObjectName("LocalAudio Thread"); + _localAudioThread.start(); + configureReverb(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -215,6 +225,7 @@ void AudioClient::reset() { _stats.reset(); _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } void AudioClient::audioMixerKilled() { @@ -764,6 +775,7 @@ void AudioClient::configureReverb() { p.wetDryMix = _reverbOptions->getWetDryMix(); _listenerReverb.setParameters(&p); + _localReverb.setParameters(&p); // used only for adding self-reverb to loopback audio p.sampleRate = _outputFormat.sampleRate(); @@ -810,6 +822,7 @@ void AudioClient::setReverb(bool reverb) { if (!_reverb) { _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } } @@ -1059,42 +1072,56 @@ void AudioClient::prepareLocalAudioInjectors() { return; } - int periodSamples = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - int samplesNeeded; - if ((samplesNeeded = periodSamples - _localInjectorsBuffer.samplesAvailable()) > 0) { - while (samplesNeeded > 0) { - // get a network frame of local injectors' audio - if (!mixLocalAudioInjectors(_localMixBuffer)) { - return; - } + int bufferCapacity = _localInjectorsBuffer.getSampleCapacity(); + if (_localToOutputResampler) { + // avoid overwriting the buffer + bufferCapacity -= + _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; + bufferCapacity += 1; + } - // reverb - if (_reverb) { - updateReverbOptions(); - _listenerReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } + int samplesNeeded = std::numeric_limits::max(); + while (samplesNeeded > 0) { + // lock for every write to avoid locking out the device callback + Lock lock(_localAudioMutex); - convertToScratch(_localScratchBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - - int samples; - if (_networkToOutputResampler) { - // resample to output sample rate - int frames = _networkToOutputResampler->render(_localScratchBuffer, _outputScratchBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - // write to local injectors' ring buffer - samples = frames * AudioConstants::STEREO; - _localInjectorsBuffer.writeSamples(_outputScratchBuffer, samples); - - } else { - // write to local injectors' ring buffer - samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localInjectorsBuffer.writeSamples(_localScratchBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - } - - samplesNeeded -= samples; + samplesNeeded = bufferCapacity - _localInjectorsBuffer.samplesAvailable(); + if (samplesNeeded <= 0) { + break; } + + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + break; + } + + // reverb + if (_reverb) { + _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + convertToScratch(_localScratchBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + + int samples; + if (_localToOutputResampler) { + // resample to output sample rate + int frames = _localToOutputResampler->render(_localScratchBuffer, _localOutputScratchBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer + samples = frames * AudioConstants::STEREO; + _localInjectorsBuffer.writeSamples(_localOutputScratchBuffer, samples); + + } + else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsBuffer.writeSamples(_localScratchBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + } + + samplesNeeded -= samples; } } @@ -1158,7 +1185,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); float gain = gainForSource(distance, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, @@ -1395,6 +1422,8 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; + Lock lock(_localAudioMutex); + // cleanup any previously initialized device if (_audioOutput) { _audioOutput->stop(); @@ -1406,17 +1435,23 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; - delete _outputMixBuffer; + delete[] _outputMixBuffer; _outputMixBuffer = NULL; - delete _outputScratchBuffer; + delete[] _outputScratchBuffer; _outputScratchBuffer = NULL; + + delete[] _localOutputScratchBuffer; + _localOutputScratchBuffer = NULL; } if (_networkToOutputResampler) { // if we were using an input to network resampler, delete it here delete _networkToOutputResampler; _networkToOutputResampler = NULL; + + delete _localToOutputResampler; + _localToOutputResampler = NULL; } if (!outputDeviceInfo.isNull()) { @@ -1436,6 +1471,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_outputFormat.sampleSize() == 16); _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1466,7 +1502,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _outputPeriod = periodSampleSize * 2; _outputMixBuffer = new float[_outputPeriod]; _outputScratchBuffer = new int16_t[_outputPeriod]; - _localInjectorsBuffer.resizeForFrameSize(_outputPeriod); + _localOutputScratchBuffer = new int16_t[_outputPeriod]; + _localInjectorsBuffer.resizeForFrameSize(_outputPeriod * 2); qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << @@ -1595,21 +1632,26 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } int injectorSamplesPopped; - if ((injectorSamplesPopped = _localInjectorsBuffer.readSamples(scratchBuffer, samplesRequested)) > 0) { - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable(), samplesRequested); + { + Lock lock(_audio->_localAudioMutex); + if ((injectorSamplesPopped = _localInjectorsBuffer.readSamples(scratchBuffer, samplesRequested)) > 0) { + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable(), samplesRequested); - if (networkSamplesPopped == 0) { - convertToMix(mixBuffer, scratchBuffer, injectorSamplesPopped); - } else { - // add to mix buffer (float) - for (int i = 0; i < injectorSamplesPopped; i++) { - mixBuffer[i] += (float)scratchBuffer[i] * (1/32768.0f); + if (_audio->_shouldEchoToServer) { + // omit local audio, it should be echoed + } else if (networkSamplesPopped == 0) { + convertToMix(mixBuffer, scratchBuffer, injectorSamplesPopped); + } else { + // add to mix buffer (float) + for (int i = 0; i < injectorSamplesPopped; i++) { + mixBuffer[i] += (float)scratchBuffer[i] * (1 / 32768.0f); + } } } } // prepare injectors for the next callback - QMetaObject::invokeMethod(_audio, "prepareLocalAudioInjectors", Qt::QueuedConnection); + QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection); int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 322a27d71e..78fd97abb2 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,6 +69,19 @@ class QIODevice; class Transform; class NLPacket; +class AudioInjectorsThread : public QThread { + Q_OBJECT + +public: + AudioInjectorsThread(AudioClient* audio) : _audio(audio) {} + +public slots : + void prepare(); + +private: + AudioClient* _audio; +}; + class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -295,20 +308,25 @@ private: AudioEffectOptions* _reverbOptions; AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE }; AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE }; + AudioReverb _localReverb { AudioConstants::SAMPLE_RATE }; // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + AudioSRC* _localToOutputResampler; - // for network audio (used by network audio threads) + // for network audio (used by network audio thread) float _networkMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; - // for local audio (used by this thread only) + // for local audio (used by audio injectors thread) float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + int16_t* _localOutputScratchBuffer { NULL }; + AudioInjectorsThread _localAudioThread; + Mutex _localAudioMutex; - // for output audio (used by this thread only) + // for output audio (used by this thread) int _outputPeriod { 0 }; float* _outputMixBuffer { NULL }; int16_t* _outputScratchBuffer { NULL }; From 492795f7e5a7ef7c54e2e6717c414d30c6c2c8a1 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 18:08:45 -0500 Subject: [PATCH 026/142] audio client cosmetics --- libraries/audio-client/src/AudioClient.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index eb2b0f87e0..2358c20a2b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1113,8 +1113,7 @@ void AudioClient::prepareLocalAudioInjectors() { samples = frames * AudioConstants::STEREO; _localInjectorsBuffer.writeSamples(_localOutputScratchBuffer, samples); - } - else { + } else { // write to local injectors' ring buffer samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; _localInjectorsBuffer.writeSamples(_localScratchBuffer, @@ -1675,7 +1674,6 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; } else { // nothing on network, don't grab anything from injectors, and just return 0s - // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); memset(data, 0, maxSize); bytesWritten = maxSize; } From 7261b5285ee35f714ffba843d882109143c2787d Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 18:09:07 -0500 Subject: [PATCH 027/142] omit all injector samples on server echo --- libraries/audio-client/src/AudioClient.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 2358c20a2b..b0d8619482 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1638,6 +1638,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { if (_audio->_shouldEchoToServer) { // omit local audio, it should be echoed + injectorSamplesPopped = 0; } else if (networkSamplesPopped == 0) { convertToMix(mixBuffer, scratchBuffer, injectorSamplesPopped); } else { From c5415f96240696037278eeaa8dda336a3927a8d0 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 14:36:49 -0500 Subject: [PATCH 028/142] clean audio client logs --- libraries/audio-client/src/AudioClient.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b0d8619482..eadbe08b20 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -378,7 +378,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "DEBUG [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif @@ -400,12 +400,12 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, audioFormat.setByteOrder(QAudioFormat::LittleEndian); if (!audioDevice.isFormatSupported(audioFormat)) { - qCDebug(audioclient) << "WARNING: The native format is" << audioFormat << "but isFormatSupported() failed."; + qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed."; return false; } // converting to/from this rate must produce an integral number of samples if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { - qCDebug(audioclient) << "WARNING: The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; + qCWarning(audioclient) << "The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; return false; } return true; @@ -739,12 +739,12 @@ QVector AudioClient::getDeviceNames(QAudio::Mode mode) { } bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName)); } bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName)); } From 4f7f3c2a609a897db8bc64f1cf1ffcc3cd36196c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 15:17:07 -0500 Subject: [PATCH 029/142] mv localInjectorsBuffer to float-based localInjectorsStream The localInjectorsBuffer is based on AudioRingBuffer, which only accounts for int16_t. Local injectors are mixed, and so they can exceed std::numeric_limits before limiting. This will allow them to remain as float until limiting (in the device callback) - once the new stream is implemented. --- libraries/audio-client/src/AudioClient.cpp | 46 +++++++++------------- libraries/audio-client/src/AudioClient.h | 21 +++++++--- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index eadbe08b20..491c4e341b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -132,7 +132,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsBuffer(0), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -154,7 +154,7 @@ AudioClient::AudioClient() : _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_localInjectorsBuffer, _receivedAudioStream, this), + _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), @@ -1072,7 +1072,7 @@ void AudioClient::prepareLocalAudioInjectors() { return; } - int bufferCapacity = _localInjectorsBuffer.getSampleCapacity(); + int bufferCapacity = _localInjectorsStream.getSampleCapacity(); if (_localToOutputResampler) { // avoid overwriting the buffer bufferCapacity -= @@ -1086,7 +1086,7 @@ void AudioClient::prepareLocalAudioInjectors() { // lock for every write to avoid locking out the device callback Lock lock(_localAudioMutex); - samplesNeeded = bufferCapacity - _localInjectorsBuffer.samplesAvailable(); + samplesNeeded = bufferCapacity - _localInjectorsStream.samplesAvailable(); if (samplesNeeded <= 0) { break; } @@ -1101,22 +1101,20 @@ void AudioClient::prepareLocalAudioInjectors() { _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - convertToScratch(_localScratchBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - int samples; if (_localToOutputResampler) { // resample to output sample rate - int frames = _localToOutputResampler->render(_localScratchBuffer, _localOutputScratchBuffer, + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); // write to local injectors' ring buffer samples = frames * AudioConstants::STEREO; - _localInjectorsBuffer.writeSamples(_localOutputScratchBuffer, samples); + _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); } else { // write to local injectors' ring buffer samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localInjectorsBuffer.writeSamples(_localScratchBuffer, + _localInjectorsStream.writeSamples(_localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } @@ -1440,8 +1438,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice delete[] _outputScratchBuffer; _outputScratchBuffer = NULL; - delete[] _localOutputScratchBuffer; - _localOutputScratchBuffer = NULL; + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; } if (_networkToOutputResampler) { @@ -1501,8 +1499,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _outputPeriod = periodSampleSize * 2; _outputMixBuffer = new float[_outputPeriod]; _outputScratchBuffer = new int16_t[_outputPeriod]; - _localOutputScratchBuffer = new int16_t[_outputPeriod]; - _localInjectorsBuffer.resizeForFrameSize(_outputPeriod * 2); + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << @@ -1630,22 +1628,16 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { samplesRequested = networkSamplesPopped; } - int injectorSamplesPopped; + int injectorSamplesPopped = 0; { Lock lock(_audio->_localAudioMutex); - if ((injectorSamplesPopped = _localInjectorsBuffer.readSamples(scratchBuffer, samplesRequested)) > 0) { - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsBuffer.samplesAvailable(), samplesRequested); - - if (_audio->_shouldEchoToServer) { - // omit local audio, it should be echoed - injectorSamplesPopped = 0; - } else if (networkSamplesPopped == 0) { - convertToMix(mixBuffer, scratchBuffer, injectorSamplesPopped); - } else { - // add to mix buffer (float) - for (int i = 0; i < injectorSamplesPopped; i++) { - mixBuffer[i] += (float)scratchBuffer[i] * (1 / 32768.0f); - } + if (_audio->_shouldEchoToServer) { + // omit local audio, it should be echoed + _localInjectorsStream.skipSamples(samplesRequested); + } else { + bool append = networkSamplesPopped > 0; + if ((injectorSamplesPopped = _localInjectorsStream.readSamples(mixBuffer, samplesRequested, append)) > 0) { + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); } } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 78fd97abb2..8befd86f26 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,6 +69,17 @@ class QIODevice; class Transform; class NLPacket; +class LocalInjectorsStream { +public: + LocalInjectorsStream(int numFrameSamples); + int getSampleCapacity() { return 0; }; + int samplesAvailable() { return 0; } + int writeSamples(const float*, int numSamples) { return 0; } + void resizeForFrameSize(int numFrameSamples) {} + int skipSamples(int numSamples) { return 0; } + int readSamples(float* mixBuffer, int numSamples, bool append) { return 0; } +}; + class AudioInjectorsThread : public QThread { Q_OBJECT @@ -97,9 +108,9 @@ public: class AudioOutputIODevice : public QIODevice { public: - AudioOutputIODevice(AudioRingBuffer& localInjectorsBuffer, MixedProcessedAudioStream& receivedAudioStream, + AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : - _localInjectorsBuffer(localInjectorsBuffer), _receivedAudioStream(receivedAudioStream), + _localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } @@ -108,7 +119,7 @@ public: qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: - AudioRingBuffer& _localInjectorsBuffer; + LocalInjectorsStream& _localInjectorsStream; MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; @@ -277,7 +288,7 @@ private: QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; - AudioRingBuffer _localInjectorsBuffer; + LocalInjectorsStream _localInjectorsStream; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; @@ -322,7 +333,7 @@ private: // for local audio (used by audio injectors thread) float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; - int16_t* _localOutputScratchBuffer { NULL }; + float* _localOutputMixBuffer { NULL }; AudioInjectorsThread _localAudioThread; Mutex _localAudioMutex; From 95a7b38ea44241ea27cf8b7633511ceb36a995aa Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 15:46:24 -0500 Subject: [PATCH 030/142] templatize AudioRingBuffer --- libraries/audio/src/AudioRingBuffer.cpp | 120 ++++++++++++------------ libraries/audio/src/AudioRingBuffer.h | 115 ++++++++++++----------- 2 files changed, 121 insertions(+), 114 deletions(-) diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 260c682cde..c52d8f1447 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,15 +26,15 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; -AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) : +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), _sampleCapacity(numFrameSamples * numFramesCapacity), _bufferLength(numFrameSamples * (numFramesCapacity + 1)) { if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); _nextOutput = _buffer; _endOfLastWrite = _buffer; } @@ -43,29 +43,29 @@ AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) : static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); }; -AudioRingBuffer::~AudioRingBuffer() { +AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } -void AudioRingBuffer::clear() { +void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } -void AudioRingBuffer::reset() { +void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } -void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; _sampleCapacity = numFrameSamples * _frameCapacity; _bufferLength = numFrameSamples * (_frameCapacity + 1); if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); } else { _buffer = nullptr; } @@ -73,17 +73,17 @@ void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { reset(); } -int AudioRingBuffer::readSamples(int16_t* destination, int maxSamples) { - return readData((char*)destination, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } -int AudioRingBuffer::writeSamples(const int16_t* source, int maxSamples) { - return writeData((char*)source, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { + return writeData((char*)source, maxSamples * SampleSize) / SampleSize; } -int AudioRingBuffer::readData(char *data, int maxSize) { +int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); if (_nextOutput + numReadSamples > _buffer + _bufferLength) { @@ -91,22 +91,22 @@ int AudioRingBuffer::readData(char *data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; // read to the end of the buffer - memcpy(data, _nextOutput, numSamplesToEnd * sizeof(int16_t)); + memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); // read the rest from the beginning of the buffer - memcpy(data + (numSamplesToEnd * sizeof(int16_t)), _buffer, (numReadSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(data, _nextOutput, numReadSamples * sizeof(int16_t)); + memcpy(data, _nextOutput, numReadSamples * SampleSize); } shiftReadPosition(numReadSamples); - return numReadSamples * sizeof(int16_t); + return numReadSamples * SampleSize; } -int AudioRingBuffer::writeData(const char* data, int maxSize) { +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numWriteSamples = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -124,20 +124,20 @@ int AudioRingBuffer::writeData(const char* data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; // write to the end of the buffer - memcpy(_endOfLastWrite, data, numSamplesToEnd * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); // write the rest to the beginning of the buffer - memcpy(_buffer, data + (numSamplesToEnd * sizeof(int16_t)), (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(_endOfLastWrite, data, numWriteSamples * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - return numWriteSamples * sizeof(int16_t); + return numWriteSamples * SampleSize; } -int AudioRingBuffer::samplesAvailable() const { +int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; } @@ -149,31 +149,7 @@ int AudioRingBuffer::samplesAvailable() const { return sampleDifference; } -int AudioRingBuffer::addSilentSamples(int silentSamples) { - // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there - int numWriteSamples = std::min(silentSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - numWriteSamples = samplesRoomFor; - - qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); - memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); - } else { - memset(_endOfLastWrite, 0, numWriteSamples * sizeof(int16_t)); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples; -} - -int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const { +int16_t* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { // this shift will wrap the position around to the beginning of the ring @@ -186,11 +162,35 @@ int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int } } -float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { +int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { + // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there + int numWriteSamples = std::min(silentSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + numWriteSamples = samplesRoomFor; + + qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); + memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples; +} + +float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) float loudness = 0.0f; - const int16_t* sampleAt = frameStart; - const int16_t* bufferLastAt = _buffer + _bufferLength - 1; + const Sample* sampleAt = frameStart; + const Sample* bufferLastAt = _buffer + _bufferLength - 1; for (int i = 0; i < _numFrameSamples; ++i) { loudness += (float) std::abs(*sampleAt); @@ -203,14 +203,14 @@ float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { return loudness; } -float AudioRingBuffer::getFrameLoudness(ConstIterator frameStart) const { +float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { if (frameStart.isNull()) { return 0.0f; } return getFrameLoudness(&(*frameStart)); } -int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { +int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -221,7 +221,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { *_endOfLastWrite = *source; _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; @@ -231,7 +231,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { return samplesToCopy; } -int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { +int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -242,9 +242,9 @@ int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = (int16_t)((float)(*source) * fade); + *_endOfLastWrite = (Sample)((float)(*source) * fade); _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; ++source; } diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 29e7a9e998..b1f65b8a8d 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -21,15 +21,19 @@ const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10; -class AudioRingBuffer { +template +class AudioRingBufferTemplate { + using Sample = T; + static size_t SampleSize = sizeof(Sample); + public: - AudioRingBuffer(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); - ~AudioRingBuffer(); + AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); + ~AudioRingBufferTemplate(); // disallow copying - AudioRingBuffer(const AudioRingBuffer&) = delete; - AudioRingBuffer(AudioRingBuffer&&) = delete; - AudioRingBuffer& operator=(const AudioRingBuffer&) = delete; + AudioRingBufferTemplate(const AudioRingBufferTemplate&) = delete; + AudioRingBufferTemplate(AudioRingBufferTemplate&&) = delete; + AudioRingBufferTemplate& operator=(const AudioRingBufferTemplate&) = delete; /// Invalidate any data in the buffer void clear(); @@ -43,11 +47,11 @@ public: /// Read up to maxSamples into destination (will only read up to samplesAvailable()) /// Returns number of read samples - int readSamples(int16_t* destination, int maxSamples); + int readSamples(Sample* destination, int maxSamples); /// Write up to maxSamples from source (will only write up to sample capacity) /// Returns number of written samples - int writeSamples(const int16_t* source, int maxSamples); + int writeSamples(const Sample* source, int maxSamples); /// Write up to maxSamples silent samples (will only write until other data exists in the buffer) /// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow @@ -63,8 +67,8 @@ public: int writeData(const char* source, int maxSize); /// Returns a reference to the index-th sample offset from the current read sample - int16_t& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } - const int16_t& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + Sample& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + const Sample& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } /// Essentially discards the next numSamples from the ring buffer /// NOTE: This is not checked - it is possible to shift past written data @@ -85,36 +89,36 @@ public: class ConstIterator { public: ConstIterator(); - ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at); + ConstIterator(Sample* bufferFirst, int capacity, Sample* at); ConstIterator(const ConstIterator& rhs) = default; bool isNull() const { return _at == NULL; } bool operator==(const ConstIterator& rhs) { return _at == rhs._at; } bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; } - const int16_t& operator*() { return *_at; } + const Sample& operator*() { return *_at; } ConstIterator& operator=(const ConstIterator& rhs); ConstIterator& operator++(); ConstIterator operator++(int); ConstIterator& operator--(); ConstIterator operator--(int); - const int16_t& operator[] (int i); + const Sample& operator[] (int i); ConstIterator operator+(int i); ConstIterator operator-(int i); - void readSamples(int16_t* dest, int numSamples); - void readSamplesWithFade(int16_t* dest, int numSamples, float fade); - void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); - void readSamplesWithDownmix(int16_t* dest, int numSamples); + void readSamples(Sample* dest, int numSamples); + void readSamplesWithFade(Sample* dest, int numSamples, float fade); + void readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels); + void readSamplesWithDownmix(Sample* dest, int numSamples); private: - int16_t* atShiftedBy(int i); + Sample* atShiftedBy(int i); int _bufferLength; - int16_t* _bufferFirst; - int16_t* _bufferLast; - int16_t* _at; + Sample* _bufferFirst; + Sample* _bufferLast; + Sample* _at; }; ConstIterator nextOutput() const; @@ -126,8 +130,8 @@ public: float getFrameLoudness(ConstIterator frameStart) const; protected: - int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; - float getFrameLoudness(const int16_t* frameStart) const; + Sample* shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const; + float getFrameLoudness(const Sample* frameStart) const; int _numFrameSamples; int _frameCapacity; @@ -135,25 +139,28 @@ protected: int _bufferLength; // actual _buffer length (_sampleCapacity + 1) int _overflowCount{ 0 }; // times the ring buffer has overwritten data - int16_t* _nextOutput{ nullptr }; - int16_t* _endOfLastWrite{ nullptr }; - int16_t* _buffer{ nullptr }; + Sample* _nextOutput{ nullptr }; + Sample* _endOfLastWrite{ nullptr }; + Sample* _buffer{ nullptr }; }; +using AudioRingBuffer = AudioRingBufferTemplate; +using AudioRingMixBuffer = AudioRingBufferTemplate; + // inline the iterator: -inline AudioRingBuffer::ConstIterator::ConstIterator() : +inline AudioRingBufferTemplate::ConstIterator::ConstIterator() : _bufferLength(0), _bufferFirst(NULL), _bufferLast(NULL), _at(NULL) {} -inline AudioRingBuffer::ConstIterator::ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at) : +inline AudioRingBufferTemplate::ConstIterator::ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : _bufferLength(capacity), _bufferFirst(bufferFirst), _bufferLast(bufferFirst + capacity - 1), _at(at) {} -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator=(const ConstIterator& rhs) { +inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator=(const ConstIterator& rhs) { _bufferLength = rhs._bufferLength; _bufferFirst = rhs._bufferFirst; _bufferLast = rhs._bufferLast; @@ -161,41 +168,41 @@ inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator= return *this; } -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator++() { +inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator++() { _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; return *this; } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator++(int) { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator++(int) { ConstIterator tmp(*this); ++(*this); return tmp; } -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator--() { +inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator--() { _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; return *this; } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator--(int) { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator--(int) { ConstIterator tmp(*this); --(*this); return tmp; } -inline const int16_t& AudioRingBuffer::ConstIterator::operator[] (int i) { +inline const int16_t& AudioRingBufferTemplate::ConstIterator::operator[] (int i) { return *atShiftedBy(i); } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator+(int i) { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator+(int i) { return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator-(int i) { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator-(int i) { return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); } -inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) { +inline int16_t* AudioRingBufferTemplate::ConstIterator::atShiftedBy(int i) { i = (_at - _bufferFirst + i) % _bufferLength; if (i < 0) { i += _bufferLength; @@ -203,23 +210,23 @@ inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) { return _bufferFirst + i; } -inline void AudioRingBuffer::ConstIterator::readSamples(int16_t* dest, int numSamples) { +inline void AudioRingBufferTemplate::ConstIterator::readSamples(Sample* dest, int numSamples) { auto samplesToEnd = _bufferLast - _at + 1; if (samplesToEnd >= numSamples) { - memcpy(dest, _at, numSamples * sizeof(int16_t)); + memcpy(dest, _at, numSamples * SampleSize); _at += numSamples; } else { auto samplesFromStart = numSamples - samplesToEnd; - memcpy(dest, _at, samplesToEnd * sizeof(int16_t)); - memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * sizeof(int16_t)); + memcpy(dest, _at, samplesToEnd * SampleSize); + memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); _at = _bufferFirst + samplesFromStart; } } -inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, int numSamples, float fade) { - int16_t* at = _at; +inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithFade(Sample* dest, int numSamples, float fade) { + Sample* at = _at; for (int i = 0; i < numSamples; i++) { *dest = (float)*at * fade; ++dest; @@ -227,14 +234,14 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, i } } -inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { - int16_t* at = _at; +inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels) { + Sample* at = _at; for (int i = 0; i < numSamples/2; i++) { // read 2 samples - int16_t left = *at; + Sample left = *at; at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; + Sample right = *at; at = (at == _bufferLast) ? _bufferFirst : at + 1; // write 2 + N samples @@ -246,26 +253,26 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, } } -inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { - int16_t* at = _at; +inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithDownmix(Sample* dest, int numSamples) { + Sample* at = _at; for (int i = 0; i < numSamples/2; i++) { // read 2 samples - int16_t left = *at; + Sample left = *at; at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; + Sample right = *at; at = (at == _bufferLast) ? _bufferFirst : at + 1; // write 1 sample - *dest++ = (int16_t)((left + right) / 2); + *dest++ = (Sample)((left + right) / 2); } } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::nextOutput() const { return ConstIterator(_buffer, _bufferLength, _nextOutput); } -inline AudioRingBuffer::ConstIterator AudioRingBuffer::lastFrameWritten() const { +inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::lastFrameWritten() const { return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; } From 02e62938a46d10f2bb0ca752e6489b6b80c3f90c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 16:37:06 -0500 Subject: [PATCH 031/142] add AudioRingMixBuffer --- libraries/audio/src/AudioRingBuffer.cpp | 207 +++++++++++++++++++++++- libraries/audio/src/AudioRingBuffer.h | 48 +++--- 2 files changed, 236 insertions(+), 19 deletions(-) diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index c52d8f1447..ab0948e328 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,6 +26,7 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; +template<> AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), @@ -41,22 +42,26 @@ AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, i static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); -}; +} +template<> AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } +template<> void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } +template<> void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } +template<> void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; @@ -73,14 +78,17 @@ void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { reset(); } +template<> int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } +template<> int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { return writeData((char*)source, maxSamples * SampleSize) / SampleSize; } +template<> int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available int maxSamples = maxSize / SampleSize; @@ -104,6 +112,7 @@ int AudioRingBufferTemplate::readData(char *data, int maxSize) { return numReadSamples * SampleSize; } +template<> int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for int maxSamples = maxSize / SampleSize; @@ -137,6 +146,7 @@ int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { return numWriteSamples * SampleSize; } +template<> int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; @@ -149,6 +159,7 @@ int AudioRingBufferTemplate::samplesAvailable() const { return sampleDifference; } +template<> int16_t* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { @@ -162,6 +173,7 @@ int16_t* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sampl } } +template<> int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there int numWriteSamples = std::min(silentSamples, _sampleCapacity); @@ -186,6 +198,7 @@ int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { return numWriteSamples; } +template<> float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) float loudness = 0.0f; @@ -203,6 +216,7 @@ float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStar return loudness; } +template<> float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { if (frameStart.isNull()) { return 0.0f; @@ -210,6 +224,7 @@ float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStar return getFrameLoudness(&(*frameStart)); } +template<> int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -231,6 +246,7 @@ int AudioRingBufferTemplate::writeSamples(ConstIterator source, int max return samplesToCopy; } +template<> int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -251,3 +267,192 @@ int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, return samplesToCopy; } + +template<> +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : + _numFrameSamples(numFrameSamples), + _frameCapacity(numFramesCapacity), + _sampleCapacity(numFrameSamples * numFramesCapacity), + _bufferLength(numFrameSamples * (numFramesCapacity + 1)) +{ + if (numFrameSamples) { + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); + _nextOutput = _buffer; + _endOfLastWrite = _buffer; + } + + static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); + static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); +} + +template<> +AudioRingBufferTemplate::~AudioRingBufferTemplate() { + delete[] _buffer; +} + +template<> +void AudioRingBufferTemplate::clear() { + _endOfLastWrite = _buffer; + _nextOutput = _buffer; +} + +template<> +void AudioRingBufferTemplate::reset() { + clear(); + _overflowCount = 0; +} + +template<> +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { + delete[] _buffer; + _numFrameSamples = numFrameSamples; + _sampleCapacity = numFrameSamples * _frameCapacity; + _bufferLength = numFrameSamples * (_frameCapacity + 1); + + if (numFrameSamples) { + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); + } else { + _buffer = nullptr; + } + + reset(); +} + +template<> +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; +} + +template<> +int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { + if (append) { + return appendData((char*)destination, maxSamples * SampleSize) / SampleSize; + } else { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; + } +} + +template<> +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { + return writeData((char*)source, maxSamples * SampleSize) / SampleSize; +} + +template<> +int AudioRingBufferTemplate::readData(char *data, int maxSize) { + // only copy up to the number of samples we have available + int maxSamples = maxSize / SampleSize; + int numReadSamples = std::min(maxSamples, samplesAvailable()); + + if (_nextOutput + numReadSamples > _buffer + _bufferLength) { + // we're going to need to do two reads to get this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; + + // read to the end of the buffer + memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); + + // read the rest from the beginning of the buffer + memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); + } else { + memcpy(data, _nextOutput, numReadSamples * SampleSize); + } + + shiftReadPosition(numReadSamples); + + return numReadSamples * SampleSize; +} + +template<> +int AudioRingBufferTemplate::appendData(char *data, int maxSize) { + // only copy up to the number of samples we have available + int maxSamples = maxSize / SampleSize; + int numReadSamples = std::min(maxSamples, samplesAvailable()); + + Sample* dest = reinterpret_cast(data); + Sample* output = _nextOutput; + if (_nextOutput + numReadSamples > _buffer + _bufferLength) { + // we're going to need to do two reads to get this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; + + // read to the end of the buffer + for (int i = 0; i < numSamplesToEnd; i++) { + *dest++ = *output++; + } + + // read the rest from the beginning of the buffer + output = _buffer; + for (int i = 0; i < (numReadSamples - numSamplesToEnd); i++) { + *dest++ = *output++; + } + } else { + for (int i = 0; i < numReadSamples; i++) { + *dest++ = *output++; + } + } + + shiftReadPosition(numReadSamples); + + return numReadSamples * SampleSize; +} + +template<> +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { + // only copy up to the number of samples we have capacity for + int maxSamples = maxSize / SampleSize; + int numWriteSamples = std::min(maxSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + // there's not enough room for this write. erase old data to make room for this new data + int samplesToDelete = numWriteSamples - samplesRoomFor; + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); + _overflowCount++; + + qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + // we're going to need to do two writes to set this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + + // write to the end of the buffer + memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); + + // write the rest to the beginning of the buffer + memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples * SampleSize; +} + +template<> +int AudioRingBufferTemplate::samplesAvailable() const { + if (!_endOfLastWrite) { + return 0; + } + + int sampleDifference = _endOfLastWrite - _nextOutput; + if (sampleDifference < 0) { + sampleDifference += _bufferLength; + } + return sampleDifference; +} + +template<> +float* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { + // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur + if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { + // this shift will wrap the position around to the beginning of the ring + return position + numSamplesShift - _bufferLength; + } else if (numSamplesShift < 0 && position + numSamplesShift < _buffer) { + // this shift will go around to the end of the ring + return position + numSamplesShift + _bufferLength; + } else { + return position + numSamplesShift; + } +} \ No newline at end of file diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index b1f65b8a8d..92c6dcc336 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -24,7 +24,7 @@ const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10; template class AudioRingBufferTemplate { using Sample = T; - static size_t SampleSize = sizeof(Sample); + static const int SampleSize = sizeof(Sample); public: AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); @@ -49,6 +49,14 @@ public: /// Returns number of read samples int readSamples(Sample* destination, int maxSamples); + /// Append up to maxSamples into destination (will only read up to samplesAvailable()) + /// If append == false, behaves as readSamples + /// Returns number of appended samples + int appendSamples(Sample* destination, int maxSamples, bool append = true); + + /// Skip up to maxSamples (will only skip up to samplesAvailable()) + void skipSamples(int maxSamples) { shiftReadPosition(std::min(maxSamples, samplesAvailable())); } + /// Write up to maxSamples from source (will only write up to sample capacity) /// Returns number of written samples int writeSamples(const Sample* source, int maxSamples); @@ -62,6 +70,10 @@ public: /// Returns number of read bytes int readData(char* destination, int maxSize); + /// Append up to maxSize into destination + /// Returns number of read bytes + int appendData(char* destination, int maxSize); + /// Write up to maxSize from source /// Returns number of written bytes int writeData(const char* source, int maxSize); @@ -148,19 +160,19 @@ using AudioRingBuffer = AudioRingBufferTemplate; using AudioRingMixBuffer = AudioRingBufferTemplate; // inline the iterator: -inline AudioRingBufferTemplate::ConstIterator::ConstIterator() : +template<> inline AudioRingBufferTemplate::ConstIterator::ConstIterator() : _bufferLength(0), _bufferFirst(NULL), _bufferLast(NULL), _at(NULL) {} -inline AudioRingBufferTemplate::ConstIterator::ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : +template<> inline AudioRingBufferTemplate::ConstIterator::ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : _bufferLength(capacity), _bufferFirst(bufferFirst), _bufferLast(bufferFirst + capacity - 1), _at(at) {} -inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator=(const ConstIterator& rhs) { +template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator=(const ConstIterator& rhs) { _bufferLength = rhs._bufferLength; _bufferFirst = rhs._bufferFirst; _bufferLast = rhs._bufferLast; @@ -168,41 +180,41 @@ inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate< return *this; } -inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator++() { +template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator++() { _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; return *this; } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator++(int) { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator++(int) { ConstIterator tmp(*this); ++(*this); return tmp; } -inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator--() { +template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator--() { _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; return *this; } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator--(int) { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator--(int) { ConstIterator tmp(*this); --(*this); return tmp; } -inline const int16_t& AudioRingBufferTemplate::ConstIterator::operator[] (int i) { +template<> inline const int16_t& AudioRingBufferTemplate::ConstIterator::operator[] (int i) { return *atShiftedBy(i); } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator+(int i) { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator+(int i) { return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator-(int i) { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator-(int i) { return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); } -inline int16_t* AudioRingBufferTemplate::ConstIterator::atShiftedBy(int i) { +template<> inline int16_t* AudioRingBufferTemplate::ConstIterator::atShiftedBy(int i) { i = (_at - _bufferFirst + i) % _bufferLength; if (i < 0) { i += _bufferLength; @@ -210,7 +222,7 @@ inline int16_t* AudioRingBufferTemplate::ConstIterator::atShiftedBy(int return _bufferFirst + i; } -inline void AudioRingBufferTemplate::ConstIterator::readSamples(Sample* dest, int numSamples) { +template<> inline void AudioRingBufferTemplate::ConstIterator::readSamples(Sample* dest, int numSamples) { auto samplesToEnd = _bufferLast - _at + 1; if (samplesToEnd >= numSamples) { @@ -225,7 +237,7 @@ inline void AudioRingBufferTemplate::ConstIterator::readSamples(Sample* } } -inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithFade(Sample* dest, int numSamples, float fade) { +template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithFade(Sample* dest, int numSamples, float fade) { Sample* at = _at; for (int i = 0; i < numSamples; i++) { *dest = (float)*at * fade; @@ -234,7 +246,7 @@ inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithFade } } -inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels) { +template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels) { Sample* at = _at; for (int i = 0; i < numSamples/2; i++) { @@ -253,7 +265,7 @@ inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithUpmi } } -inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithDownmix(Sample* dest, int numSamples) { +template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithDownmix(Sample* dest, int numSamples) { Sample* at = _at; for (int i = 0; i < numSamples/2; i++) { @@ -268,11 +280,11 @@ inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithDown } } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::nextOutput() const { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::nextOutput() const { return ConstIterator(_buffer, _bufferLength, _nextOutput); } -inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::lastFrameWritten() const { +template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::lastFrameWritten() const { return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; } From 5927c089ac1368ce4e7bf94266eb8c8c8b7ba5b1 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 19 Jan 2017 16:37:31 -0500 Subject: [PATCH 032/142] use AudioRingMixBuffer as LocalInjectorsStream --- libraries/audio-client/src/AudioClient.cpp | 2 +- libraries/audio-client/src/AudioClient.h | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 491c4e341b..1fa6bd2d41 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1636,7 +1636,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { _localInjectorsStream.skipSamples(samplesRequested); } else { bool append = networkSamplesPopped > 0; - if ((injectorSamplesPopped = _localInjectorsStream.readSamples(mixBuffer, samplesRequested, append)) > 0) { + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 8befd86f26..103d8a0892 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,17 +69,6 @@ class QIODevice; class Transform; class NLPacket; -class LocalInjectorsStream { -public: - LocalInjectorsStream(int numFrameSamples); - int getSampleCapacity() { return 0; }; - int samplesAvailable() { return 0; } - int writeSamples(const float*, int numSamples) { return 0; } - void resizeForFrameSize(int numFrameSamples) {} - int skipSamples(int numSamples) { return 0; } - int readSamples(float* mixBuffer, int numSamples, bool append) { return 0; } -}; - class AudioInjectorsThread : public QThread { Q_OBJECT @@ -96,6 +85,8 @@ private: class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + using LocalInjectorsStream = AudioRingMixBuffer; public: static const int MIN_BUFFER_FRAMES; static const int MAX_BUFFER_FRAMES; From 75281099bd480298d65fc1fddc0a12bea9df3452 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 14:33:54 -0500 Subject: [PATCH 033/142] add conformant explicit instantiation for AudioRingBuffers --- libraries/audio/src/AudioRingBuffer.cpp | 395 ++++++++---------------- libraries/audio/src/AudioRingBuffer.h | 227 +++++--------- 2 files changed, 207 insertions(+), 415 deletions(-) diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index ab0948e328..4f64d4a4b0 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,8 +26,8 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; -template<> -AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : +template +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), _sampleCapacity(numFrameSamples * numFramesCapacity), @@ -44,25 +44,25 @@ AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, i static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); } -template<> -AudioRingBufferTemplate::~AudioRingBufferTemplate() { +template +AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } -template<> -void AudioRingBufferTemplate::clear() { +template +void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } -template<> -void AudioRingBufferTemplate::reset() { +template +void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } -template<> -void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { +template +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; _sampleCapacity = numFrameSamples * _frameCapacity; @@ -78,255 +78,13 @@ void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { reset(); } -template<> -int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { +template +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } -template<> -int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { - return writeData((char*)source, maxSamples * SampleSize) / SampleSize; -} - -template<> -int AudioRingBufferTemplate::readData(char *data, int maxSize) { - // only copy up to the number of samples we have available - int maxSamples = maxSize / SampleSize; - int numReadSamples = std::min(maxSamples, samplesAvailable()); - - if (_nextOutput + numReadSamples > _buffer + _bufferLength) { - // we're going to need to do two reads to get this data, it wraps around the edge - int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; - - // read to the end of the buffer - memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); - - // read the rest from the beginning of the buffer - memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); - } else { - memcpy(data, _nextOutput, numReadSamples * SampleSize); - } - - shiftReadPosition(numReadSamples); - - return numReadSamples * SampleSize; -} - -template<> -int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { - // only copy up to the number of samples we have capacity for - int maxSamples = maxSize / SampleSize; - int numWriteSamples = std::min(maxSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - // there's not enough room for this write. erase old data to make room for this new data - int samplesToDelete = numWriteSamples - samplesRoomFor; - _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); - _overflowCount++; - - qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - // we're going to need to do two writes to set this data, it wraps around the edge - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - - // write to the end of the buffer - memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); - - // write the rest to the beginning of the buffer - memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); - } else { - memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples * SampleSize; -} - -template<> -int AudioRingBufferTemplate::samplesAvailable() const { - if (!_endOfLastWrite) { - return 0; - } - - int sampleDifference = _endOfLastWrite - _nextOutput; - if (sampleDifference < 0) { - sampleDifference += _bufferLength; - } - return sampleDifference; -} - -template<> -int16_t* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { - // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur - if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { - // this shift will wrap the position around to the beginning of the ring - return position + numSamplesShift - _bufferLength; - } else if (numSamplesShift < 0 && position + numSamplesShift < _buffer) { - // this shift will go around to the end of the ring - return position + numSamplesShift + _bufferLength; - } else { - return position + numSamplesShift; - } -} - -template<> -int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { - // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there - int numWriteSamples = std::min(silentSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - numWriteSamples = samplesRoomFor; - - qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); - memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); - } else { - memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples; -} - -template<> -float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { - // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) - float loudness = 0.0f; - const Sample* sampleAt = frameStart; - const Sample* bufferLastAt = _buffer + _bufferLength - 1; - - for (int i = 0; i < _numFrameSamples; ++i) { - loudness += (float) std::abs(*sampleAt); - // wrap if necessary - sampleAt = sampleAt == bufferLastAt ? _buffer : sampleAt + 1; - } - loudness /= _numFrameSamples; - loudness /= AudioConstants::MAX_SAMPLE_VALUE; - - return loudness; -} - -template<> -float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { - if (frameStart.isNull()) { - return 0.0f; - } - return getFrameLoudness(&(*frameStart)); -} - -template<> -int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { - int samplesToCopy = std::min(maxSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - if (samplesToCopy > samplesRoomFor) { - // there's not enough room for this write. erase old data to make room for this new data - int samplesToDelete = samplesToCopy - samplesRoomFor; - _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); - _overflowCount++; - qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); - } - - Sample* bufferLast = _buffer + _bufferLength - 1; - for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = *source; - _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; - ++source; - } - - return samplesToCopy; -} - -template<> -int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { - int samplesToCopy = std::min(maxSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - if (samplesToCopy > samplesRoomFor) { - // there's not enough room for this write. erase old data to make room for this new data - int samplesToDelete = samplesToCopy - samplesRoomFor; - _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); - _overflowCount++; - qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); - } - - Sample* bufferLast = _buffer + _bufferLength - 1; - for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = (Sample)((float)(*source) * fade); - _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; - ++source; - } - - return samplesToCopy; -} - -template<> -AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : - _numFrameSamples(numFrameSamples), - _frameCapacity(numFramesCapacity), - _sampleCapacity(numFrameSamples * numFramesCapacity), - _bufferLength(numFrameSamples * (numFramesCapacity + 1)) -{ - if (numFrameSamples) { - _buffer = new Sample[_bufferLength]; - memset(_buffer, 0, _bufferLength * SampleSize); - _nextOutput = _buffer; - _endOfLastWrite = _buffer; - } - - static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); - static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); -} - -template<> -AudioRingBufferTemplate::~AudioRingBufferTemplate() { - delete[] _buffer; -} - -template<> -void AudioRingBufferTemplate::clear() { - _endOfLastWrite = _buffer; - _nextOutput = _buffer; -} - -template<> -void AudioRingBufferTemplate::reset() { - clear(); - _overflowCount = 0; -} - -template<> -void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { - delete[] _buffer; - _numFrameSamples = numFrameSamples; - _sampleCapacity = numFrameSamples * _frameCapacity; - _bufferLength = numFrameSamples * (_frameCapacity + 1); - - if (numFrameSamples) { - _buffer = new Sample[_bufferLength]; - memset(_buffer, 0, _bufferLength * SampleSize); - } else { - _buffer = nullptr; - } - - reset(); -} - -template<> -int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { - return readData((char*)destination, maxSamples * SampleSize) / SampleSize; -} - -template<> -int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { +template +int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { if (append) { return appendData((char*)destination, maxSamples * SampleSize) / SampleSize; } else { @@ -334,13 +92,13 @@ int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSa } } -template<> -int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { +template +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { return writeData((char*)source, maxSamples * SampleSize) / SampleSize; } -template<> -int AudioRingBufferTemplate::readData(char *data, int maxSize) { +template +int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); @@ -363,8 +121,8 @@ int AudioRingBufferTemplate::readData(char *data, int maxSize) { return numReadSamples * SampleSize; } -template<> -int AudioRingBufferTemplate::appendData(char *data, int maxSize) { +template +int AudioRingBufferTemplate::appendData(char *data, int maxSize) { // only copy up to the number of samples we have available int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); @@ -396,8 +154,8 @@ int AudioRingBufferTemplate::appendData(char *data, int maxSize) { return numReadSamples * SampleSize; } -template<> -int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { +template +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for int maxSamples = maxSize / SampleSize; int numWriteSamples = std::min(maxSamples, _sampleCapacity); @@ -430,8 +188,8 @@ int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { return numWriteSamples * SampleSize; } -template<> -int AudioRingBufferTemplate::samplesAvailable() const { +template +int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; } @@ -443,8 +201,8 @@ int AudioRingBufferTemplate::samplesAvailable() const { return sampleDifference; } -template<> -float* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { +template +typename AudioRingBufferTemplate::Sample* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { // this shift will wrap the position around to the beginning of the ring @@ -455,4 +213,103 @@ float* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* p } else { return position + numSamplesShift; } -} \ No newline at end of file +} + +template +int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { + // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there + int numWriteSamples = std::min(silentSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + numWriteSamples = samplesRoomFor; + + qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); + memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples; +} + +template +float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { + // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) + float loudness = 0.0f; + const Sample* sampleAt = frameStart; + const Sample* bufferLastAt = _buffer + _bufferLength - 1; + + for (int i = 0; i < _numFrameSamples; ++i) { + loudness += (float) std::abs(*sampleAt); + // wrap if necessary + sampleAt = sampleAt == bufferLastAt ? _buffer : sampleAt + 1; + } + loudness /= _numFrameSamples; + loudness /= AudioConstants::MAX_SAMPLE_VALUE; + + return loudness; +} + +template +float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { + if (frameStart.isNull()) { + return 0.0f; + } + return getFrameLoudness(&(*frameStart)); +} + +template +int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { + int samplesToCopy = std::min(maxSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (samplesToCopy > samplesRoomFor) { + // there's not enough room for this write. erase old data to make room for this new data + int samplesToDelete = samplesToCopy - samplesRoomFor; + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); + _overflowCount++; + qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); + } + + Sample* bufferLast = _buffer + _bufferLength - 1; + for (int i = 0; i < samplesToCopy; i++) { + *_endOfLastWrite = *source; + _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; + ++source; + } + + return samplesToCopy; +} + +template +int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { + int samplesToCopy = std::min(maxSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (samplesToCopy > samplesRoomFor) { + // there's not enough room for this write. erase old data to make room for this new data + int samplesToDelete = samplesToCopy - samplesRoomFor; + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); + _overflowCount++; + qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); + } + + Sample* bufferLast = _buffer + _bufferLength - 1; + for (int i = 0; i < samplesToCopy; i++) { + *_endOfLastWrite = (Sample)((float)(*source) * fade); + _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; + ++source; + } + + return samplesToCopy; +} + +// explicit instantiations for scratch/mix buffers +template class AudioRingBufferTemplate; +template class AudioRingBufferTemplate; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 92c6dcc336..0208c8686e 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -100,8 +100,16 @@ public: class ConstIterator { public: - ConstIterator(); - ConstIterator(Sample* bufferFirst, int capacity, Sample* at); + ConstIterator() : + _bufferLength(0), + _bufferFirst(NULL), + _bufferLast(NULL), + _at(NULL) {} + ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : + _bufferLength(capacity), + _bufferFirst(bufferFirst), + _bufferLast(bufferFirst + capacity - 1), + _at(at) {} ConstIterator(const ConstIterator& rhs) = default; bool isNull() const { return _at == NULL; } @@ -110,22 +118,73 @@ public: bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; } const Sample& operator*() { return *_at; } - ConstIterator& operator=(const ConstIterator& rhs); - ConstIterator& operator++(); - ConstIterator operator++(int); - ConstIterator& operator--(); - ConstIterator operator--(int); - const Sample& operator[] (int i); - ConstIterator operator+(int i); - ConstIterator operator-(int i); + ConstIterator& operator=(const ConstIterator& rhs) { + _bufferLength = rhs._bufferLength; + _bufferFirst = rhs._bufferFirst; + _bufferLast = rhs._bufferLast; + _at = rhs._at; + return *this; + } + ConstIterator& operator++() { + _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; + return *this; + } + ConstIterator operator++(int) { + ConstIterator tmp(*this); + ++(*this); + return tmp; + } + ConstIterator& operator--() { + _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; + return *this; + } + ConstIterator operator--(int) { + ConstIterator tmp(*this); + --(*this); + return tmp; + } + const Sample& operator[] (int i) { + return *atShiftedBy(i); + } + ConstIterator operator+(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); + } + ConstIterator operator-(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); + } + + void readSamples(Sample* dest, int numSamples) { + auto samplesToEnd = _bufferLast - _at + 1; + + if (samplesToEnd >= numSamples) { + memcpy(dest, _at, numSamples * SampleSize); + _at += numSamples; + } else { + auto samplesFromStart = numSamples - samplesToEnd; + memcpy(dest, _at, samplesToEnd * SampleSize); + memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); + + _at = _bufferFirst + samplesFromStart; + } + } + void readSamplesWithFade(Sample* dest, int numSamples, float fade) { + Sample* at = _at; + for (int i = 0; i < numSamples; i++) { + *dest = (float)*at * fade; + ++dest; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + } + } - void readSamples(Sample* dest, int numSamples); - void readSamplesWithFade(Sample* dest, int numSamples, float fade); - void readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels); - void readSamplesWithDownmix(Sample* dest, int numSamples); private: - Sample* atShiftedBy(int i); + Sample* atShiftedBy(int i) { + i = (_at - _bufferFirst + i) % _bufferLength; + if (i < 0) { + i += _bufferLength; + } + return _bufferFirst + i; + } int _bufferLength; Sample* _bufferFirst; @@ -133,8 +192,12 @@ public: Sample* _at; }; - ConstIterator nextOutput() const; - ConstIterator lastFrameWritten() const; + ConstIterator nextOutput() const { + return ConstIterator(_buffer, _bufferLength, _nextOutput); + } + ConstIterator lastFrameWritten() const { + return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; + } int writeSamples(ConstIterator source, int maxSamples); int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade); @@ -156,136 +219,8 @@ protected: Sample* _buffer{ nullptr }; }; +// expose explicit instantiations for scratch/mix buffers using AudioRingBuffer = AudioRingBufferTemplate; using AudioRingMixBuffer = AudioRingBufferTemplate; -// inline the iterator: -template<> inline AudioRingBufferTemplate::ConstIterator::ConstIterator() : - _bufferLength(0), - _bufferFirst(NULL), - _bufferLast(NULL), - _at(NULL) {} - -template<> inline AudioRingBufferTemplate::ConstIterator::ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : - _bufferLength(capacity), - _bufferFirst(bufferFirst), - _bufferLast(bufferFirst + capacity - 1), - _at(at) {} - -template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator=(const ConstIterator& rhs) { - _bufferLength = rhs._bufferLength; - _bufferFirst = rhs._bufferFirst; - _bufferLast = rhs._bufferLast; - _at = rhs._at; - return *this; -} - -template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator++() { - _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; - return *this; -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator++(int) { - ConstIterator tmp(*this); - ++(*this); - return tmp; -} - -template<> inline AudioRingBufferTemplate::ConstIterator& AudioRingBufferTemplate::ConstIterator::operator--() { - _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; - return *this; -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator--(int) { - ConstIterator tmp(*this); - --(*this); - return tmp; -} - -template<> inline const int16_t& AudioRingBufferTemplate::ConstIterator::operator[] (int i) { - return *atShiftedBy(i); -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator+(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::ConstIterator::operator-(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); -} - -template<> inline int16_t* AudioRingBufferTemplate::ConstIterator::atShiftedBy(int i) { - i = (_at - _bufferFirst + i) % _bufferLength; - if (i < 0) { - i += _bufferLength; - } - return _bufferFirst + i; -} - -template<> inline void AudioRingBufferTemplate::ConstIterator::readSamples(Sample* dest, int numSamples) { - auto samplesToEnd = _bufferLast - _at + 1; - - if (samplesToEnd >= numSamples) { - memcpy(dest, _at, numSamples * SampleSize); - _at += numSamples; - } else { - auto samplesFromStart = numSamples - samplesToEnd; - memcpy(dest, _at, samplesToEnd * SampleSize); - memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); - - _at = _bufferFirst + samplesFromStart; - } -} - -template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithFade(Sample* dest, int numSamples, float fade) { - Sample* at = _at; - for (int i = 0; i < numSamples; i++) { - *dest = (float)*at * fade; - ++dest; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - } -} - -template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithUpmix(Sample* dest, int numSamples, int numExtraChannels) { - Sample* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - Sample left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - Sample right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -template<> inline void AudioRingBufferTemplate::ConstIterator::readSamplesWithDownmix(Sample* dest, int numSamples) { - Sample* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - Sample left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - Sample right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 1 sample - *dest++ = (Sample)((left + right) / 2); - } -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::nextOutput() const { - return ConstIterator(_buffer, _bufferLength, _nextOutput); -} - -template<> inline AudioRingBufferTemplate::ConstIterator AudioRingBufferTemplate::lastFrameWritten() const { - return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; -} - #endif // hifi_AudioRingBuffer_h From 280ed04f74fe386d619f6a3e737c50850731c35b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 14:35:43 -0500 Subject: [PATCH 034/142] fix unused warning for channel up/downmix --- libraries/audio-client/src/AudioClient.cpp | 28 ++++++++++++++++++++ libraries/shared/src/AudioHelpers.h | 30 ---------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1fa6bd2d41..e94e1f4385 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -117,6 +117,34 @@ void AudioInjectorsThread::prepare() { _audio->prepareLocalAudioInjectors(); } +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), diff --git a/libraries/shared/src/AudioHelpers.h b/libraries/shared/src/AudioHelpers.h index b0fcb7248d..c891876648 100644 --- a/libraries/shared/src/AudioHelpers.h +++ b/libraries/shared/src/AudioHelpers.h @@ -103,34 +103,4 @@ static inline void convertToScratch(int16_t* scratchBuffer, const float* mixBuff } } -static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - #endif // hifi_AudioHelpers_h From bb247fe8a39339bd2c4f68aed589a6b835c83aaa Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 14:48:54 -0500 Subject: [PATCH 035/142] rename AudioMixRingBuffer --- libraries/audio-client/src/AudioClient.h | 2 +- libraries/audio/src/AudioRingBuffer.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 103d8a0892..b4ae84754a 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -86,7 +86,7 @@ class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - using LocalInjectorsStream = AudioRingMixBuffer; + using LocalInjectorsStream = AudioMixRingBuffer; public: static const int MIN_BUFFER_FRAMES; static const int MAX_BUFFER_FRAMES; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 0208c8686e..e43fd22548 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -221,6 +221,6 @@ protected: // expose explicit instantiations for scratch/mix buffers using AudioRingBuffer = AudioRingBufferTemplate; -using AudioRingMixBuffer = AudioRingBufferTemplate; +using AudioMixRingBuffer = AudioRingBufferTemplate; #endif // hifi_AudioRingBuffer_h From afc85a3afe7b79e5030dc0a550472722a2ea43ab Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Fri, 20 Jan 2017 13:53:09 -0800 Subject: [PATCH 036/142] Don't display users who aren't in a domain --- scripts/system/html/users.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 79590f6443..1f60675b00 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -71,8 +71,13 @@ function displayUsers(data) { $("#users-list").empty(); for (var i = 0; i < data.users.length; i++) { - console.log(data.users[i].username + " @ " + data.users[i].location.root.name); - $("#users-list").append("
" + data.users[i].username + " @ " + data.users[i].location.root.name + "
"); + // Don't display users who aren't in a domain + if (typeof data.users[i].location.root.name === "undefined") { + console.log(data.users[i].username + "is online but not in a domain"); + } else { + console.log(data.users[i].username + " @ " + data.users[i].location.root.name); + $("#users-list").append("
" + data.users[i].username + " @ " + data.users[i].location.root.name + "
"); + } } } From 685483b924d0658288788e7336f0ab82445ce009 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 17:52:03 -0500 Subject: [PATCH 037/142] do not omit local audio when echoing server audio --- libraries/audio-client/src/AudioClient.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index e94e1f4385..62dc5c527b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1659,14 +1659,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int injectorSamplesPopped = 0; { Lock lock(_audio->_localAudioMutex); - if (_audio->_shouldEchoToServer) { - // omit local audio, it should be echoed - _localInjectorsStream.skipSamples(samplesRequested); - } else { - bool append = networkSamplesPopped > 0; - if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); - } + bool append = networkSamplesPopped > 0; + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); } } From df051ff8df4a3b4bbb165361e7f3f29b2709ecfb Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 18:37:46 -0500 Subject: [PATCH 038/142] maintain network audio in int16_t --- libraries/audio-client/src/AudioClient.cpp | 21 +++++++++++---------- libraries/audio-client/src/AudioClient.h | 1 - 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 62dc5c527b..aff8406897 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1248,23 +1248,24 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - convertToMix(_networkMixBuffer, decodedSamples, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - - // apply stereo reverb bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); + + // apply stereo reverb if (hasReverb) { updateReverbOptions(); - _listenerReverb.render(_networkMixBuffer, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } + // resample to output sample rate if (_networkToOutputResampler) { - convertToScratch(_networkScratchBuffer, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } - // resample to output sample rate - _networkToOutputResampler->render(_networkScratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - } else { - convertToScratch(outputSamples, _networkMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b4ae84754a..b0eaa2dd34 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -318,7 +318,6 @@ private: AudioSRC* _localToOutputResampler; // for network audio (used by network audio thread) - float _networkMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) From 72f8fa49f9f7febaf66ca6cb358d8cdbc63c1d1c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 20 Jan 2017 18:38:21 -0500 Subject: [PATCH 039/142] inline audio convertToFloat --- libraries/audio-client/src/AudioClient.cpp | 11 ++++++++--- libraries/shared/src/AudioHelpers.h | 12 ------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index aff8406897..b12c48ef82 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -46,7 +46,6 @@ #include #include "PositionalAudioStream.h" -#include "AudioHelpers.h" #include "AudioClientLogging.h" #include "AudioLogging.h" @@ -145,6 +144,10 @@ static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { } } +static inline float convertToFloat(int16_t sample) { + return (float)sample * (1 / 32768.0f); +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), @@ -1201,7 +1204,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += (float)_localScratchBuffer[i] * (1/32768.0f) * gain; + mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } } else { @@ -1652,7 +1655,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - convertToMix(mixBuffer, scratchBuffer, networkSamplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); + } samplesRequested = networkSamplesPopped; } diff --git a/libraries/shared/src/AudioHelpers.h b/libraries/shared/src/AudioHelpers.h index c891876648..b43764ef5d 100644 --- a/libraries/shared/src/AudioHelpers.h +++ b/libraries/shared/src/AudioHelpers.h @@ -91,16 +91,4 @@ static inline float unpackFloatGainFromByte(uint8_t byte) { return gain; } -static inline void convertToMix(float* mixBuffer, const int16_t* scratchBuffer, int numSamples) { - for (int i = 0; i < numSamples; i++) { - mixBuffer[i] = (float)scratchBuffer[i] * (1/32768.0f); - } -} - -static inline void convertToScratch(int16_t* scratchBuffer, const float* mixBuffer, int numSamples) { - for (int i = 0; i < numSamples; i++) { - scratchBuffer[i] = (int16_t)(mixBuffer[i] * 32768.0f); - } -} - #endif // hifi_AudioHelpers_h From 061668cba41bb76a77d0147cec536a724ba960ef Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Sat, 21 Jan 2017 19:10:34 -0500 Subject: [PATCH 040/142] use lock-free pipe for local audio in device callback --- libraries/audio-client/src/AudioClient.cpp | 13 +++++++++++-- libraries/audio-client/src/AudioClient.h | 3 +++ libraries/audio/src/AudioRingBuffer.h | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b12c48ef82..2e532d67bf 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -190,6 +190,9 @@ AudioClient::AudioClient() : _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { + // avoid putting a lock in the device callback + assert(_localSamplesAvailable.is_lock_free()); + // deprecate legacy settings { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); @@ -1105,7 +1108,8 @@ void AudioClient::prepareLocalAudioInjectors() { int bufferCapacity = _localInjectorsStream.getSampleCapacity(); if (_localToOutputResampler) { - // avoid overwriting the buffer + // avoid overwriting the buffer, + // instead of failing on writes because the buffer is used as a lock-free pipe bufferCapacity -= _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * AudioConstants::STEREO; @@ -1115,9 +1119,10 @@ void AudioClient::prepareLocalAudioInjectors() { int samplesNeeded = std::numeric_limits::max(); while (samplesNeeded > 0) { // lock for every write to avoid locking out the device callback + // this lock is intentional - the buffer is only lock-free in its use in the device callback Lock lock(_localAudioMutex); - samplesNeeded = bufferCapacity - _localInjectorsStream.samplesAvailable(); + samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); if (samplesNeeded <= 0) { break; } @@ -1149,6 +1154,7 @@ void AudioClient::prepareLocalAudioInjectors() { AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } + _localSamplesAvailable.fetch_add(samples, std::memory_order_release); samplesNeeded -= samples; } } @@ -1452,6 +1458,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice bool supportedFormat = false; Lock lock(_localAudioMutex); + _localSamplesAvailable.exchange(0, std::memory_order_release); // cleanup any previously initialized device if (_audioOutput) { @@ -1666,7 +1673,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { { Lock lock(_audio->_localAudioMutex); bool append = networkSamplesPopped > 0; + samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b0eaa2dd34..699ba71ef7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -280,6 +280,9 @@ private: QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; LocalInjectorsStream _localInjectorsStream; + // In order to use _localInjectorsStream as a lock-free pipe, + // use it with a single producer/consumer, and track available samples + std::atomic _localSamplesAvailable { 0 }; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index e43fd22548..bb32df19a2 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -45,6 +45,12 @@ public: // FIXME: discards any data in the buffer void resizeForFrameSize(int numFrameSamples); + // Reading and writing to the buffer uses minimal shared data, such that + // in cases that avoid overwriting the buffer, a single producer/consumer + // may use this as a lock-free pipe (see audio-client/src/AudioClient.cpp). + // IMPORTANT: Avoid changes to the implementation that touch shared data unless you can + // maintain this behavior. + /// Read up to maxSamples into destination (will only read up to samplesAvailable()) /// Returns number of read samples int readSamples(Sample* destination, int maxSamples); From f8162970ec7223ed13790aa4fc07dacc6d366f03 Mon Sep 17 00:00:00 2001 From: Faye Li Si Fi Date: Mon, 23 Jan 2017 15:24:33 -0800 Subject: [PATCH 041/142] filter out yourself from user list --- scripts/system/html/users.html | 35 +++++++++++++++++++++++++--------- scripts/system/users.js | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 1f60675b00..569d352ba4 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -67,6 +67,7 @@ \ No newline at end of file diff --git a/scripts/system/users.js b/scripts/system/users.js index c55bccf961..0c10b1384d 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -11,7 +11,7 @@ // (function() { // BEGIN LOCAL_SCOPE - var USERS_URL = Script.resolvePath("html/users.html"); + var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ // TODO: work with Alan to make new icon art @@ -24,7 +24,20 @@ } function onWebEventReceived(event) { - print(event); + print("Script received a web event, its type is " + typeof event); + if (typeof event === "string") { + event = JSON.parse(event); + } + if (event.type === "ready") { + // send username to html + var myUsername = GlobalServices.username; + var object = { + "type": "sendUsername", + "data": {"username": myUsername} + }; + print("sending username: " + myUsername); + tablet.emitScriptEvent(JSON.stringify(object)); + } } button.clicked.connect(onClicked); From c7a0e609fc9d10aac529621211cff16a5559041c Mon Sep 17 00:00:00 2001 From: Faye Li Date: Wed, 25 Jan 2017 11:41:07 -0800 Subject: [PATCH 042/142] created a friends button for metaverse friends page --- scripts/defaultScripts.js | 1 + scripts/system/friends.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 scripts/system/friends.js diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd3131f4ff..2b52dae9b2 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -15,6 +15,7 @@ var DEFAULT_SCRIPTS = [ "system/progress.js", "system/away.js", "system/users.js", + "system/friends.js", "system/mute.js", "system/goto.js", "system/hmd.js", diff --git a/scripts/system/friends.js b/scripts/system/friends.js new file mode 100644 index 0000000000..6afef1579f --- /dev/null +++ b/scripts/system/friends.js @@ -0,0 +1,33 @@ +"use strict"; + +// +// friends.js +// +// Created by Faye Li on 25 Jan 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + var FRIENDS_URL = "https://metaverse.highfidelity.com/user/friends"; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/people-i.svg", + text: "Friends" + }); + + function onClicked() { + tablet.gotoWebScreen(FRIENDS_URL); + } + + button.clicked.connect(onClicked); + + function cleanup() { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + } + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE From f02b2ef40ab03142cab93a42f18c1979580c6ea2 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Wed, 25 Jan 2017 11:59:31 -0800 Subject: [PATCH 043/142] auto poll users when page loads --- scripts/system/html/users.html | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 569d352ba4..20c020dbb8 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -115,6 +115,7 @@ $(document).ready(function() { $("#dev-div").append("

ready

"); + pollUsers(); // Listen for events from hifi EventBridge.scriptEventReceived.connect(onScriptEventReceived); // Send a ready event to hifi From ce0ee610eaf7855bb226411e0b91bb09e750eaaf Mon Sep 17 00:00:00 2001 From: Faye Li Date: Wed, 25 Jan 2017 14:32:36 -0800 Subject: [PATCH 044/142] a test on polling friends --- scripts/system/html/users-friends.html | 126 +++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 scripts/system/html/users-friends.html diff --git a/scripts/system/html/users-friends.html b/scripts/system/html/users-friends.html new file mode 100644 index 0000000000..8890b2a109 --- /dev/null +++ b/scripts/system/html/users-friends.html @@ -0,0 +1,126 @@ + + + + Users Online + + + + + + +
+
+
Users Online (Friends)
+ +
+
+
+
+
+ + + + \ No newline at end of file From 5ad522668a9e36204efb4bc29dddbe3f70178ca7 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Wed, 25 Jan 2017 16:29:25 -0800 Subject: [PATCH 045/142] tabs header for everyone and friends --- scripts/system/html/users.html | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 20c020dbb8..86f8494512 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -47,6 +47,21 @@ padding: 30px; } + .tabs { + list-style: none; + padding: 0; + margin: 0; + } + + .tabs li { + display: inline-block; + padding: 10px 15px; + } + + .tabs li.current { + background: rgba(255,255,255,0.15); + } + #users-list div { padding-top: 4px; padding-bottom: 4px; @@ -62,7 +77,14 @@
-
+
    +
  • Everyone (10)
  • +
  • Friends (2)
  • +
+
+
+
+
@@ -190,6 +191,13 @@ // Send a ready event to hifi var eventObject = {"type": "ready"}; EventBridge.emitWebEvent(JSON.stringify(eventObject)); + + // Click listener mangage friends button + $("#friends-button").click(function() { + // Send a manage friends event to hifi + eventObject = {"type": "manage-friends"}; + EventBridge.emitWebEvent(JSON.stringify(eventObject)); + }); }); diff --git a/scripts/system/users.js b/scripts/system/users.js index 0c10b1384d..7930892395 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -12,9 +12,12 @@ (function() { // BEGIN LOCAL_SCOPE var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; + var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; + var FRIENDS_WINDOW_WIDTH = 290; + var FRIENDS_WINDOW_HEIGHT = 500; + var FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - // TODO: work with Alan to make new icon art icon: "icons/tablet-icons/people-i.svg", text: "Users" }); @@ -37,7 +40,19 @@ }; print("sending username: " + myUsername); tablet.emitScriptEvent(JSON.stringify(object)); - } + } + if (event.type === "manage-friends") { + // open a web overlay to metaverse friends page + var friendsWindow = new OverlayWebWindow({ + title: FRIENDS_WINDOW_TITLE, + width: FRIENDS_WINDOW_WIDTH, + height: FRIENDS_WINDOW_HEIGHT, + visible: false + }); + friendsWindow.setURL(FRIENDS_WINDOW_URL); + friendsWindow.setVisible(true); + friendsWindow.raise(); + } } button.clicked.connect(onClicked); From 8091b0ced4bfce8d5436dd93653eaf7b074f4bed Mon Sep 17 00:00:00 2001 From: Faye Li Date: Fri, 27 Jan 2017 13:33:07 -0800 Subject: [PATCH 054/142] button style --- scripts/system/html/users.html | 39 +++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 947642331c..23de9cf0b0 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -81,6 +81,43 @@ padding: 2px 0px; } + input[type=button] { + font-family: 'Raleway'; + font-weight: bold; + font-size: 13px; + text-transform: uppercase; + vertical-align: top; + height: 28px; + min-width: 120px; + padding: 0px 18px; + margin-right: 6px; + border-radius: 5px; + border: none; + color: #fff; + background-color: #000; + background: linear-gradient(#343434 20%, #000 100%); + cursor: pointer; + } + + input[type=button].blue { + color: #fff; + background-color: #1080b8; + background: linear-gradient(#00b4ef 20%, #1080b8 100%); + } + + input[type=button].blue:hover { + background: linear-gradient(#00b4ef, #00b4ef); + border: none; + } + + input[type=button].blue:active { + background: linear-gradient(#1080b8, #1080b8); + } + + #friends-button { + margin: 0px 0px 15px 10px; + } + @@ -100,7 +137,7 @@
    - +
    From ea366dd227748aefd1e1d99edaa795c199b659e0 Mon Sep 17 00:00:00 2001 From: Faye Li Date: Fri, 27 Jan 2017 15:38:49 -0800 Subject: [PATCH 055/142] removed unused code --- scripts/defaultScripts.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2b52dae9b2..bd3131f4ff 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -15,7 +15,6 @@ var DEFAULT_SCRIPTS = [ "system/progress.js", "system/away.js", "system/users.js", - "system/friends.js", "system/mute.js", "system/goto.js", "system/hmd.js", From ff1c4995af7ea4b7c68cae55375a2a4a9830589f Mon Sep 17 00:00:00 2001 From: Faye Li Date: Fri, 27 Jan 2017 15:44:01 -0800 Subject: [PATCH 056/142] removed unused code --- scripts/system/friends.js | 33 ------- scripts/system/html/users-friends.html | 126 ------------------------- 2 files changed, 159 deletions(-) delete mode 100644 scripts/system/friends.js delete mode 100644 scripts/system/html/users-friends.html diff --git a/scripts/system/friends.js b/scripts/system/friends.js deleted file mode 100644 index 6afef1579f..0000000000 --- a/scripts/system/friends.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -// -// friends.js -// -// Created by Faye Li on 25 Jan 2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { // BEGIN LOCAL_SCOPE - var FRIENDS_URL = "https://metaverse.highfidelity.com/user/friends"; - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var button = tablet.addButton({ - icon: "icons/tablet-icons/people-i.svg", - text: "Friends" - }); - - function onClicked() { - tablet.gotoWebScreen(FRIENDS_URL); - } - - button.clicked.connect(onClicked); - - function cleanup() { - button.clicked.disconnect(onClicked); - tablet.removeButton(button); - } - - Script.scriptEnding.connect(cleanup); -}()); // END LOCAL_SCOPE diff --git a/scripts/system/html/users-friends.html b/scripts/system/html/users-friends.html deleted file mode 100644 index 8890b2a109..0000000000 --- a/scripts/system/html/users-friends.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - Users Online - - - - - - -
    -
    -
    Users Online (Friends)
    - -
    -
    -
    -
    -
    - - - - \ No newline at end of file From 8f4586b9b8e66eb1317885456ed52464db50d854 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 28 Jan 2017 10:46:50 -0500 Subject: [PATCH 057/142] hide webview context menu when clicked --- interface/resources/QtWebEngine/UIDelegates/Menu.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 21c5e71394..5176d9d11e 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -33,6 +33,7 @@ Item { propagateComposedEvents: true acceptedButtons: "AllButtons" onClicked: { + menu.visible = false; menu.done(); mouse.accepted = false; } From 8c0d7f9e28fce4215c8f2e1ec378684640104764 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 28 Jan 2017 14:37:49 -0500 Subject: [PATCH 058/142] hide context menu on item click --- interface/resources/QtWebEngine/UIDelegates/MenuItem.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml index 561a8926e1..1890fcb81d 100644 --- a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml +++ b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml @@ -32,6 +32,7 @@ Item { MouseArea { anchors.fill: parent onClicked: { + menu.visible = false; root.triggered(); menu.done(); } From 93414d802d740cfaa99d974f548924b5199ff381 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 30 Jan 2017 11:06:36 -0800 Subject: [PATCH 059/142] fetch entity filter script asynchronously (but reject edits while waiting) --- assignment-client/src/entities/EntityServer.cpp | 17 ++++------------- assignment-client/src/entities/EntityServer.h | 1 - libraries/entities/src/EntityTree.cpp | 12 ++++++++++-- libraries/entities/src/EntityTree.h | 1 + 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 83a1b1ef84..dc1a693590 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -294,7 +294,9 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio } if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) { - // Fetch script from file synchronously. We don't want the server processing edits while a restarting entity server is fetching from a DOS'd source. + // Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up. + std::static_pointer_cast(_tree)->setHasEntityFilter(true); + // Now fetch script from file asynchronously. QUrl scriptURL(_entityEditFilter); // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) @@ -315,8 +317,6 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); scriptRequest->send(); qDebug() << "script request sent"; - _scriptRequestLoop.exec(); // Block here, but allow the request to be processed and its signals to be handled. - qDebug() << "script request event loop complete"; } } @@ -367,11 +367,7 @@ void EntityServer::scriptRequestFinished() { return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter); }); scriptRequest->deleteLater(); - qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning(); - if (_scriptRequestLoop.isRunning()) { - _scriptRequestLoop.quit(); - } - qDebug() << "script request event loop quit"; + qDebug() << "script request filter processed"; return; } } @@ -386,11 +382,6 @@ void EntityServer::scriptRequestFinished() { // Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities. qDebug() << "script request failure causing stop"; stop(); - qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning(); - if (_scriptRequestLoop.isRunning()) { - _scriptRequestLoop.quit(); - } - qDebug() << "script request event loop quit"; } void EntityServer::nodeAdded(SharedNodePointer node) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 25270c9dd5..f142145d5f 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -80,7 +80,6 @@ private: QString _entityEditFilter{}; QScriptEngine _entityEditFilterEngine{}; - QEventLoop _scriptRequestLoop{}; }; #endif // hifi_EntityServer_h diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 7c3eb7bec3..e75c5442b2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -927,13 +927,21 @@ void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function _entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions; auto global = _entityEditFilterEngine->globalObject(); _entityEditFilterFunction = global.property("filter"); - _hasEntityEditFilter = _entityEditFilterFunction.isFunction(); + if (!_entityEditFilterFunction.isFunction()) { + qCDebug(entities) << "Filter function specified but not found. Will reject all edits."; + _entityEditFilterEngine = nullptr; // So that we don't try to call it. See filterProperties. + } + _hasEntityEditFilter = true; } bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) { - if (!_hasEntityEditFilter || !_entityEditFilterEngine) { + if (!_entityEditFilterEngine) { propertiesOut = propertiesIn; wasChanged = false; // not changed + if (_hasEntityEditFilter) { + qCDebug(entities) << "Rejecting properties because filter has not been set."; + return false; + } return true; // allowed } auto oldProperties = propertiesIn.getDesiredProperties(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index cc179e7de0..23bc2cf8af 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -267,6 +267,7 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); void initEntityEditFilterEngine(QScriptEngine* engine, std::function entityEditFilterHadUncaughtExceptions); + void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; } static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; From ab0d5ec178172ccd9fd6d3dacc1fca743bb3f505 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 30 Jan 2017 11:59:30 -0800 Subject: [PATCH 060/142] Fix for memory-leak in Windows audio stack (audiodg.exe grows without bound) --- cmake/externals/wasapi/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index b085eefb0c..d4d4b42e10 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi6.zip - URL_MD5 fcac808c1ba0b0f5b44ea06e2612ebab + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip + URL_MD5 bc2861e50852dd590cdc773a14a041a7 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" From 725cfa804553e8642ff5a0a6b935d59de7d08c95 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 30 Jan 2017 11:52:08 -0800 Subject: [PATCH 061/142] Fix it! --- .../entity-server-filter-example.js | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index 4d4f7273f1..424e6e9668 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -4,51 +4,75 @@ function filter(p) { /* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */ if (p.name) {p.name += 'x'; print('fixme name', p. name);} + /* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */ if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);} + /* Can also reject altogether */ if (p.userData) { return false; } + /* Reject if modifications made to Model properties */ if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; } + /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ var maxVelocity = 5; + function sign(val) { + if (val > 0) { + return 1; + } else if (val < 0) { + return -1; + } else { + return 0; + } + } + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; if (p.velocity) { if (Math.abs(p.velocity.x) > maxVelocity) { - p.velocity.x = Math.sign(p.velocity.x) * maxVelocity; - p.acceleration.x = 0; + p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero); + p.acceleration.x = nearZero; } if (Math.abs(p.velocity.y) > maxVelocity) { - p.velocity.y = Math.sign(p.velocity.y) * maxVelocity; - p.acceleration.y = 0; + p.velocity.y = sign(p.velocity.y) * (maxVelocity + nearZero); + p.acceleration.y = nearZero; } if (Math.abs(p.velocity.z) > maxVelocity) { - p.velocity.z = Math.sign(p.velocity.z) * maxVelocity; - p.acceleration.z = 0; + p.velocity.z = sign(p.velocity.z) * (maxVelocity + nearZero); + p.acceleration.z = nearZero; } } + /* Define an axis-aligned zone in which entities are not allowed to enter. */ /* This example zone corresponds to an area to the right of the spawnpoint in your Sandbox. It's an area near the big rock to the right. If an entity enters the zone, it'll move behind the rock.*/ - var boxMin = {x: 25.5, y: -0.48, z: -9.9}; - var boxMax = {x: 31.1, y: 4, z: -3.79}; - var zero = {x: 0.0, y: 0.0, z: 0.0}; - if (p.position) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + /* Define the points that create the "NO ENTITIES ALLOWED" box */ + var boxMin = {x: 25.5, y: -0.48, z: -9.9}; + var boxMax = {x: 31.1, y: 4, z: -3.79}; + /* Define the point that you want entites that enter the box to appear */ + var resetPoint = {x: 29.5, y: 0.37 + nearZero, z: -2}; var x = p.position.x; var y = p.position.y; var z = p.position.z; if ((x > boxMin.x && x < boxMax.x) && (y > boxMin.y && y < boxMax.y) && (z > boxMin.z && z < boxMax.z)) { - /* Move it to the origin of the zone */ - p.position = boxMin; - p.velocity = zero; - p.acceleration = zero; + p.position = resetPoint; + if (p.velocity) { + p.velocity = {x: 0, y: nearZero, z: 0}; + } + if (p.acceleration) { + p.acceleration = {x: 0, y: nearZero, z: 0}; + } } } From 54b4612ee3cf267f6edb386b49c4280d1f4d1e26 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 23 Dec 2016 10:36:13 -0800 Subject: [PATCH 062/142] fix for missed START collision events --- libraries/physics/src/ContactInfo.cpp | 10 +++++----- libraries/physics/src/ContactInfo.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index c2ea6e8671..59948db671 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -13,15 +13,15 @@ void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) { _lastStep = currentStep; - ++_numSteps; positionWorldOnB = p.m_positionWorldOnB; normalWorldOnB = p.m_normalWorldOnB; distance = p.m_distance1; -} +} ContactEventType ContactInfo::computeType(uint32_t thisStep) { - if (_lastStep != thisStep) { - return CONTACT_EVENT_TYPE_END; + ++_numChecks; + if (_numChecks == 1) { + return CONTACT_EVENT_TYPE_START; } - return (_numSteps == 1) ? CONTACT_EVENT_TYPE_START : CONTACT_EVENT_TYPE_CONTINUE; + return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END; } diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 11c908a414..17356969d1 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -19,7 +19,7 @@ class ContactInfo { -public: +public: void update(uint32_t currentStep, const btManifoldPoint& p); ContactEventType computeType(uint32_t thisStep); @@ -30,9 +30,9 @@ public: btVector3 normalWorldOnB; btScalar distance; private: - uint32_t _lastStep = 0; - uint32_t _numSteps = 0; -}; + uint32_t _lastStep { 0 }; + uint32_t _numChecks { 0 }; +}; #endif // hifi_ContactEvent_h From ed17c4fa16a1417a493dba09ed0945cc217a9953 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 23 Dec 2016 16:55:39 -0800 Subject: [PATCH 063/142] fix an old typo about when to send collision event --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d277fd540f..9a3b66ff1f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -957,7 +957,8 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { + const EntityItemID& id, const Collision& collision) { + // BUG: this method is poorly named. It should be called like: isOwnerOfObjectOrOwnerOfOtherIfObjectIsUnowned() EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); if (!entity) { return false; @@ -965,7 +966,7 @@ bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePoint QUuid simulatorID = entity->getSimulatorID(); if (simulatorID.isNull()) { // Can be null if it has never moved since being created or coming out of persistence. - // However, for there to be a collission, one of the two objects must be moving. + // However, for there to be a collision, one of the two objects must be moving. const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA; EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID); if (!otherEntity) { @@ -1054,6 +1055,8 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons playEntityCollisionSound(myNodeID, entityTree, idB, collision); // And now the entity scripts + // BUG! scripts don't get the final COLLISION_EVENT_TYPE_END event in a timely manner because + // by the time it gets here the object has been deactivated and local ownership is relenquished. if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { emit collisionWithEntity(idA, idB, collision); if (_entitiesScriptEngine) { From b7cd8827f93e752bc512243c33b6e8f7562fcaaf Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 30 Dec 2016 14:11:24 -0800 Subject: [PATCH 064/142] collision events for owned objects only also: fewer entityID lookups for scripted collision sounds and events --- .../src/EntityTreeRenderer.cpp | 63 +++++++++---------- .../src/EntityTreeRenderer.h | 6 +- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 9a3b66ff1f..0e7fbee5c1 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -956,6 +956,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } +/* bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id, const Collision& collision) { // BUG: this method is poorly named. It should be called like: isOwnerOfObjectOrOwnerOfOtherIfObjectIsUnowned() @@ -982,43 +983,35 @@ bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePoint return true; } -void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { +bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id) { + EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); + return ((bool)entity && myNodeID == entity->getSimulatorID()); +} +*/ - if (!isCollisionOwner(myNodeID, entityTree, id, collision)) { - return; - } - - SharedSoundPointer collisionSound; - float mass = 1.0; // value doesn't get used, but set it so compiler is quiet - AACube minAACube; - bool success = false; - _tree->withReadLock([&] { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (entity) { - collisionSound = entity->getCollisionSound(); - mass = entity->computeMass(); - minAACube = entity->getMinimumAACube(success); - } - }); - if (!success) { - return; - } +void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) { + assert((bool)entity); + SharedSoundPointer collisionSound = entity->getCollisionSound(); if (!collisionSound) { return; } + bool success = false; + AACube minAACube = entity->getMinimumAACube(success); + if (!success) { + return; + } + float mass = entity->computeMass(); - const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity() + const float COLLISION_PENETRATION_TO_VELOCITY = 50.0f; // as a subsitute for RELATIVE entity->getVelocity() // The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact, // but that first contact depends on exactly where we hit in the physics step. // We can get a more consistent initial-contact energy reading by using the changed velocity. // Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish // between bounce and sliding along a surface. - const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ? - glm::length(collision.velocityChange) : - glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; - const float energy = mass * linearVelocity * linearVelocity / 2.0f; - const glm::vec3 position = collision.contactPoint; + const float speedSquared = (collision.type == CONTACT_EVENT_TYPE_START) ? + glm::length2(collision.velocityChange) : + glm::length2(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; + const float energy = mass * speedSquared / 2.0f; const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f; const float COLLISION_MINIMUM_VOLUME = 0.005f; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); @@ -1032,7 +1025,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2); - AudioInjector::playSound(collisionSound, volume, stretchFactor, position); + AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, @@ -1048,23 +1041,23 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons return; } - // See if we should play sounds EntityTreePointer entityTree = std::static_pointer_cast(_tree); const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); - playEntityCollisionSound(myNodeID, entityTree, idA, collision); - playEntityCollisionSound(myNodeID, entityTree, idB, collision); - // And now the entity scripts + // trigger scripted collision sounds and events for locally owned objects // BUG! scripts don't get the final COLLISION_EVENT_TYPE_END event in a timely manner because // by the time it gets here the object has been deactivated and local ownership is relenquished. - if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { + EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA); + if ((bool)entityA && myNodeID == entityA->getSimulatorID()) { + playEntityCollisionSound(entityA, collision); emit collisionWithEntity(idA, idB, collision); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); } } - - if (isCollisionOwner(myNodeID, entityTree, idB, collision)) { + EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB); + if ((bool)entityB && myNodeID == entityB->getSimulatorID()) { + playEntityCollisionSound(entityB, collision); emit collisionWithEntity(idB, idA, collision); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8c021ad184..a0673207f9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -170,11 +170,9 @@ private: bool _wantScripts; QSharedPointer _entitiesScriptEngine; - bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); + //static bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id); - void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); + static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision); bool _lastPointerEventValid; PointerEvent _lastPointerEvent; From 8cf7aee0092d576c4520c6f5e6c6e7c5e5606edf Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 30 Dec 2016 14:13:11 -0800 Subject: [PATCH 065/142] fix bug: second collision event with bad data --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 5 ++++- libraries/shared/src/RegisteredMetaTypes.cpp | 6 ++++++ libraries/shared/src/RegisteredMetaTypes.h | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 0e7fbee5c1..da06a07552 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1060,7 +1060,10 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons playEntityCollisionSound(entityB, collision); emit collisionWithEntity(idB, idA, collision); if (_entitiesScriptEngine) { - _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + // since we're swapping A and B we need to send the inverted collision + Collision invertedCollision(collision); + invertedCollision.invert(); + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 984529c4ba..7f12d6cc00 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -742,6 +742,12 @@ void collisionFromScriptValue(const QScriptValue &object, Collision& collision) // TODO: implement this when we know what it means to accept collision events from JS } +void Collision::invert() { + std::swap(idA, idB); + contactPoint += penetration; + penetration *= -1.0f; +} + QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) { if (uuid.isNull()) { return QScriptValue::NullValue; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 2aefd3aa47..498a8b3b3a 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -142,11 +142,13 @@ public: const glm::vec3& cPenetration, const glm::vec3& velocityChange) : type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { } + void invert(); // swap A and B + ContactEventType type; QUuid idA; QUuid idB; - glm::vec3 contactPoint; - glm::vec3 penetration; + glm::vec3 contactPoint; // on B in world-frame + glm::vec3 penetration; // from B towards A in world-frame glm::vec3 velocityChange; }; Q_DECLARE_METATYPE(Collision) From 2162a364a93faadd564df779c5e0e7efcbeecf83 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 30 Dec 2016 16:04:28 -0800 Subject: [PATCH 066/142] minor cleanup --- libraries/physics/src/PhysicsEngine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index ba002d925c..f8e02a14b8 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -402,7 +402,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { + if (type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) + @@ -421,7 +421,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { QUuid idB = motionStateB->getObjectID(); glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset; // NOTE: we're flipping the order of A and B (so that the first objectID is never NULL) - // hence we must negate the penetration. + // hence we must negate the penetration (because penetration always points from B to A). glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange)); } From 0809149a8c9ba30ada618da126d745727b293cda Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 10 Jan 2017 19:36:06 -0800 Subject: [PATCH 067/142] harvest collision events before disowning --- interface/src/Application.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 576241ddc2..77f5487986 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4377,6 +4377,10 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("harvestChanges"); if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); @@ -4384,11 +4388,10 @@ void Application::update(float deltaTime) { avatarManager->handleOutgoingChanges(outgoingChanges); }); - auto collisionEvents = _physicsEngine->getCollisionEvents(); - avatarManager->handleCollisionEvents(collisionEvents); - if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); From 2541bfb1a8fdf4a81a1ccf83e6b426e6bbbe6d0b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 10 Jan 2017 20:19:09 -0800 Subject: [PATCH 068/142] only create collision events for owned entities --- .../src/EntityTreeRenderer.cpp | 2 -- libraries/physics/src/EntityMotionState.cpp | 5 ++++ libraries/physics/src/EntityMotionState.h | 2 ++ libraries/physics/src/ObjectMotionState.h | 2 ++ libraries/physics/src/PhysicsEngine.cpp | 23 ++++++++++++++----- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index da06a07552..c56d55286e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1045,8 +1045,6 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); // trigger scripted collision sounds and events for locally owned objects - // BUG! scripts don't get the final COLLISION_EVENT_TYPE_END event in a timely manner because - // by the time it gets here the object has been deactivated and local ownership is relenquished. EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA); if ((bool)entityA && myNodeID == entityA->getSimulatorID()) { playEntityCollisionSound(entityA, collision); diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 1833d0aba4..b0bdc34b52 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -762,6 +762,11 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::shouldBeLocallyOwned() const { + return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || + _entity->getSimulatorID() == Physics::getSessionUUID(); +} + void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { _outgoingPriority = glm::max(_outgoingPriority, priority); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 194d82805f..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -78,6 +78,8 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool shouldBeLocallyOwned() const override; + friend class PhysicalEntitySimulation; protected: diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index a7894998a8..1d258560c3 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -146,6 +146,8 @@ public: void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; } void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } + virtual bool shouldBeLocallyOwned() const { return false; } + friend class PhysicsEngine; protected: diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index f8e02a14b8..5b638b9a98 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -405,25 +405,36 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { if (type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); - glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) + - (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); - if (motionStateA) { + // NOTE: the MyAvatar RigidBody is the only object in the simulation that does NOT have a MotionState + // which means should we ever want to report ALL collision events against the avatar we can + // modify the logic below. + // + // We only create events when at least one of the objects is (or should be) owned in the local simulation. + if (motionStateA && (motionStateA->shouldBeLocallyOwned())) { QUuid idA = motionStateA->getObjectID(); QUuid idB; if (motionStateB) { idB = motionStateB->getObjectID(); } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset; + glm::vec3 velocityChange = motionStateA->getObjectLinearVelocityChange() + + (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange)); - } else if (motionStateB) { + } else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) { QUuid idB = motionStateB->getObjectID(); + QUuid idA; + if (motionStateA) { + idA = motionStateA->getObjectID(); + } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset; + glm::vec3 velocityChange = motionStateB->getObjectLinearVelocityChange() + + (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)); // NOTE: we're flipping the order of A and B (so that the first objectID is never NULL) - // hence we must negate the penetration (because penetration always points from B to A). + // hence we negate the penetration (because penetration always points from B to A). glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB); - _collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange)); + _collisionEvents.push_back(Collision(type, idB, idA, position, penetration, velocityChange)); } } From b5537304a3366c08ee30bb69cacbb0b2cba57852 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 10 Jan 2017 21:29:08 -0800 Subject: [PATCH 069/142] more correct CONTINUE collision event filter --- libraries/physics/src/ContactInfo.cpp | 14 ++++++++++++-- libraries/physics/src/ContactInfo.h | 4 +++- libraries/physics/src/PhysicsEngine.cpp | 5 ++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index 59948db671..085f746a73 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -18,10 +18,20 @@ void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) { distance = p.m_distance1; } +const uint32_t STEPS_BETWEEN_CONTINUE_EVENTS = 9; + ContactEventType ContactInfo::computeType(uint32_t thisStep) { - ++_numChecks; - if (_numChecks == 1) { + if (_continueExpiry == 0) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; return CONTACT_EVENT_TYPE_START; } return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END; } + +bool ContactInfo::readyForContinue(uint32_t thisStep) { + if (thisStep > _continueExpiry) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return true; + } + return false; +} diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 17356969d1..8d05f73b61 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -26,12 +26,14 @@ public: const btVector3& getPositionWorldOnB() const { return positionWorldOnB; } btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; } + bool readyForContinue(uint32_t thisStep); + btVector3 positionWorldOnB; btVector3 normalWorldOnB; btScalar distance; private: uint32_t _lastStep { 0 }; - uint32_t _numChecks { 0 }; + uint32_t _continueExpiry { 0 }; }; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 5b638b9a98..72596cb599 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -270,7 +270,7 @@ void PhysicsEngine::stepSimulation() { } auto onSubStep = [this]() { - updateContactMap(); + this->updateContactMap(); }; int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS, @@ -393,7 +393,6 @@ void PhysicsEngine::updateContactMap() { } const CollisionEvents& PhysicsEngine::getCollisionEvents() { - const uint32_t CONTINUE_EVENT_FILTER_FREQUENCY = 10; _collisionEvents.clear(); // scan known contacts and trigger events @@ -402,7 +401,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if (type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { + if (type != CONTACT_EVENT_TYPE_CONTINUE || contact.readyForContinue(_numContactFrames)) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); From aa8e7d27dbdf654c7394faa45fa87667fd961c62 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 10 Jan 2017 21:53:26 -0800 Subject: [PATCH 070/142] move depth filtering closer to source --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 5 ----- libraries/physics/src/PhysicsEngine.cpp | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c56d55286e..1a9b327adc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1035,11 +1035,6 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons if (!_tree || _shuttingDown) { return; } - // Don't respond to small continuous contacts. - const float COLLISION_MINUMUM_PENETRATION = 0.002f; - if ((collision.type == CONTACT_EVENT_TYPE_CONTINUE) && (glm::length(collision.penetration) < COLLISION_MINUMUM_PENETRATION)) { - return; - } EntityTreePointer entityTree = std::static_pointer_cast(_tree); const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 72596cb599..f57be4eab3 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -401,7 +401,10 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if (type != CONTACT_EVENT_TYPE_CONTINUE || contact.readyForContinue(_numContactFrames)) { + const btScalar SIGNIFICANT_DEPTH = -0.002f; // penetrations have negative distance + if (type != CONTACT_EVENT_TYPE_CONTINUE || + (contact.distance < SIGNIFICANT_DEPTH && + contact.readyForContinue(_numContactFrames))) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); From 31861d31920631583494daa9e64215ad9eb74f15 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 11 Jan 2017 08:24:53 -0800 Subject: [PATCH 071/142] use inverted collision for B-A event --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1a9b327adc..a1f9d6d414 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1051,11 +1051,11 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB); if ((bool)entityB && myNodeID == entityB->getSimulatorID()) { playEntityCollisionSound(entityB, collision); - emit collisionWithEntity(idB, idA, collision); + // since we're swapping A and B we need to send the inverted collision + Collision invertedCollision(collision); + invertedCollision.invert(); + emit collisionWithEntity(idB, idA, invertedCollision); if (_entitiesScriptEngine) { - // since we're swapping A and B we need to send the inverted collision - Collision invertedCollision(collision); - invertedCollision.invert(); _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } From 6ef4420f3738da8d089c98c850b12734897826c6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 11 Jan 2017 08:26:49 -0800 Subject: [PATCH 072/142] remove commented out cruft --- .../src/EntityTreeRenderer.cpp | 33 ------------------- .../src/EntityTreeRenderer.h | 2 -- 2 files changed, 35 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index a1f9d6d414..60bb29f85f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -956,39 +956,6 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } -/* -bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - // BUG: this method is poorly named. It should be called like: isOwnerOfObjectOrOwnerOfOtherIfObjectIsUnowned() - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (!entity) { - return false; - } - QUuid simulatorID = entity->getSimulatorID(); - if (simulatorID.isNull()) { - // Can be null if it has never moved since being created or coming out of persistence. - // However, for there to be a collision, one of the two objects must be moving. - const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA; - EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID); - if (!otherEntity) { - return false; - } - simulatorID = otherEntity->getSimulatorID(); - } - - if (simulatorID.isNull() || (simulatorID != myNodeID)) { - return false; - } - - return true; -} - -bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id) { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - return ((bool)entity && myNodeID == entity->getSimulatorID()); -} -*/ - void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) { assert((bool)entity); SharedSoundPointer collisionSound = entity->getCollisionSound(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a0673207f9..29d463b915 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -170,8 +170,6 @@ private: bool _wantScripts; QSharedPointer _entitiesScriptEngine; - //static bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, const EntityItemID& id); - static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision); bool _lastPointerEventValid; From 8879727d88c9e43759be1c83a9ef7bc43ad32282 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 30 Jan 2017 18:11:42 +0000 Subject: [PATCH 073/142] ability to grab tablet in edit mode --- scripts/system/controllers/handControllerGrab.js | 7 ++----- scripts/system/edit.js | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 972d95e9e9..f4b760dbce 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -853,7 +853,7 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - if (isInEditMode() && (newState !== STATE_OFF && + if ((isInEditMode() && this.grabbedEntity !== HMD.tabletID )&& (newState !== STATE_OFF && newState !== STATE_SEARCHING && newState !== STATE_OVERLAY_STYLUS_TOUCHING)) { return; @@ -1510,7 +1510,6 @@ function MyController(hand) { }; this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - if (!this.entityIsGrabbable(entityID)) { return false; } @@ -1703,7 +1702,7 @@ function MyController(hand) { }; this.isTablet = function (entityID) { - if (entityID === HMD.tabletID) { // XXX what's a better way to know this? + if (entityID === HMD.tabletID) { return true; } return false; @@ -2264,7 +2263,6 @@ function MyController(hand) { this.clearEquipHaptics(); this.shouldScale = false; - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); if (this.entityActivated) { @@ -2394,7 +2392,6 @@ function MyController(hand) { this.nearGrabbing = function(deltaTime, timestamp) { this.grabPointSphereOff(); - if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index d49f7ad3c5..dccd097817 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -191,7 +191,6 @@ var toolBar = (function () { selectionManager.clearSelections(); entityListTool.sendUpdate(); selectionManager.setSelections([entityID]); - return entityID; } @@ -714,9 +713,6 @@ function mouseClickEvent(event) { toolBar.setActive(true); var pickRay = result.pickRay; var foundEntity = result.entityID; - if (foundEntity === HMD.tabletID) { - return; - } properties = Entities.getEntityProperties(foundEntity); if (isLocked(properties)) { if (wantDebug) { From 57a9d34cdafa15a1c02d9551758398910504d55a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 30 Jan 2017 18:33:56 +0000 Subject: [PATCH 074/142] minimize git diff --- scripts/system/controllers/handControllerGrab.js | 3 +++ scripts/system/edit.js | 1 + 2 files changed, 4 insertions(+) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f4b760dbce..2efafa504d 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1510,6 +1510,7 @@ function MyController(hand) { }; this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { + if (!this.entityIsGrabbable(entityID)) { return false; } @@ -2263,6 +2264,7 @@ function MyController(hand) { this.clearEquipHaptics(); this.shouldScale = false; + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); if (this.entityActivated) { @@ -2392,6 +2394,7 @@ function MyController(hand) { this.nearGrabbing = function(deltaTime, timestamp) { this.grabPointSphereOff(); + if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index dccd097817..8f9697b060 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -191,6 +191,7 @@ var toolBar = (function () { selectionManager.clearSelections(); entityListTool.sendUpdate(); selectionManager.setSelections([entityID]); + return entityID; } From b264d84385d50984af5eefb38e00352351982ef4 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 30 Jan 2017 18:38:02 +0000 Subject: [PATCH 075/142] minimize git diff --- scripts/system/controllers/handControllerGrab.js | 6 +++--- scripts/system/edit.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2efafa504d..86820c990a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1510,7 +1510,7 @@ function MyController(hand) { }; this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - + if (!this.entityIsGrabbable(entityID)) { return false; } @@ -2264,7 +2264,7 @@ function MyController(hand) { this.clearEquipHaptics(); this.shouldScale = false; - + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); if (this.entityActivated) { @@ -2394,7 +2394,7 @@ function MyController(hand) { this.nearGrabbing = function(deltaTime, timestamp) { this.grabPointSphereOff(); - + if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 8f9697b060..40952e9de1 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -191,7 +191,7 @@ var toolBar = (function () { selectionManager.clearSelections(); entityListTool.sendUpdate(); selectionManager.setSelections([entityID]); - + return entityID; } From f553656e36d51090dc1ec096a6a7056c01eb051d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 30 Jan 2017 13:33:42 -0800 Subject: [PATCH 076/142] Fix importing of PolyVox entities PolyVox entities reference neighboring PolyVox entities in their entity properties so that they can be stitched together. When importing, a new ID is generated for each entity. When importing PolyVox entities, the neighboring entity IDs were not updated to reflect the newly generated IDs. This commit fixes that. --- libraries/entities/src/EntityTree.cpp | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 7c3eb7bec3..29c1f1af86 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1552,6 +1552,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return args->map->value(oldID); } EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent @@ -1567,6 +1569,43 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } } + if (!properties.getXNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); + if (neighborEntity) { + properties.setXNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getXPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); + if (neighborEntity) { + properties.setXPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); + if (neighborEntity) { + properties.setYNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); + if (neighborEntity) { + properties.setYPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); + if (neighborEntity) { + properties.setZNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); + if (neighborEntity) { + properties.setZPNeighborID(getMapped(neighborEntity)); + } + } + // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); @@ -1584,7 +1623,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra args->otherTree->addEntity(newID, properties); }); } - args->map->insert(oldID, newID); return newID; }; From e58c9326a0f454c715607e34802c5191131582e5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 30 Jan 2017 13:37:00 -0800 Subject: [PATCH 077/142] Fix Entities.isChildOfParent crashing if given unknown ID If an entity was not in the local tree a null deref crash would occur. This commit makes sure the entity pointer is checked for null before it is used. --- .../entities/src/EntityScriptingInterface.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 26754335a0..9ee5e991fb 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1366,12 +1366,14 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) { _entityTree->withReadLock([&] { EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID); - parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { - if(descendant->getID() == childID) { - isChild = true; - return; - } - }); + if (parent) { + parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { + if (descendant->getID() == childID) { + isChild = true; + return; + } + }); + } }); return isChild; From 3c56bd29600eb6066f367eb631dd986149d5d04b Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 30 Jan 2017 15:03:04 -0800 Subject: [PATCH 078/142] Adding support for disabling texturing from the material --- libraries/render-utils/src/LightingModel.cpp | 16 +++++++++++++- libraries/render-utils/src/LightingModel.h | 12 +++++++++- .../render-utils/src/MeshPartPayload.cpp | 22 +++++++++---------- libraries/render-utils/src/MeshPartPayload.h | 2 +- libraries/shared/src/RenderArgs.h | 1 + .../utilities/render/deferredLighting.qml | 1 + 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 5a251fc5e9..47af83da36 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -92,6 +92,15 @@ bool LightingModel::isAlbedoEnabled() const { return (bool)_parametersBuffer.get().enableAlbedo; } +void LightingModel::setMaterialTexturing(bool enable) { + if (enable != isMaterialTexturingEnabled()) { + _parametersBuffer.edit().enableMaterialTexturing = (float)enable; + } +} +bool LightingModel::isMaterialTexturingEnabled() const { + return (bool)_parametersBuffer.get().enableMaterialTexturing; +} + void LightingModel::setAmbientLight(bool enable) { if (enable != isAmbientLightEnabled()) { _parametersBuffer.edit().enableAmbientLight = (float)enable; @@ -150,6 +159,8 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpecular(config.enableSpecular); _lightingModel->setAlbedo(config.enableAlbedo); + _lightingModel->setMaterialTexturing(config.enableMaterialTexturing); + _lightingModel->setAmbientLight(config.enableAmbientLight); _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); @@ -160,5 +171,8 @@ void MakeLightingModel::configure(const Config& config) { void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { - lightingModel = _lightingModel; + lightingModel = _lightingModel; + + // make sure the enableTexturing flag of the render ARgs is in sync + renderContext->args->_enableTexturing = _lightingModel->isMaterialTexturingEnabled(); } \ No newline at end of file diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 8f3ee9b7d6..45514654f2 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -49,6 +49,8 @@ public: void setAlbedo(bool enable); bool isAlbedoEnabled() const; + void setMaterialTexturing(bool enable); + bool isMaterialTexturingEnabled() const; void setAmbientLight(bool enable); bool isAmbientLightEnabled() const; @@ -88,9 +90,12 @@ protected: float enableSpotLight{ 1.0f }; float showLightContour{ 0.0f }; // false by default + float enableObscurance{ 1.0f }; - glm::vec2 spares{ 0.0f }; + float enableMaterialTexturing { 1.0f }; + + float spares{ 0.0f }; Parameters() {} }; @@ -117,6 +122,8 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty) + Q_PROPERTY(bool enableMaterialTexturing MEMBER enableMaterialTexturing NOTIFY dirty) + Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty) Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty) Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) @@ -136,13 +143,16 @@ public: bool enableScattering{ true }; bool enableDiffuse{ true }; bool enableSpecular{ true }; + bool enableAlbedo{ true }; + bool enableMaterialTexturing { true }; bool enableAmbientLight{ true }; bool enableDirectionalLight{ true }; bool enablePointLight{ true }; bool enableSpotLight{ true }; + bool showLightContour { false }; // false by default signals: diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index fa180a654a..3276efd529 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -129,7 +129,7 @@ void MeshPartPayload::bindMesh(gpu::Batch& batch) const { } } -void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations) const { +void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, bool enableTextures) const { if (!_drawMaterial) { return; } @@ -148,7 +148,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Albedo - if (materialKey.isAlbedoMap()) { + if (enableTextures && materialKey.isAlbedoMap()) { auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, itr->second->getTextureView()); @@ -160,7 +160,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Roughness map - if (materialKey.isRoughnessMap()) { + if (enableTextures && materialKey.isRoughnessMap()) { auto itr = textureMaps.find(model::MaterialKey::ROUGHNESS_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, itr->second->getTextureView()); @@ -174,7 +174,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Normal map - if (materialKey.isNormalMap()) { + if (enableTextures && materialKey.isNormalMap()) { auto itr = textureMaps.find(model::MaterialKey::NORMAL_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, itr->second->getTextureView()); @@ -188,7 +188,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Metallic map - if (materialKey.isMetallicMap()) { + if (enableTextures && materialKey.isMetallicMap()) { auto itr = textureMaps.find(model::MaterialKey::METALLIC_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, itr->second->getTextureView()); @@ -202,7 +202,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Occlusion map - if (materialKey.isOcclusionMap()) { + if (enableTextures && materialKey.isOcclusionMap()) { auto itr = textureMaps.find(model::MaterialKey::OCCLUSION_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, itr->second->getTextureView()); @@ -216,7 +216,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Scattering map - if (materialKey.isScatteringMap()) { + if (enableTextures && materialKey.isScatteringMap()) { auto itr = textureMaps.find(model::MaterialKey::SCATTERING_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, itr->second->getTextureView()); @@ -230,7 +230,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Emissive / Lightmap - if (materialKey.isLightmapMap()) { + if (enableTextures && materialKey.isLightmapMap()) { auto itr = textureMaps.find(model::MaterialKey::LIGHTMAP_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { @@ -238,7 +238,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, textureCache->getGrayTexture()); } - } else if (materialKey.isEmissiveMap()) { + } else if (enableTextures && materialKey.isEmissiveMap()) { auto itr = textureMaps.find(model::MaterialKey::EMISSIVE_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { @@ -271,7 +271,7 @@ void MeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); if (args) { args->_details._materialSwitches++; @@ -588,7 +588,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); args->_details._materialSwitches++; diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 7d0aeab2bd..1f3778c34a 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -51,7 +51,7 @@ public: // ModelMeshPartPayload functions to perform render void drawCall(gpu::Batch& batch) const; virtual void bindMesh(gpu::Batch& batch) const; - virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations) const; + virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const; virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const; // Payload resource cached values diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 851e065f20..b2c05b0548 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + bool _enableTexturing { true }; RenderDetails _details; }; diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 26dbc1f2bc..0ac4cbc5b5 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,6 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", + "Textures:LightingModel:enableMaterialTexturing", ] CheckBox { text: modelData.split(":")[0] From 0962a15a06dfdb68fbcb2201da4d98b486b45c02 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 30 Jan 2017 15:34:46 -0800 Subject: [PATCH 079/142] Fix for one-frame lag in the tablet stylus. --- interface/src/ui/overlays/ModelOverlay.cpp | 2 ++ .../entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- libraries/render-utils/src/Model.cpp | 7 +++---- libraries/render-utils/src/Model.h | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index f70537a952..e17ab587f6 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -22,6 +22,7 @@ ModelOverlay::ModelOverlay() _modelTextures(QVariantMap()) { _model->init(); + _model->setSpatiallyNestableOverride(this); _isLoaded = false; } @@ -33,6 +34,7 @@ ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : _updateModel(false) { _model->init(); + _model->setSpatiallyNestableOverride(this); if (_url.isValid()) { _updateModel = true; _isLoaded = false; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 087fcda8e1..be64985fdb 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -508,7 +508,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointerallocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this)); - _model->setSpatiallyNestableOverride(shared_from_this()); + _model->setSpatiallyNestableOverride(this); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if (QUrl(getModelURL()) != _model->getURL()) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 59b997b2cc..b28b2022fc 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -133,16 +133,15 @@ void Model::setRotation(const glm::quat& rotation) { updateRenderItems(); } -void Model::setSpatiallyNestableOverride(SpatiallyNestablePointer override) { +void Model::setSpatiallyNestableOverride(SpatiallyNestable* override) { _spatiallyNestableOverride = override; updateRenderItems(); } Transform Model::getTransform() const { - SpatiallyNestablePointer spatiallyNestableOverride = _spatiallyNestableOverride.lock(); - if (spatiallyNestableOverride) { + if (_spatiallyNestableOverride) { bool success; - Transform transform = spatiallyNestableOverride->getTransform(success); + Transform transform = _spatiallyNestableOverride->getTransform(success); if (success) { transform.setScale(getScale()); return transform; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 8b6992394f..dfb6822eb5 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -205,7 +205,7 @@ public: void setTranslation(const glm::vec3& translation); void setRotation(const glm::quat& rotation); - void setSpatiallyNestableOverride(SpatiallyNestablePointer ptr); + void setSpatiallyNestableOverride(SpatiallyNestable* ptr); const glm::vec3& getTranslation() const { return _translation; } const glm::quat& getRotation() const { return _rotation; } @@ -297,7 +297,7 @@ protected: glm::quat _rotation; glm::vec3 _scale; - SpatiallyNestableWeakPointer _spatiallyNestableOverride; + SpatiallyNestable* _spatiallyNestableOverride { nullptr }; glm::vec3 _offset; From 740a0add8ad062b8833d38862798fe82a8b5bafb Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 30 Jan 2017 15:47:04 -0800 Subject: [PATCH 080/142] Updates --- .../entity-server-filter-example.js | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index 424e6e9668..a82670c53d 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -1,6 +1,17 @@ function filter(p) { - /* block comments are ok, but not double-slash end-of-line-comments */ - + /******************************************************/ + /* General Filter Comments + /* + - Custom filters must be named "filter" and must be global + - Block comments are ok, but not double-slash end-of-line-comments + - Certain JavaScript functions are not available, like Math.sign(), as they are undefined in QT's non-conforming JS + - HiFi's scripting interface is unavailable here. That means you can't call, for example, Users.*() + */ + /******************************************************/ + + /******************************************************/ + /* Simple Filter Examples + /******************************************************/ /* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */ if (p.name) {p.name += 'x'; print('fixme name', p. name);} @@ -9,7 +20,7 @@ function filter(p) { if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);} - /* Can also reject altogether */ + /* Can also reject new properties altogether by returning false */ if (p.userData) { return false; } @@ -17,21 +28,35 @@ function filter(p) { if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; } + /******************************************************/ + /* Physical Property Filter Examples + /* + NOTES about filtering physical properties: + - For now, ensure you always supply a new value for the filtered physical property + (instead of simply removing the property) + - Ensure you always specify a slightly different value for physical properties every + time your filter returns. Look to "var nearZero" below for an example). + This is necessary because Interface checks if a physical property has changed + when deciding whether to apply or reject the server's physical properties. + If a physical property's value doesn't change, Interface will reject the server's property value, + and Bullet will continue simulating the entity with stale physical properties. + */ + /******************************************************/ /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ - var maxVelocity = 5; - function sign(val) { - if (val > 0) { - return 1; - } else if (val < 0) { - return -1; - } else { - return 0; - } - } - /* Random near-zero value used as "zero" to prevent two sequential updates from being - exactly the same (which would cause them to be ignored) */ - var nearZero = 0.0001 * Math.random() + 0.001; if (p.velocity) { + var maxVelocity = 5; + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + function sign(val) { + if (val > 0) { + return 1; + } else if (val < 0) { + return -1; + } else { + return 0; + } + } if (Math.abs(p.velocity.x) > maxVelocity) { p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero); p.acceleration.x = nearZero; From 249ec80d74ab57c48f1bb063e95b1118f2fd5065 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 30 Jan 2017 15:48:48 -0800 Subject: [PATCH 081/142] Writting a better version of the no texture case --- .../render-utils/src/MeshPartPayload.cpp | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 3276efd529..5e47ed8b0f 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -147,8 +147,19 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat numUnlit++; } + if (!enableTextures) { + batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, nullptr); + return; + } + // Albedo - if (enableTextures && materialKey.isAlbedoMap()) { + if (materialKey.isAlbedoMap()) { auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, itr->second->getTextureView()); @@ -160,7 +171,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Roughness map - if (enableTextures && materialKey.isRoughnessMap()) { + if (materialKey.isRoughnessMap()) { auto itr = textureMaps.find(model::MaterialKey::ROUGHNESS_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, itr->second->getTextureView()); @@ -174,7 +185,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Normal map - if (enableTextures && materialKey.isNormalMap()) { + if (materialKey.isNormalMap()) { auto itr = textureMaps.find(model::MaterialKey::NORMAL_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, itr->second->getTextureView()); @@ -188,7 +199,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Metallic map - if (enableTextures && materialKey.isMetallicMap()) { + if (materialKey.isMetallicMap()) { auto itr = textureMaps.find(model::MaterialKey::METALLIC_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, itr->second->getTextureView()); @@ -202,7 +213,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Occlusion map - if (enableTextures && materialKey.isOcclusionMap()) { + if (materialKey.isOcclusionMap()) { auto itr = textureMaps.find(model::MaterialKey::OCCLUSION_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, itr->second->getTextureView()); @@ -216,7 +227,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Scattering map - if (enableTextures && materialKey.isScatteringMap()) { + if (materialKey.isScatteringMap()) { auto itr = textureMaps.find(model::MaterialKey::SCATTERING_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, itr->second->getTextureView()); @@ -230,7 +241,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } // Emissive / Lightmap - if (enableTextures && materialKey.isLightmapMap()) { + if (materialKey.isLightmapMap()) { auto itr = textureMaps.find(model::MaterialKey::LIGHTMAP_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { @@ -238,7 +249,7 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, textureCache->getGrayTexture()); } - } else if (enableTextures && materialKey.isEmissiveMap()) { + } else if (materialKey.isEmissiveMap()) { auto itr = textureMaps.find(model::MaterialKey::EMISSIVE_MAP); if (itr != textureMaps.end() && itr->second->isDefined()) { From c65e8841518db91fe98c57880777246f450216df Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 30 Jan 2017 16:04:54 -0800 Subject: [PATCH 082/142] Don't set joint "set" flags for animation values --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 945726255c..b947c5283d 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -320,12 +320,9 @@ bool RenderableModelEntityItem::getAnimationFrame() { glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * rotationMat * fbxJoints[index].postTransform); _localJointTranslations[j] = extractTranslation(finalMat); - _localJointTranslationsSet[j] = true; _localJointTranslationsDirty[j] = true; _localJointRotations[j] = glmExtractRotation(finalMat); - - _localJointRotationsSet[j] = true; _localJointRotationsDirty[j] = true; } } From 18fd965c3369ae313a82cc75ded0591928a8b4d5 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 30 Jan 2017 16:07:43 -0800 Subject: [PATCH 083/142] Add note about floats? --- script-archive/entity-server-filter-example.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index a82670c53d..ad44bf1583 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -40,6 +40,9 @@ function filter(p) { when deciding whether to apply or reject the server's physical properties. If a physical property's value doesn't change, Interface will reject the server's property value, and Bullet will continue simulating the entity with stale physical properties. + Ensure that this value is not changed by such a small amount such that new values + fall within floating point precision boundaries. If you accidentally do this, prepare for many + hours of frustrating debugging :). */ /******************************************************/ /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ From eaf033107c7c7022840d7f4f86ca835b257dfd75 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 30 Jan 2017 17:13:13 -0700 Subject: [PATCH 084/142] Require password verification in domain-server settings If you modify the security settings to use a username/password for access to the domain server settings, we now have a second password field which must match the first one you entered. --- domain-server/resources/describe-settings.json | 7 +++++++ .../resources/web/settings/js/settings.js | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 58b1df08c1..20d2711743 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -372,6 +372,13 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, + { + "name": "verify_http_password", + "label": "Verify HTTP Password", + "type": "password", + "help": "Must match the password entered above for change to be saved.", + "value-hidden": true + }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c31d6e2dfc..659372267c 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -904,10 +904,18 @@ function saveSettings() { var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); // check if we've set the basic http password - if so convert it to base64 + var canPost = true; if (formJSON["security"]) { var password = formJSON["security"]["http_password"]; + var verify_password = formJSON["security"]["verify_http_password"]; if (password && password.length > 0) { - formJSON["security"]["http_password"] = sha256_digest(password); + if (password != verify_password) { + bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); + canPost = false; + } else { + formJSON["security"]["http_password"] = sha256_digest(password); + delete formJSON["security"]["verify_http_password"]; + } } } @@ -923,7 +931,9 @@ function saveSettings() { $(this).blur(); // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); + if (canPost) { + postSettings(formJSON); + } } $('body').on('click', '.save-button', function(e){ From fe8fe816f4aa0b83e76d4f967e58fcb16798e83b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 30 Jan 2017 16:14:40 -0800 Subject: [PATCH 085/142] accept forced physics results from entity server (e.g., when filtered) --- libraries/entities/src/EntityItem.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64bc9fbd5a..cd5558823f 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -651,6 +651,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. + bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { QByteArray simOwnerData; @@ -663,6 +664,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (wantTerseEditLogging() && _simulationOwner != newSimOwner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner; } + filterRejection = newSimOwner.getID().isNull(); if (weOwnSimulation) { if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) { // entity-server is trying to clear our ownership (probably at our own request) @@ -708,10 +710,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Note: duplicate packets are expected and not wrong. They may be sent for any number of // reasons and the contract is that the client handles them in an idempotent manner. auto lastEdited = lastEditedFromBufferAdjusted; - auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp; bool valueChanged = value != _lastUpdatedPositionValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); if (shouldUpdate) { updatePositionFromNetwork(value); _lastUpdatedPositionTimestamp = lastEdited; @@ -719,10 +721,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } }; - auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::quat value){ + auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::quat value){ bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp; bool valueChanged = value != _lastUpdatedRotationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); if (shouldUpdate) { updateRotationFromNetwork(value); _lastUpdatedRotationTimestamp = lastEdited; @@ -730,10 +732,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } }; - auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp; bool valueChanged = value != _lastUpdatedVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); if (shouldUpdate) { updateVelocityFromNetwork(value); _lastUpdatedVelocityTimestamp = lastEdited; @@ -741,10 +743,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } }; - auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp; bool valueChanged = value != _lastUpdatedAngularVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); if (shouldUpdate) { updateAngularVelocityFromNetwork(value); _lastUpdatedAngularVelocityTimestamp = lastEdited; @@ -752,10 +754,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } }; - auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ + auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp; bool valueChanged = value != _lastUpdatedAccelerationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; + bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); if (shouldUpdate) { setAcceleration(value); _lastUpdatedAccelerationTimestamp = lastEdited; From c3f9663ab0addb14ed9e3c6d64fe249d7f255b9f Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 31 Jan 2017 01:25:59 +0100 Subject: [PATCH 086/142] - Fix for simulation owned entities moving to 0,0,0 after cache reload - Fix for simulation priority, use SCRIPT_GRAB_SIMULATION_PRIORITY in EntityItem::grabSimulationOwnership() --- libraries/entities/src/EntityItem.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64bc9fbd5a..52dad5e976 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -688,6 +688,14 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef somethingChanged = true; _simulationOwner.clearCurrentOwner(); } + } else if (newSimOwner.matchesValidID(myNodeID) && !(_dirtyFlags & Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_POKE) + && !(_dirtyFlags & Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB)) { + // entity-server tells us that we have simulation ownership while we never requested this for this EntityItem, + // this could happen when the user reloads the cache and entity tree. + _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + somethingChanged = true; + _simulationOwner.clearCurrentOwner(); + weOwnSimulation = false; } else if (_simulationOwner.set(newSimOwner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; somethingChanged = true; @@ -1278,7 +1286,7 @@ void EntityItem::grabSimulationOwnership() { auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it - _simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY); + _simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY); } else { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); From 6e7260207f1de58cff3b17729c79c0c70c576bdc Mon Sep 17 00:00:00 2001 From: Alan Z Date: Mon, 30 Jan 2017 16:51:22 -0800 Subject: [PATCH 087/142] Fix label typo in help graphic fixes #2748 in Fogbugz --- .../html/img/controls-help-gamepad.png | Bin 108160 -> 127220 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/interface/resources/html/img/controls-help-gamepad.png b/interface/resources/html/img/controls-help-gamepad.png index cb77dbdabd07adb02cf5a4e0d992d84366c64d00..c9d2aa14ec220b33e93e5c36f70b33252eaae76a 100644 GIT binary patch literal 127220 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 Date: Mon, 30 Jan 2017 17:01:45 -0800 Subject: [PATCH 088/142] don't recurse on octree elements that have not changed --- libraries/octree/src/Octree.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index d2d7aba517..58910c66bd 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1084,17 +1084,17 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW; return bytesAtThisLevel; } + } - // If we're not in delta sending mode, and we weren't asked to do a force send, and the voxel hasn't changed, - // then we can also bail early and save bits - if (!params.forceSendScene && !params.deltaView && - !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { - if (params.stats) { - params.stats->skippedNoChange(element); - } - params.stopReason = EncodeBitstreamParams::NO_CHANGE; - return bytesAtThisLevel; + // If we're not in delta sending mode, and we weren't asked to do a force send, and the octree element hasn't changed, + // then we can also bail early and save bits + if (!params.forceSendScene && !params.deltaView && + !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { + if (params.stats) { + params.stats->skippedNoChange(element); } + params.stopReason = EncodeBitstreamParams::NO_CHANGE; + return bytesAtThisLevel; } bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more! From 2ce357db3a1ea99eb93b28c702cba9df2d850ac2 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 30 Jan 2017 17:11:48 -0800 Subject: [PATCH 089/142] Fix bug that causes GCC not to detect AVX2 --- libraries/shared/src/CPUDetect.h | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libraries/shared/src/CPUDetect.h b/libraries/shared/src/CPUDetect.h index c9d2eb649b..ea6d23d8d6 100644 --- a/libraries/shared/src/CPUDetect.h +++ b/libraries/shared/src/CPUDetect.h @@ -134,7 +134,7 @@ static inline bool cpuSupportsAVX() { result = true; } } - return result; + return result; } static inline bool cpuSupportsAVX2() { @@ -143,11 +143,18 @@ static inline bool cpuSupportsAVX2() { bool result = false; if (cpuSupportsAVX()) { - if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & MASK_AVX2) == MASK_AVX2)) { - result = true; + // Work around a bug where __get_cpuid(0x7) returns wrong values on older GCC + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77756 + if (__get_cpuid(0x0, &eax, &ebx, &ecx, &edx) && (eax >= 0x7)) { + + __cpuid_count(0x7, 0x0, eax, ebx, ecx, edx); + + if ((ebx & MASK_AVX2) == MASK_AVX2) { + result = true; + } } } - return result; + return result; } #else From 7a4a76901571a764ebaeea1de036d88ed94dcc60 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 31 Jan 2017 16:53:57 +0000 Subject: [PATCH 090/142] fix more editing bugs --- scripts/system/edit.js | 3 +++ scripts/system/libraries/entitySelectionTool.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 40952e9de1..d49f7ad3c5 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -714,6 +714,9 @@ function mouseClickEvent(event) { toolBar.setActive(true); var pickRay = result.pickRay; var foundEntity = result.entityID; + if (foundEntity === HMD.tabletID) { + return; + } properties = Entities.getEntityProperties(foundEntity); if (isLocked(properties)) { if (wantDebug) { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 2932417d25..b9bae72d14 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1038,7 +1038,7 @@ SelectionDisplay = (function() { if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { - if (HMD.tabletID == entityIntersection.entityID) { + if (HMD.tabletID === entityIntersection.entityID) { return; } From 0f75668923c0632929c630cc1955764f75819650 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 31 Jan 2017 09:36:21 -0800 Subject: [PATCH 091/142] Moved Model setting of _spatiallyNestableOverride to constructor --- interface/src/ui/overlays/ModelOverlay.cpp | 6 ++---- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 4 ++-- libraries/entities-renderer/src/EntityTreeRenderer.h | 2 +- .../entities-renderer/src/RenderableModelEntityItem.cpp | 3 +-- libraries/render-utils/src/Model.cpp | 8 ++------ libraries/render-utils/src/Model.h | 8 +++----- 6 files changed, 11 insertions(+), 20 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index e17ab587f6..a0f7c4e824 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -18,23 +18,21 @@ QString const ModelOverlay::TYPE = "model"; ModelOverlay::ModelOverlay() - : _model(std::make_shared(std::make_shared())), + : _model(std::make_shared(std::make_shared(), nullptr, this)), _modelTextures(QVariantMap()) { _model->init(); - _model->setSpatiallyNestableOverride(this); _isLoaded = false; } ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : Volume3DOverlay(modelOverlay), - _model(std::make_shared(std::make_shared())), + _model(std::make_shared(std::make_shared(), nullptr, this)), _modelTextures(QVariantMap()), _url(modelOverlay->_url), _updateModel(false) { _model->init(); - _model->setSpatiallyNestableOverride(this); if (_url.isValid()) { _updateModel = true; _isLoaded = false; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d277fd540f..88b952de95 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -540,7 +540,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha std::static_pointer_cast(_tree)->processEraseMessage(message, sourceNode); } -ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority) { +ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority, SpatiallyNestable* spatiallyNestableOverride) { ModelPointer model = nullptr; // Only create and delete models on the thread that owns the EntityTreeRenderer @@ -552,7 +552,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading return model; } - model = std::make_shared(std::make_shared()); + model = std::make_shared(std::make_shared(), nullptr, spatiallyNestableOverride); model->setLoadingPriority(loadingPriority); model->init(); model->setURL(QUrl(url)); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8c021ad184..395025543d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -77,7 +77,7 @@ public: void reloadEntityScripts(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f); + Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f, SpatiallyNestable* spatiallyNestableOverride = nullptr); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index be64985fdb..751f44d816 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -507,8 +507,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointerallocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this)); - _model->setSpatiallyNestableOverride(this); + _model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this), this); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if (QUrl(getModelURL()) != _model->getURL()) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b28b2022fc..41ac39dfa8 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -78,11 +78,12 @@ void initCollisionMaterials() { } } -Model::Model(RigPointer rig, QObject* parent) : +Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : QObject(parent), _renderGeometry(), _collisionGeometry(), _renderWatcher(_renderGeometry), + _spatiallyNestableOverride(spatiallyNestableOverride), _translation(0.0f), _rotation(), _scale(1.0f, 1.0f, 1.0f), @@ -133,11 +134,6 @@ void Model::setRotation(const glm::quat& rotation) { updateRenderItems(); } -void Model::setSpatiallyNestableOverride(SpatiallyNestable* override) { - _spatiallyNestableOverride = override; - updateRenderItems(); -} - Transform Model::getTransform() const { if (_spatiallyNestableOverride) { bool success; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index dfb6822eb5..301a4592de 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -67,7 +67,7 @@ public: static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; } - Model(RigPointer rig, QObject* parent = nullptr); + Model(RigPointer rig, QObject* parent = nullptr, SpatiallyNestable* spatiallyNestableOverride = nullptr); virtual ~Model(); inline ModelPointer getThisPointer() const { @@ -205,7 +205,6 @@ public: void setTranslation(const glm::vec3& translation); void setRotation(const glm::quat& rotation); - void setSpatiallyNestableOverride(SpatiallyNestable* ptr); const glm::vec3& getTranslation() const { return _translation; } const glm::quat& getRotation() const { return _rotation; } @@ -293,12 +292,11 @@ protected: GeometryResourceWatcher _renderWatcher; + SpatiallyNestable* _spatiallyNestableOverride; + glm::vec3 _translation; glm::quat _rotation; glm::vec3 _scale; - - SpatiallyNestable* _spatiallyNestableOverride { nullptr }; - glm::vec3 _offset; static float FAKE_DIMENSION_PLACEHOLDER; From 015aafe0fbe8de86f5d72dd7e45b2c02e16d4ade Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 30 Jan 2017 13:57:13 -0800 Subject: [PATCH 092/142] make table additions in DS settings clearer --- domain-server/resources/web/css/style.css | 4 + .../resources/web/settings/js/settings.js | 249 +++++++++++------- 2 files changed, 162 insertions(+), 91 deletions(-) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index ad426671a4..553f408e15 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -125,6 +125,10 @@ tr.new-row { background-color: #dff0d8; } +tr.invalid-input { + background-color: #f2dede; +} + .graphable-stat { text-align: center; color: #5286BC; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 659372267c..fbc2aefceb 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -38,14 +38,15 @@ var Settings = { DOMAIN_ID_SELECTOR: '[name="metaverse.id"]', ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]', PLACES_TABLE_ID: 'places-table', - FORM_ID: 'settings-form' + FORM_ID: 'settings-form', + INVALID_ROW_CLASS: 'invalid-input' }; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { form_group = "
    "; setting_value = _(values).valueForKeyPath(keypath); @@ -891,23 +892,105 @@ function reloadSettings(callback) { }); } +function validateInputs() { + // check if any new values are bad + var tables = $('table'); + + var inputsValid = true; + + var tables = $('table'); + + // clear any current invalid rows + $('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS); + + function markParentRowInvalid(rowChild) { + $(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS); + } + + _.each(tables, function(table) { + var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' input[data-changed="true"]'); + + var empty = false; + + _.each(inputs, function(input){ + var inputVal = $(input).val(); + + if (inputVal.length === 0) { + empty = true + markParentRowInvalid(input); + return; + } + }); + + if (empty) { + showErrorMessage("Error", "Empty field(s)"); + inputsValid = false; + return + } + + // validate keys specificially for spaces and equality to an existing key + var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key'); + + var keyWithSpaces = false; + var duplicateKey = false; + + _.each(newKeys, function(keyCell) { + var keyVal = $(keyCell).children('input').val(); + + if (keyVal.indexOf(' ') !== -1) { + keyWithSpaces = true; + markParentRowInvalid(keyCell); + return; + } + + // make sure we don't have duplicate keys in the table + var otherKeys = $(table).find('td.key').not(keyCell); + _.each(otherKeys, function(otherKeyCell) { + var keyInput = $(otherKeyCell).children('input'); + + if (keyInput.length) { + if ($(keyInput).val() == keyVal) { + duplicateKey = true; + } + } else if ($(otherKeyCell).html() == keyVal) { + duplicateKey = true; + } + + if (duplicateKey) { + markParentRowInvalid(keyCell); + return; + } + }); + + }); + + if (keyWithSpaces) { + showErrorMessage("Error", "Key contains spaces"); + inputsValid = false; + return + } + + if (duplicateKey) { + showErrorMessage("Error", "Two keys cannot be identical"); + inputsValid = false; + return; + } + }); + + return inputsValid; +} var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; function saveSettings() { - // disable any inputs not changed - $("input:not([data-changed])").each(function(){ - $(this).prop('disabled', true); - }); - // grab a JSON representation of the form via form2js - var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); - - // check if we've set the basic http password - if so convert it to base64 + // verify that the password and confirmation match before saving var canPost = true; + if (formJSON["security"]) { var password = formJSON["security"]["http_password"]; var verify_password = formJSON["security"]["verify_http_password"]; + if (password && password.length > 0) { if (password != verify_password) { bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); @@ -919,23 +1002,46 @@ function saveSettings() { } } - console.log("----- SAVING ------"); - console.log(formJSON); + if (canPost && validateInputs()) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - // re-enable all inputs - $("input").each(function(){ - $(this).prop('disabled', false); - }); + // disable any inputs not changed + $("input:not([data-changed])").each(function(){ + $(this).prop('disabled', true); + }); - // remove focus from the button - $(this).blur(); + // grab a JSON representation of the form via form2js + var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - if (canPost) { + // check if we've set the basic http password - if so convert it to base64 + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + if (password && password.length > 0) { + formJSON["security"]["http_password"] = sha256_digest(password); + } + } + + console.log("----- SAVING ------"); + console.log(formJSON); + + // re-enable all inputs + $("input").each(function(){ + $(this).prop('disabled', false); + }); + + // remove focus from the button + $(this).blur(); + + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved postSettings(formJSON); } } +// disable any inputs not changed +$("input:not([data-changed])").each(function(){ + $(this).prop('disabled', true); +}); + $('body').on('click', '.save-button', function(e){ saveSettings(); return false; @@ -1110,8 +1216,9 @@ function makeTable(setting, keypath, setting_value) { if (setting.can_add_new_categories) { html += makeTableCategoryInput(setting, numVisibleColumns); } + if (setting.can_add_new_rows || setting.can_add_new_categories) { - html += makeTableInputs(setting, {}, ""); + html += makeTableHiddenInputs(setting, {}, ""); } } html += "" @@ -1137,7 +1244,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, return html; } -function makeTableInputs(setting, initialValues, categoryValue) { +function makeTableHiddenInputs(setting, initialValues, categoryValue) { var html = ""; @@ -1148,7 +1255,7 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (setting.key) { html += "\ - \ + \ " } @@ -1157,14 +1264,14 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (col.type === "checkbox") { html += "" + - "" + ""; } else { html += "" + - "" + ""; @@ -1244,49 +1351,17 @@ function addTableRow(row) { var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); + var input_clone = row.clone(); + if (!isArray) { - // Check key spaces - var key = row.children(".key").children("input").val() - if (key.indexOf(' ') !== -1) { - showErrorMessage("Error", "Key contains spaces") - return - } - // Check keys with the same name - var equals = false; - _.each(columns.children(".key"), function(element) { - if ($(element).text() === key) { - equals = true - return - } - }) - if (equals) { - showErrorMessage("Error", "Two keys cannot be identical") - return - } + // show the key input + var keyInput = row.children(".key").children("input"); } - // Check empty fields - var empty = false; - _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { - if ($(element).val().length === 0) { - empty = true - return - } - }) - - if (empty) { - showErrorMessage("Error", "Empty field(s)") - return - } - - var input_clone = row.clone() - // Change input row to data row - var table = row.parents("table") - var setting_name = table.attr("name") - var full_name = setting_name + "." + key - row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) - row.removeClass("inputs") + var table = row.parents("table"); + var setting_name = table.attr("name"); + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS); _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { @@ -1308,34 +1383,17 @@ function addTableRow(row) { anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { var input = $(element).children("input") - $(element).html(input.val()) - input.remove() + input.show(); } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { - // Hide inputs - var input = $(element).find("input") - var isCheckbox = false; - var isTime = false; - if (input.hasClass("table-checkbox")) { - input = $(input).parent(); - isCheckbox = true; - } else if (input.hasClass("table-time")) { - input = $(input).parent(); - isTime = true; - } + // show inputs + var input = $(element).find("input"); + input.show(); - var val = input.val(); - if (isCheckbox) { - // don't hide the checkbox - val = $(input).find("input").is(':checked'); - } else if (isTime) { - // don't hide the time - } else { - input.attr("type", "hidden") - } + var isCheckbox = input.hasClass("table-checkbox"); if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length - var key = $(element).attr('name') + var key = $(element).attr('name'); // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is @@ -1347,17 +1405,21 @@ function addTableRow(row) { input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } } else { - input.attr("name", full_name + "." + $(element).attr("name")) + // because the name of the setting in question requires the key + // setup a hook to change the HTML name of the element whenever the key changes + var colName = $(element).attr("name"); + keyInput.on('change', function(){ + input.attr("name", setting_name + "." + $(this).val() + "." + colName); + }); } if (isCheckbox) { $(input).find("input").attr("data-changed", "true"); } else { input.attr("data-changed", "true"); - $(element).append(val); } } else { - console.log("Unknown table element") + console.log("Unknown table element"); } }); @@ -1387,7 +1449,12 @@ function deleteTableRow($row) { $row.empty(); if (!isArray) { - $row.html(""); + if ($row.attr('name')) { + $row.html(""); + } else { + // for rows that didn't have a key, simply remove the row + $row.remove(); + } } else { if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { // This is the last row of the category, so delete the header From 1474f22fd72e31966088d166917a1c83e170eb60 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 31 Jan 2017 10:54:46 -0800 Subject: [PATCH 093/142] don't validate category inputs for empty --- .../resources/web/settings/js/settings.js | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index fbc2aefceb..d483e8171f 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -908,7 +908,7 @@ function validateInputs() { } _.each(tables, function(table) { - var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' input[data-changed="true"]'); + var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ':not([data-category]) input[data-changed="true"]'); var empty = false; @@ -917,6 +917,7 @@ function validateInputs() { if (inputVal.length === 0) { empty = true + markParentRowInvalid(input); return; } @@ -984,25 +985,7 @@ var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please function saveSettings() { - // verify that the password and confirmation match before saving - var canPost = true; - - if (formJSON["security"]) { - var password = formJSON["security"]["http_password"]; - var verify_password = formJSON["security"]["verify_http_password"]; - - if (password && password.length > 0) { - if (password != verify_password) { - bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); - canPost = false; - } else { - formJSON["security"]["http_password"] = sha256_digest(password); - delete formJSON["security"]["verify_http_password"]; - } - } - } - - if (canPost && validateInputs()) { + if (validateInputs()) { // POST the form JSON to the domain-server settings.json endpoint so the settings are saved // disable any inputs not changed @@ -1021,6 +1004,24 @@ function saveSettings() { } } + // verify that the password and confirmation match before saving + var canPost = true; + + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + var verify_password = formJSON["security"]["verify_http_password"]; + + if (password && password.length > 0) { + if (password != verify_password) { + bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); + canPost = false; + } else { + formJSON["security"]["http_password"] = sha256_digest(password); + delete formJSON["security"]["verify_http_password"]; + } + } + } + console.log("----- SAVING ------"); console.log(formJSON); @@ -1032,16 +1033,13 @@ function saveSettings() { // remove focus from the button $(this).blur(); - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); + if (canPost) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + postSettings(formJSON); + } } } -// disable any inputs not changed -$("input:not([data-changed])").each(function(){ - $(this).prop('disabled', true); -}); - $('body').on('click', '.save-button', function(e){ saveSettings(); return false; From 79cb0ba074787907827664242440ea87657a7ba3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 31 Jan 2017 11:05:57 -0800 Subject: [PATCH 094/142] fix keyboard behaviour for category tables --- domain-server/resources/web/settings/js/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index d483e8171f..22ce5b3170 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -216,8 +216,8 @@ $(document).ready(function(){ sibling = sibling.next(); } - if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click(); + // for tables with categories we add the entry and setup the new row on enter + if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) { sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); // set focus to the first input in the new row From 360899887775ae01049d57ab552f4cc83e6f479a Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 31 Jan 2017 21:01:03 +0100 Subject: [PATCH 095/142] use dedicated bool rather than unreliable dirtyFlags to determine if the entityItem had ever bid for simulation ownership --- libraries/entities/src/EntityItem.cpp | 15 +++++++++++++-- libraries/entities/src/EntityItem.h | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index d77fac131d..61f082c9b6 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -688,8 +688,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef somethingChanged = true; _simulationOwner.clearCurrentOwner(); } - } else if (newSimOwner.matchesValidID(myNodeID) && !(_dirtyFlags & Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_POKE) - && !(_dirtyFlags & Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB)) { + } else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) { // entity-server tells us that we have simulation ownership while we never requested this for this EntityItem, // this could happen when the user reloads the cache and entity tree. _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; @@ -1279,6 +1278,7 @@ void EntityItem::pokeSimulationOwnership() { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_POKE_SIMULATION_PRIORITY, usecTimestampNow()); } + checkForFirstSimulationBid(_simulationOwner); } void EntityItem::grabSimulationOwnership() { @@ -1291,6 +1291,7 @@ void EntityItem::grabSimulationOwnership() { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); } + checkForFirstSimulationBid(_simulationOwner); } bool EntityItem::setProperties(const EntityItemProperties& properties) { @@ -1861,6 +1862,7 @@ void EntityItem::setSimulationOwner(const QUuid& id, quint8 priority) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << id << priority; } _simulationOwner.set(id, priority); + checkForFirstSimulationBid(_simulationOwner); } void EntityItem::setSimulationOwner(const SimulationOwner& owner) { @@ -1869,6 +1871,7 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } _simulationOwner.set(owner); + checkForFirstSimulationBid(_simulationOwner); } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { @@ -1879,6 +1882,7 @@ void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { if (_simulationOwner.set(owner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } + checkForFirstSimulationBid(_simulationOwner); } void EntityItem::clearSimulationOwnership() { @@ -1895,6 +1899,7 @@ void EntityItem::clearSimulationOwnership() { void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& timestamp) { _simulationOwner.setPendingPriority(priority, timestamp); + checkForFirstSimulationBid(_simulationOwner); } QString EntityItem::actionsToDebugString() { @@ -2152,6 +2157,12 @@ void EntityItem::setActionDataInternal(QByteArray actionData) { checkWaitingToRemove(); } +void EntityItem::checkForFirstSimulationBid(const SimulationOwner& simulationOwner) const { + if (!_hasBidOnSimulation && simulationOwner.matchesValidID(DependencyManager::get()->getSessionUUID())) { + _hasBidOnSimulation = true; + } +} + void EntityItem::serializeActions(bool& success, QByteArray& result) const { if (_objectActions.size() == 0) { success = true; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index b203de203b..98a2a1e268 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -478,6 +478,8 @@ protected: const QByteArray getActionDataInternal() const; void setActionDataInternal(QByteArray actionData); + void checkForFirstSimulationBid(const SimulationOwner& simulationOwner) const; + virtual void locationChanged(bool tellPhysics = true) override; virtual void dimensionsChanged() override; @@ -586,6 +588,9 @@ protected: static quint64 _rememberDeletedActionTime; mutable QHash _previouslyDeletedActions; + // per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership + mutable bool _hasBidOnSimulation = false; + QUuid _sourceUUID; /// the server node UUID we came from bool _clientOnly { false }; @@ -594,7 +599,7 @@ protected: // physics related changes from the network to suppress any duplicates and make // sure redundant applications are idempotent glm::vec3 _lastUpdatedPositionValue; - glm::quat _lastUpdatedRotationValue; + glm::quat _lastUpdatedRotationValue; glm::vec3 _lastUpdatedVelocityValue; glm::vec3 _lastUpdatedAngularVelocityValue; glm::vec3 _lastUpdatedAccelerationValue; From 8d8b338c66b0634befe5a03c3d8c5e80474fcafb Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 31 Jan 2017 12:07:48 -0800 Subject: [PATCH 096/142] dry up custom setters, per code review --- libraries/entities/src/EntityItem.cpp | 40 ++++++++++----------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index f5d883eb92..a73420c587 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -710,55 +710,45 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Note: duplicate packets are expected and not wrong. They may be sent for any number of // reasons and the contract is that the client handles them in an idempotent manner. auto lastEdited = lastEditedFromBufferAdjusted; - auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp; - bool valueChanged = value != _lastUpdatedPositionValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); - if (shouldUpdate) { + bool otherOverwrites = overwriteLocalData && !weOwnSimulation; + auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { + bool simulationChanged = lastEdited > updatedTimestamp; + return otherOverwrites && simulationChanged && (valueChanged || filterRejection); + }; + auto customUpdatePositionFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedPositionTimestamp, value != _lastUpdatedPositionValue)) { updatePositionFromNetwork(value); _lastUpdatedPositionTimestamp = lastEdited; _lastUpdatedPositionValue = value; } }; - auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::quat value){ - bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp; - bool valueChanged = value != _lastUpdatedRotationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); - if (shouldUpdate) { + auto customUpdateRotationFromNetwork = [this, shouldUpdate, lastEdited](glm::quat value){ + if (shouldUpdate(_lastUpdatedRotationTimestamp, value != _lastUpdatedRotationValue)) { updateRotationFromNetwork(value); _lastUpdatedRotationTimestamp = lastEdited; _lastUpdatedRotationValue = value; } }; - auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp; - bool valueChanged = value != _lastUpdatedVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); - if (shouldUpdate) { + auto customUpdateVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedVelocityTimestamp, value != _lastUpdatedVelocityValue)) { updateVelocityFromNetwork(value); _lastUpdatedVelocityTimestamp = lastEdited; _lastUpdatedVelocityValue = value; } }; - auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp; - bool valueChanged = value != _lastUpdatedAngularVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); - if (shouldUpdate) { + auto customUpdateAngularVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedAngularVelocityTimestamp, value != _lastUpdatedAngularVelocityValue)) { updateAngularVelocityFromNetwork(value); _lastUpdatedAngularVelocityTimestamp = lastEdited; _lastUpdatedAngularVelocityValue = value; } }; - auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation, filterRejection](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp; - bool valueChanged = value != _lastUpdatedAccelerationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && (valueChanged || filterRejection); - if (shouldUpdate) { + auto customSetAcceleration = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedAccelerationTimestamp, value != _lastUpdatedAccelerationValue)) { setAcceleration(value); _lastUpdatedAccelerationTimestamp = lastEdited; _lastUpdatedAccelerationValue = value; From 18f8c2b866e3aaa4d640cd450f7ac67967d2d7f8 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 31 Jan 2017 13:31:33 -0800 Subject: [PATCH 097/142] comment --- libraries/entities/src/EntityItem.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index a73420c587..7f3c273772 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -664,6 +664,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (wantTerseEditLogging() && _simulationOwner != newSimOwner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner; } + // This is used in the custom physics setters, below. When an entity-server filter alters + // or rejects a set of properties, it clears this. In such cases, we don't want those custom + // setters to ignore what the server says. filterRejection = newSimOwner.getID().isNull(); if (weOwnSimulation) { if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) { From 174a7ad5bdadc72f14875281889b26fb7706ac2b Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 31 Jan 2017 22:54:58 +0100 Subject: [PATCH 098/142] Optimizations and style fixes from code review --- libraries/entities/src/EntityItem.cpp | 16 +++-------- libraries/entities/src/EntityItem.h | 27 +++++++++---------- .../entities/src/EntityScriptingInterface.cpp | 2 ++ libraries/physics/src/EntityMotionState.cpp | 2 ++ 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 61f082c9b6..3c10d0382c 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1278,7 +1278,6 @@ void EntityItem::pokeSimulationOwnership() { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_POKE_SIMULATION_PRIORITY, usecTimestampNow()); } - checkForFirstSimulationBid(_simulationOwner); } void EntityItem::grabSimulationOwnership() { @@ -1291,7 +1290,6 @@ void EntityItem::grabSimulationOwnership() { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); } - checkForFirstSimulationBid(_simulationOwner); } bool EntityItem::setProperties(const EntityItemProperties& properties) { @@ -1862,7 +1860,6 @@ void EntityItem::setSimulationOwner(const QUuid& id, quint8 priority) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << id << priority; } _simulationOwner.set(id, priority); - checkForFirstSimulationBid(_simulationOwner); } void EntityItem::setSimulationOwner(const SimulationOwner& owner) { @@ -1871,7 +1868,6 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } _simulationOwner.set(owner); - checkForFirstSimulationBid(_simulationOwner); } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { @@ -1882,7 +1878,6 @@ void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { if (_simulationOwner.set(owner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } - checkForFirstSimulationBid(_simulationOwner); } void EntityItem::clearSimulationOwnership() { @@ -1899,7 +1894,10 @@ void EntityItem::clearSimulationOwnership() { void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& timestamp) { _simulationOwner.setPendingPriority(priority, timestamp); - checkForFirstSimulationBid(_simulationOwner); +} + +void EntityItem::rememberHasSimulationOwnershipBid() const { + _hasBidOnSimulation = true; } QString EntityItem::actionsToDebugString() { @@ -2157,12 +2155,6 @@ void EntityItem::setActionDataInternal(QByteArray actionData) { checkWaitingToRemove(); } -void EntityItem::checkForFirstSimulationBid(const SimulationOwner& simulationOwner) const { - if (!_hasBidOnSimulation && simulationOwner.matchesValidID(DependencyManager::get()->getSessionUUID())) { - _hasBidOnSimulation = true; - } -} - void EntityItem::serializeActions(bool& success, QByteArray& result) const { if (_objectActions.size() == 0) { success = true; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 98a2a1e268..e69195d53d 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -321,6 +321,7 @@ public: void updateSimulationOwner(const SimulationOwner& owner); void clearSimulationOwnership(); void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); + void rememberHasSimulationOwnershipBid() const; const QString& getMarketplaceID() const { return _marketplaceID; } void setMarketplaceID(const QString& value) { _marketplaceID = value; } @@ -478,8 +479,6 @@ protected: const QByteArray getActionDataInternal() const; void setActionDataInternal(QByteArray actionData); - void checkForFirstSimulationBid(const SimulationOwner& simulationOwner) const; - virtual void locationChanged(bool tellPhysics = true) override; virtual void dimensionsChanged() override; @@ -499,16 +498,16 @@ protected: mutable AABox _cachedAABox; mutable AACube _maxAACube; mutable AACube _minAACube; - mutable bool _recalcAABox = true; - mutable bool _recalcMinAACube = true; - mutable bool _recalcMaxAACube = true; + mutable bool _recalcAABox { true }; + mutable bool _recalcMinAACube { true }; + mutable bool _recalcMaxAACube { true }; float _localRenderAlpha; - float _density = ENTITY_ITEM_DEFAULT_DENSITY; // kg/m^3 + float _density { ENTITY_ITEM_DEFAULT_DENSITY }; // kg/m^3 // NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class // rather than in all of the derived classes. If we ever collapse these classes to one we could do it a // different way. - float _volumeMultiplier = 1.0f; + float _volumeMultiplier { 1.0f }; glm::vec3 _gravity; glm::vec3 _acceleration; float _damping; @@ -518,7 +517,7 @@ protected: QString _script; /// the value of the script property QString _loadedScript; /// the value of _script when the last preload signal was sent - quint64 _scriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload + quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload QString _serverScripts; /// keep track of time when _serverScripts property was last changed @@ -526,7 +525,7 @@ protected: /// the value of _scriptTimestamp when the last preload signal was sent // NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it - quint64 _loadedScriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; + quint64 _loadedScriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; QString _collisionSoundURL; SharedSoundPointer _collisionSound; @@ -564,8 +563,8 @@ protected: uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation // these backpointers are only ever set/cleared by friends: - EntityTreeElementPointer _element = nullptr; // set by EntityTreeElement - void* _physicsInfo = nullptr; // set by EntitySimulation + EntityTreeElementPointer _element { nullptr }; // set by EntityTreeElement + void* _physicsInfo { nullptr }; // set by EntitySimulation bool _simulated; // set by EntitySimulation bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action); @@ -582,14 +581,14 @@ protected: // are used to keep track of and work around this situation. void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr); mutable QSet _actionsToRemove; - mutable bool _actionDataDirty = false; - mutable bool _actionDataNeedsTransmit = false; + mutable bool _actionDataDirty { false }; + mutable bool _actionDataNeedsTransmit { false }; // _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag static quint64 _rememberDeletedActionTime; mutable QHash _previouslyDeletedActions; // per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership - mutable bool _hasBidOnSimulation = false; + mutable bool _hasBidOnSimulation { false }; QUuid _sourceUUID; /// the server node UUID we came from diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 6fb5d14329..85c3fc74f6 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -231,6 +231,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties // and make note of it now, so we can act on it right away. propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); + entity->rememberHasSimulationOwnershipBid(); } entity->setLastBroadcast(usecTimestampNow()); @@ -444,6 +445,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->pokeSimulationOwnership(); + entity->rememberHasSimulationOwnershipBid(); } } if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index b0bdc34b52..02cee9a03a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -582,6 +582,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS; // copy _outgoingPriority into pendingPriority... _entity->setPendingOwnershipPriority(_outgoingPriority, now); + // don't forget to remember that we have made a bid + _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { From 06d797bb3b04217452cf13d8ed12b9544a0a527c Mon Sep 17 00:00:00 2001 From: Faye Li Date: Tue, 31 Jan 2017 14:27:17 -0800 Subject: [PATCH 099/142] load bootstrao --- scripts/system/html/users.html | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index 23de9cf0b0..dd52af0620 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -13,6 +13,7 @@ +
    -
    +
    Users Online
    - +
    • Everyone (0)
    • Friends (0)
    • @@ -157,6 +203,26 @@
    + + + +