From 4bb76d6a203047a36ce15f8d1592e4361208cd84 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 23 Feb 2016 16:26:47 -0800 Subject: [PATCH 1/4] Update sound searcher and spawner for load handling. --- .../ACAudioSearchAndInject.js | 397 +++++++++--------- .../acAudioSearchCompatibleEntitySpawner.js | 55 ++- 2 files changed, 250 insertions(+), 202 deletions(-) diff --git a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index f7e983d683..3d1621a1b2 100644 --- a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -1,218 +1,233 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/ // // ACAudioSearchAndInject.js // audio // -// Created by Eric Levin 2/1/2016 +// Created by Eric Levin and Howard Stearns 2/1/2016 // Copyright 2016 High Fidelity, Inc. - -// This AC script searches for special sound entities nearby avatars and plays those sounds based off information specified in the entity's -// user data field ( see acAudioSearchAndCompatibilityEntitySpawner.js for an example) +// +// Keeps track of all sounds within QUERY_RADIUS of an avatar, where a "sound" is specified in entity userData. +// Inject as many as practical into the audio mixer. +// See acAudioSearchAndCompatibilityEntitySpawner.js. +// +// This implementation takes some precautions to scale well: +// - It doesn't hastle the entity server because it issues at most one octree query every UPDATE_TIME period, regardless of the number of avatars. +// - It does not load itself because it only gathers entities once every UPDATE_TIME period, and only +// checks entity properties for those small number of entities that are currently playing (plus a RECHECK_TIME period examination of all entities). +// This implementation tries to use all the available injectors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -Script.include("https://rawgit.com/highfidelity/hifi/master/examples/libraries/utils.js"); - -var SOUND_DATA_KEY = "soundKey"; - -var QUERY_RADIUS = 50; - -EntityViewer.setKeyholeRadius(QUERY_RADIUS); -Entities.setPacketsPerSecond(6000); - -Agent.isAvatar = true; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key. +var old_sound_data_key = "soundKey"; // For backwards compatibility. +var QUERY_RADIUS = 50; // meters +var UPDATE_TIME = 100; // ms. We'll update just one thing on this period. +var EXPIRATION_TIME = 5 * 1000; // ms. Remove sounds that have been out of range for this time. +var RECHECK_TIME = 10 * 1000; // ms. Check for new userData properties this often when not currently playing. +// (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.) +var UPDATES_PER_STATS_LOG = 50; var DEFAULT_SOUND_DATA = { - volume: 0.5, - loop: false, + volume: 0.5, // userData cannot specify zero volume with our current method of defaulting. + loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value. playbackGap: 1000, // in ms playbackGapRange: 0 // in ms }; -var MIN_PLAYBACK_GAP = 0; -var UPDATE_TIME = 100; -var EXPIRATION_TIME = 5000; +Script.include("../../libraries/utils.js"); +Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. +function ignore() {} +function debug() { // Display the arguments not just [Object object]. + //print.apply(null, [].map.call(arguments, JSON.stringify)); +} -var soundEntityMap = {}; -var soundUrls = {}; +EntityViewer.setKeyholeRadius(QUERY_RADIUS); -var avatarPositions = []; - - -function update() { - var avatars = AvatarList.getAvatarIdentifiers(); - for (var i = 0; i < avatars.length; i++) { - var avatar = AvatarList.getAvatar(avatars[i]); - var avatarPosition = avatar.position; - if (!avatarPosition) { - continue; +// ENTITY DATA CACHE +// +var entityCache = {}; // A dictionary of unexpired EntityData objects. +function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about. + // This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer. + var that = this; + that.lastUserDataUpdate = 0; // new entity is in need of rechecking user data + // State Transitions: + // no data => no data | sound data | expired + // expired => stop => remove + // sound data => downloading + // downloading => downloading | waiting + // waiting => playing | waiting (if too many already playing) + // playing => update position etc | no data + that.stop = function stop() { + if (!that.sound) { + return; } - EntityViewer.setPosition(avatarPosition); - EntityViewer.queryOctree(); - avatarPositions.push(avatarPosition); + print("stopping sound", entityIdentifier, that.url); + delete that.sound; + delete that.url; + if (!that.injector) { + return; + } + that.injector.stop(); + delete that.injector; + }; + this.update = function stateTransitions(expirationCutoff, userDataCutoff, now) { + if (that.timestamp < expirationCutoff) { // EXPIRED => STOP => REMOVE + that.stop(); // Alternatively, we could fade out and then stop... + delete entityCache[entityIdentifier]; + return; + } + var properties, soundData; // Latest data, pulled from local octree. + // getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results. + // Most entity updates are fast and only a very few do getEntityProperties. + function ensureSoundData() { // We only getEntityProperities when we need to. + if (properties) { + return; + } + properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']); + debug("updating", that, properties); + try { + var userData = properties.userData && JSON.parse(properties.userData); + soundData = userData && (userData[SOUND_DATA_KEY] || userData[old_sound_data_key]); // Don't store soundData yet. Let state changes compare. + that.lastUserDataUpdate = now; // But do update these ... + that.url = soundData && soundData.url; + that.playAfter = that.url && now; + } catch (err) { + print(err, properties.userData); + } + } + // Stumbling on big new pile of entities will do a lot of getEntityProperties. Once. + if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA + ensureSoundData(); + } + if (!that.url) { // NO DATA => NO DATA + return that.stop(); + } + if (!that.sound) { // SOUND DATA => DOWNLOADING + that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can. + } + if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING + return; + } + if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING + return; + } + ensureSoundData(); // We'll play and will need position, so we might as well get soundData, too. + if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around) + return that.stop(); + } + var options = { + position: properties.position, + loop: soundData.loop || DEFAULT_SOUND_DATA.loop, + volume: soundData.volume || DEFAULT_SOUND_DATA.volume + }; + if (!that.injector) { // WAITING => PLAYING | WAITING + debug("starting", that, options); + that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later. + if (that.injector) { + print("started", entityIdentifier, that.url); + } else { // Don't hammer ensureSoundData or injector manager. + that.playAfter = now + (soundData.playbackGap || RECHECK_TIME); + } + return; + } + that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC + if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap. + var gap = soundData.playbackGap || DEFAULT_SOUND_DATA; + if (gap) { // WAITING => PLAYING + gap = gap + randFloat(-Math.max(gap, soundData.playbackGapRange), soundData.playbackGapRange); // gapRange is bad name. Meant as +/- value. + // Setup next play just once, now. Changes won't be looked at while we wait. + that.playAfter = now + (that.sound.duration * 1000) + gap; + // Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter. + that.injector.restart(); + } else { // PLAYING => NO DATA + that.playAfter = Infinity; + } + } + }; +} +function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) { + ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play. + var entitySound = entityCache[entityIdentifier]; + if (!entitySound) { + entitySound = entityCache[entityIdentifier] = new EntityDatum(entityIdentifier); } - Script.setTimeout(function() { - avatarPositions.forEach(function(avatarPosition) { - var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); - handleFoundSoundEntities(entities); + entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine. +} +var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now(); +function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds. + var now = Date.now(), + expirationCutoff = now - EXPIRATION_TIME, + userDataRecheckCutoff = now - RECHECK_TIME; + Object.keys(entityCache).forEach(function (entityIdentifier) { + entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now); + }); + if (nUpdates-- <= 0) { // Report statistics. + // My figures using acAudioSearchCompatibleEntitySpawner.js with ONE user, N_SOUNDS = 2000, N_SILENT_ENTITIES_PER_SOUND = 5: + // audio-mixer: 23% of cpu (on Mac Activity Monitor) + // this script's assignment client: 106% of cpu. (overloaded) + // entities:12003 + // sounds:2000 + // playing:40 (correct) + // millisecondsPerUpdate:135 (100 requested, so behind by 35%. It would be nice to dig into why...) + var stats = {entities: 0, sounds: 0, playing: 0, millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG}; + nUpdates = UPDATES_PER_STATS_LOG; + lastStats = now; + Object.keys(entityCache).forEach(function (entityIdentifier) { + var datum = entityCache[entityIdentifier]; + stats.entities++; + if (datum.url) { + stats.sounds++; + if (datum.injector && datum.injector.isPlaying) { + stats.playing++; + } + } }); - //Now wipe list for next query; - avatarPositions = []; - }, UPDATE_TIME); - handleActiveSoundEntities(); -} - -function handleActiveSoundEntities() { - // Go through all our sound entities, if they have passed expiration time, remove them from map - for (var potentialSoundEntity in soundEntityMap) { - if (!soundEntityMap.hasOwnProperty(potentialSoundEntity)) { - // The current property is not a direct property of soundEntityMap so ignore it - continue; - } - var soundEntity = potentialSoundEntity; - var soundProperties = soundEntityMap[soundEntity]; - soundProperties.timeWithoutAvatarInRange += UPDATE_TIME; - if (soundProperties.timeWithoutAvatarInRange > EXPIRATION_TIME && soundProperties.soundInjector) { - // An avatar hasn't been within range of this sound entity recently, so remove it from map - soundProperties.soundInjector.stop(); - delete soundEntityMap[soundEntity]; - } else if (soundProperties.isDownloaded) { - // If this sound hasn't expired yet, we want to potentially play it! - if (soundProperties.readyToPlay) { - var newPosition = Entities.getEntityProperties(soundEntity, "position").position; - if (!soundProperties.soundInjector) { - soundProperties.soundInjector = Audio.playSound(soundProperties.sound, { - volume: soundProperties.volume, - position: newPosition, - loop: soundProperties.loop - }); - } else { - soundProperties.soundInjector.restart(); - } - soundProperties.readyToPlay = false; - } else if (soundProperties.sound && soundProperties.loop === false) { - // We need to check all of our entities that are not looping but have an interval associated with them - // to see if it's time for them to play again - soundProperties.timeSinceLastPlay += UPDATE_TIME; - if (soundProperties.timeSinceLastPlay > soundProperties.clipDuration + soundProperties.currentPlaybackGap) { - soundProperties.readyToPlay = true; - soundProperties.timeSinceLastPlay = 0; - // Now let's get our new current interval - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - } - } - } + print(JSON.stringify(stats)); } } - -function handleFoundSoundEntities(entities) { - entities.forEach(function(entity) { - var soundData = getEntityCustomData(SOUND_DATA_KEY, entity); - if (soundData && soundData.url) { - //check sound entities list- if it's not in, add it - if (!soundEntityMap[entity]) { - var soundProperties = { - url: soundData.url, - volume: soundData.volume || DEFAULT_SOUND_DATA.volume, - loop: soundData.loop || DEFAULT_SOUND_DATA.loop, - playbackGap: soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap, - playbackGapRange: soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, - readyToPlay: false, - position: Entities.getEntityProperties(entity, "position").position, - timeSinceLastPlay: 0, - timeWithoutAvatarInRange: 0, - isDownloaded: false - }; - - - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - - - soundEntityMap[entity] = soundProperties; - if (!soundUrls[soundData.url]) { - // We need to download sound before we add it to our map - var sound = SoundCache.getSound(soundData.url); - // Only add it to map once it's downloaded - soundUrls[soundData.url] = sound; - sound.ready.connect(function() { - soundProperties.sound = sound; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundProperties.clipDuration = sound.duration * 1000; - soundEntityMap[entity] = soundProperties; - - }); - } else { - // We already have sound downloaded, so just add it to map right away - soundProperties.sound = soundUrls[soundData.url]; - soundProperties.clipDuration = soundProperties.sound.duration * 1000; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundEntityMap[entity] = soundProperties; - } - } else { - //If this sound is in our map already, we want to reset timeWithoutAvatarInRange - // Also we want to check to see if the entity has been updated with new sound data- if so we want to update! - soundEntityMap[entity].timeWithoutAvatarInRange = 0; - checkForSoundPropertyChanges(soundEntityMap[entity], soundData); - } - } +// Update the set of which EntityData we know about. +// +function updateEntiesForAvatar(avatarIdentifier) { // Just one piece of update work. + // This does at most: + // one queryOctree request of the entity server, and + // one findEntities geometry query of our own octree, and + // a quick internEntityDatum of each of what may be a large number of entityIdentifiers. + // The idea is that this is a nice bounded piece of work that should not be done too frequently. + // However, it means that we won't learn about new entities until, on average (nAvatars * UPDATE_TIME) + query round trip. + var avatar = AvatarList.getAvatar(avatarIdentifier), avatarPosition = avatar && avatar.position; + if (!avatarPosition) { // No longer here. + return; + } + var timestamp = Date.now(); + EntityViewer.setPosition(avatarPosition); + EntityViewer.queryOctree(); // Requests an update, but there's no telling when we'll actually see different results. + var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); + debug("found", entities.length, "entities near", avatar.name || "unknown", "at", avatarPosition); + entities.forEach(function (entityIdentifier) { + internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar); }); } -function checkForSoundPropertyChanges(currentProps, newProps) { - var needsNewInjector = false; - - if (currentProps.playbackGap !== newProps.playbackGap && !currentProps.loop) { - // playbackGap only applies to non looping sounds - currentProps.playbackGap = newProps.playbackGap; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - - if (currentProps.playbackGapRange !== currentProps.playbackGapRange) { - currentProps.playbackGapRange = newProps.playbackGapRange; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - if (currentProps.volume !== newProps.volume) { - currentProps.volume = newProps.volume; - needsNewInjector = true; - } - if (currentProps.url !== newProps.url) { - currentProps.url = newProps.url; - currentProps.sound = null; - if (!soundUrls[currentProps.url]) { - var sound = SoundCache.getSound(currentProps.url); - currentProps.isDownloaded = false; - sound.ready.connect(function() { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - currentProps.isDownloaded = true; - }); - } else { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - } - needsNewInjector = true; - } - - if (currentProps.loop !== newProps.loop) { - currentProps.loop = newProps.loop; - needsNewInjector = true; - } - if (needsNewInjector) { - // If we were looping we need to stop that so new changes are applied - currentProps.soundInjector.stop(); - currentProps.soundInjector = null; - currentProps.readyToPlay = true; - } - +// Slowly update the set of data we have to work with. +// +var workQueue = []; +function updateWorkQueueForAvatarsPresent() { // when nothing else to do, fill queue with individual avatar updates + workQueue = AvatarList.getAvatarIdentifiers().map(function (avatarIdentifier) { + return function () { + updateEntiesForAvatar(avatarIdentifier); + }; + }); } - -Script.setInterval(update, UPDATE_TIME); +Script.setInterval(function () { + // There might be thousands of EntityData known to us, but only a few will require any work to update. + updateAllEntityData(); // i.e., this better be pretty fast. + // Each interval, we do no more than one updateEntitiesforAvatar. + if (!workQueue.length) { + workQueue = [updateWorkQueueForAvatarsPresent]; + } + workQueue.pop()(); // There's always one +}, UPDATE_TIME); diff --git a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js index 126635ee7a..5ebd7193e5 100644 --- a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js +++ b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js @@ -1,4 +1,6 @@ -// +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global Entities, Script, Quat, Vec3, Camera, MyAvatar, print, randFloat*/ // acAudioSearchCompatibleEntitySpawner.js // audio/acAudioSearching // @@ -13,6 +15,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +var N_SOUNDS = 2000; +var N_SILENT_ENTITIES_PER_SOUND = 5; +var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server. +var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties. Script.include("../../libraries/utils.js"); var orientation = Camera.getOrientation(); orientation = Quat.safeEulerAngles(orientation); @@ -20,7 +26,7 @@ orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); // http://hifi-public.s3.amazonaws.com/ryan/demo/0619_Fireplace__Tree_B.L.wav -var SOUND_DATA_KEY = "soundKey"; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; var userData = { soundKey: { url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav", @@ -29,11 +35,12 @@ var userData = { playbackGap: 2000, // In ms - time to wait in between clip plays playbackGapRange: 500 // In ms - the range to wait in between clip plays } -} +}; +var userDataString = JSON.stringify(userData); var entityProps = { type: "Box", - position: center, + name: 'audioSearchEntity', color: { red: 200, green: 10, @@ -43,15 +50,41 @@ var entityProps = { x: 0.1, y: 0.1, z: 0.1 - }, - userData: JSON.stringify(userData) + } +}; + +var entities = [], nSounds = 0; +Script.include("../../libraries/utils.js"); +function addOneSet() { + function randomizeDimension(coordinate) { + return coordinate + randFloat(-SPATIAL_DISTRIBUTION / 2, SPATIAL_DISTRIBUTION / 2); + } + function randomize() { + return {x: randomizeDimension(center.x), y: randomizeDimension(center.y), z: randomizeDimension(center.z)}; + } + function addOne() { + entityProps.position = randomize(); + entities.push(Entities.addEntity(entityProps)); + } + var i; + entityProps.userData = userDataString; + entityProps.color.red = 200; + addOne(); + delete entityProps.userData; + entityProps.color.red = 10; + for (i = 0; i < N_SILENT_ENTITIES_PER_SOUND; i++) { + addOne(); + } + if (++nSounds < N_SOUNDS) { + Script.setTimeout(addOneSet, ADD_PERIOD); + } } - -var soundEntity = Entities.addEntity(entityProps); - +addOneSet(); function cleanup() { - Entities.deleteEntity(soundEntity); + entities.forEach(Entities.deleteEntity); } +// In console: +// Entities.findEntities(MyAvatar.position, 100).forEach(function (id) { if (Entities.getEntityProperties(id).name === 'audioSearchEntity') Entities.deleteEntity(id); }) -Script.scriptEnding.connect(cleanup); \ No newline at end of file +Script.scriptEnding.connect(cleanup); From d1088031bfbd024c42990dabad7c22dc197b1534 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 23 Feb 2016 18:13:40 -0800 Subject: [PATCH 2/4] Longer gap, as an experiment. --- .../acAudioSearching/acAudioSearchCompatibleEntitySpawner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js index 5ebd7193e5..f90c0aefcf 100644 --- a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js +++ b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js @@ -32,7 +32,7 @@ var userData = { url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav", volume: 0.3, loop: false, - playbackGap: 2000, // In ms - time to wait in between clip plays + playbackGap: 7000, // In ms - time to wait in between clip plays playbackGapRange: 500 // In ms - the range to wait in between clip plays } }; From 90b5cdb30f33d265c7fc0cec76cd3ec980a774b7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Feb 2016 16:27:46 -0800 Subject: [PATCH 3/4] Fix bug. Annotate. --- .../ACAudioSearchAndInject.js | 65 +++++++++++++------ .../acAudioSearchCompatibleEntitySpawner.js | 8 +-- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index 3d1621a1b2..cd6cf88f26 100644 --- a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -21,19 +21,20 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +var MSEC_PER_SEC = 1000; var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key. var old_sound_data_key = "soundKey"; // For backwards compatibility. var QUERY_RADIUS = 50; // meters var UPDATE_TIME = 100; // ms. We'll update just one thing on this period. -var EXPIRATION_TIME = 5 * 1000; // ms. Remove sounds that have been out of range for this time. -var RECHECK_TIME = 10 * 1000; // ms. Check for new userData properties this often when not currently playing. +var EXPIRATION_TIME = 5 * MSEC_PER_SEC; // ms. Remove sounds that have been out of range for this time. +var RECHECK_TIME = 10 * MSEC_PER_SEC; // ms. Check for new userData properties this often when not currently playing. // (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.) -var UPDATES_PER_STATS_LOG = 50; +var UPDATES_PER_STATS_LOG = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.) var DEFAULT_SOUND_DATA = { volume: 0.5, // userData cannot specify zero volume with our current method of defaulting. loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value. - playbackGap: 1000, // in ms + playbackGap: MSEC_PER_SEC, // in ms playbackGapRange: 0 // in ms }; @@ -49,6 +50,7 @@ EntityViewer.setKeyholeRadius(QUERY_RADIUS); // ENTITY DATA CACHE // var entityCache = {}; // A dictionary of unexpired EntityData objects. +var examinationCount = 0; function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about. // This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer. var that = this; @@ -87,6 +89,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n return; } properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']); + examinationCount++; // Collect statistics on how many getEntityProperties we do. debug("updating", that, properties); try { var userData = properties.userData && JSON.parse(properties.userData); @@ -114,7 +117,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING return; } - ensureSoundData(); // We'll play and will need position, so we might as well get soundData, too. + ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too. if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around) return that.stop(); } @@ -123,27 +126,31 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n loop: soundData.loop || DEFAULT_SOUND_DATA.loop, volume: soundData.volume || DEFAULT_SOUND_DATA.volume }; + function repeat() { return !options.loop && (soundData.playbackGap >= 0); } + function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work + var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, + base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME; + return now + base + randFloat(-Math.min(base, range), range); + } if (!that.injector) { // WAITING => PLAYING | WAITING debug("starting", that, options); that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later. if (that.injector) { print("started", entityIdentifier, that.url); } else { // Don't hammer ensureSoundData or injector manager. - that.playAfter = now + (soundData.playbackGap || RECHECK_TIME); + that.playAfter = randomizedNextPlay(); } return; } that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap. - var gap = soundData.playbackGap || DEFAULT_SOUND_DATA; - if (gap) { // WAITING => PLAYING - gap = gap + randFloat(-Math.max(gap, soundData.playbackGapRange), soundData.playbackGapRange); // gapRange is bad name. Meant as +/- value. + if (repeat()) { // WAITING => PLAYING // Setup next play just once, now. Changes won't be looked at while we wait. - that.playAfter = now + (that.sound.duration * 1000) + gap; + that.playAfter = randomizedNextPlay(); // Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter. that.injector.restart(); - } else { // PLAYING => NO DATA - that.playAfter = Infinity; + } else { // PLAYING => NO DATA + that.playAfter = Infinity; // was one-shot and we're finished } } }; @@ -165,16 +172,34 @@ function updateAllEntityData() { // A fast update of all entities we know about. entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now); }); if (nUpdates-- <= 0) { // Report statistics. - // My figures using acAudioSearchCompatibleEntitySpawner.js with ONE user, N_SOUNDS = 2000, N_SILENT_ENTITIES_PER_SOUND = 5: - // audio-mixer: 23% of cpu (on Mac Activity Monitor) - // this script's assignment client: 106% of cpu. (overloaded) - // entities:12003 - // sounds:2000 - // playing:40 (correct) - // millisecondsPerUpdate:135 (100 requested, so behind by 35%. It would be nice to dig into why...) - var stats = {entities: 0, sounds: 0, playing: 0, millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG}; + // For example, with: + // injector-limit = 40 (in C++ code) + // N_SOUNDS = 1000 (from userData in, e.g., acAudioSearchCompatibleEntitySpawner.js) + // replay-period = 3 + 20 = 23 (seconds, ditto) + // stats-period = UPDATES_PER_STATS_LOG * UPDATE_TIME / MSEC_PER_SEC = 10 seconds + // The log should show between each stats report: + // "start" lines ~= injector-limit * P(finish) = injector-limit * stats-period/replay-period = 17 ? + // total attempts at starting ("start" lines + "could not thread" lines) ~= N_SOUNDS = 1000 ? + // entities > N_SOUNDS * (1+ N_SILENT_ENTITIES_PER_SOUND) = 11000 + whatever was in the scene before running spawner + // sounds = N_SOUNDS = 1000 + // getEntityPropertiesPerUpdate ~= playing + failed-starts/UPDATES_PER_STATS_LOG + other-rechecks-each-update + // = injector-limit + (total attempts - "start" lines)/UPDATES_PER_STATS__LOG + // + (entities - playing - failed-starts/UPDATES_PER_STATS_LOG) * P(recheck-in-update) + // where failed-starts/UPDATES_PER_STATS_LOG = (1000-17)/100 = 10 + // = 40 + 10 + (11000 - 40 - 10)*UPDATE_TIME/RECHECK_TIME + // = 40 + 10 + 10950*0.01 = 159 (mostly proportional to enties/RECHECK_TIME) + // millisecondsPerUpdate ~= UPDATE_TIME = 100 (+ some timer machinery time) + // this assignment client activity monitor < 100% cpu + var stats = { + entities: 0, + sounds: 0, + playing: 0, + getEntityPropertiesPerUpdate: examinationCount / UPDATES_PER_STATS_LOG, + millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG + }; nUpdates = UPDATES_PER_STATS_LOG; lastStats = now; + examinationCount = 0; Object.keys(entityCache).forEach(function (entityIdentifier) { var datum = entityCache[entityIdentifier]; stats.entities++; diff --git a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js index f90c0aefcf..2a80a712b6 100644 --- a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js +++ b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js @@ -15,8 +15,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var N_SOUNDS = 2000; -var N_SILENT_ENTITIES_PER_SOUND = 5; +var N_SOUNDS = 1000; +var N_SILENT_ENTITIES_PER_SOUND = 10; var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server. var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties. Script.include("../../libraries/utils.js"); @@ -32,8 +32,8 @@ var userData = { url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav", volume: 0.3, loop: false, - playbackGap: 7000, // In ms - time to wait in between clip plays - playbackGapRange: 500 // In ms - the range to wait in between clip plays + playbackGap: 20000, // In ms - time to wait in between clip plays + playbackGapRange: 5000 // In ms - the range to wait in between clip plays } }; From 077d3310049aadd7d1e2986f57672d47e71d9f05 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Feb 2016 09:34:07 -0800 Subject: [PATCH 4/4] whitespace --- .../audioExamples/acAudioSearching/ACAudioSearchAndInject.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index cd6cf88f26..2be59365b8 100644 --- a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -126,7 +126,9 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n loop: soundData.loop || DEFAULT_SOUND_DATA.loop, volume: soundData.volume || DEFAULT_SOUND_DATA.volume }; - function repeat() { return !options.loop && (soundData.playbackGap >= 0); } + function repeat() { + return !options.loop && (soundData.playbackGap >= 0); + } function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME;