mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 16:55:07 +02:00
Merge pull request #7177 from howard-stearns/ac-sound-searcher-update
Update sound searcher and spawner for load handling.
This commit is contained in:
commit
9e2d326b30
2 changed files with 280 additions and 205 deletions
|
@ -1,218 +1,260 @@
|
|||
"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 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 * 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 = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.)
|
||||
|
||||
var DEFAULT_SOUND_DATA = {
|
||||
volume: 0.5,
|
||||
loop: false,
|
||||
playbackGap: 1000, // in ms
|
||||
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: MSEC_PER_SEC, // 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.
|
||||
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;
|
||||
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']);
|
||||
examinationCount++; // Collect statistics on how many getEntityProperties we do.
|
||||
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 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();
|
||||
}
|
||||
var options = {
|
||||
position: properties.position,
|
||||
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 = randomizedNextPlay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC
|
||||
if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap.
|
||||
if (repeat()) { // WAITING => PLAYING
|
||||
// Setup next play just once, now. Changes won't be looked at while we wait.
|
||||
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; // was one-shot and we're finished
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
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.
|
||||
// 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++;
|
||||
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);
|
||||
|
|
|
@ -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 = 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");
|
||||
var orientation = Camera.getOrientation();
|
||||
orientation = Quat.safeEulerAngles(orientation);
|
||||
|
@ -20,20 +26,21 @@ 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",
|
||||
volume: 0.3,
|
||||
loop: false,
|
||||
playbackGap: 2000, // 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
|
Loading…
Reference in a new issue