// ambientSound.js // // This entity script will allow you to create an ambient sound that loops when a person is within a given // range of this entity. Great way to add one or more ambisonic soundfields to your environment. // // In the userData section for the entity, add/edit three values: // userData.soundURL should be a string giving the URL to the sound file. Defaults to 100 meters if not set. // userData.range should be an integer for the max distance away from the entity where the sound will be audible. // userData.maxVolume is the max volume at which the clip should play. Defaults to 1.0 full volume. // userData.disabled is an optionanl boolean flag which can be used to disable the ambient sound. Defaults to false. // // The rotation of the entity is copied to the ambisonic field, so by rotating the entity you will rotate the // direction in-which a certain sound comes from. // // Remember that the entity has to be visible to the user for the sound to play at all, so make sure the entity is // large enough to be loaded at the range you set, particularly for large ranges. // // Copyright 2016 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(){ // This sample clip and range will be used if you don't add userData to the entity (see above) var DEFAULT_RANGE = 100; var DEFAULT_URL = "http://hifi-content.s3.amazonaws.com/ken/samples/forest_ambiX.wav"; var DEFAULT_VOLUME = 1.0; var HALF_MULTIPLIER = 0.5; var batData; var tornadoData; var BAT_ORBIT_RADIUS = 50; var DEFAULT_USERDATA = { soundURL: DEFAULT_URL, range: DEFAULT_RANGE, maxVolume: DEFAULT_VOLUME }; var soundURL = ""; var soundOptions = { loop: true, localOnly: true, }; var range = DEFAULT_RANGE; var maxVolume = DEFAULT_VOLUME; var disabled = false; var UPDATE_INTERVAL_MSECS = 100; var rotation; var entity; var ambientSound; var center; var soundPlaying = false; var checkTimer = false; var _this; var COLOR_OFF = { red: 128, green: 128, blue: 128 }; var COLOR_ON = { red: 255, green: 0, blue: 0 }; var WANT_DEBUG = false; function getPropertiesFromNamedObjects(entityName, searchOriginPosition, searchRadius, arrayOfProperties) { var entityList = Entities.findEntitiesByName( entityName, searchOriginPosition, searchRadius ); var returnedObjects = []; if (entityList.length > 0) { entityList.forEach(function(entity){ var properties = Entities.getEntityProperties(entity, arrayOfProperties); returnedObjects.push(properties); }); return returnedObjects; } else { console.log("GOT NOTHING"); return null; } } function isPositionInsideBox(position, boxProperties) { var localPosition = Vec3.multiplyQbyV(Quat.inverse(boxProperties.rotation), Vec3.subtract(position, boxProperties.position)); var halfDimensions = Vec3.multiply(boxProperties.dimensions, HALF_MULTIPLIER); return -halfDimensions.x <= localPosition.x && halfDimensions.x >= localPosition.x && -halfDimensions.y <= localPosition.y && halfDimensions.y >= localPosition.y && -halfDimensions.z <= localPosition.z && halfDimensions.z >= localPosition.z; } function isSomeAvatarOtherThanMeStillInsideTheObject(objectProperties) { var result = false; AvatarList.getAvatarIdentifiers().forEach(function(avatarID) { var avatar = AvatarList.getAvatar(avatarID); if (avatar.sessionUUID !== MyAvatar.sessionUUID) { if (isPositionInsideBox(avatar.position, objectProperties)) { result = true; } } }); return result; } function debugPrint(string) { if (WANT_DEBUG) { print("ambientSound | " + string); } } this.updateSettings = function() { // Check user data on the entity for any changes var oldSoundURL = soundURL; var props = Entities.getEntityProperties(entity, [ "userData" ]); if (props.userData) { try { var data = JSON.parse(props.userData); } catch(e) { debugPrint("unable to parse userData JSON string: " + props.userData); this.cleanup(); return; } if (data.soundURL && !(soundURL === data.soundURL)) { soundURL = data.soundURL; debugPrint("Read ambient sound URL: " + soundURL); } if (data.range && !(range === data.range)) { range = data.range; debugPrint("Read ambient sound range: " + range); } // Check known aliases for the "volume" setting (which allows for inplace upgrade of existing marketplace entities) data.maxVolume = data.maxVolume || data.soundVolume || data.volume; if (data.maxVolume && !(maxVolume === data.maxVolume)) { maxVolume = data.maxVolume; debugPrint("Read ambient sound volume: " + maxVolume); } } if (disabled) { this.cleanup(); soundURL = ""; return; } else if (!checkTimer) { checkTimer = Script.setInterval(_this.maybeUpdate, UPDATE_INTERVAL_MSECS); } if (!(soundURL === oldSoundURL) || (soundURL === "")) { if (soundURL) { debugPrint("Loading ambient sound into cache"); // Use prefetch to detect URL loading errors var resource = SoundCache.prefetch(soundURL); function onStateChanged() { if (resource.state === Resource.State.FINISHED) { resource.stateChanged.disconnect(onStateChanged); ambientSound = SoundCache.getSound(soundURL); } else if (resource.state === Resource.State.FAILED) { resource.stateChanged.disconnect(onStateChanged); debugPrint("Failed to download ambient sound: " + soundURL); } } resource.stateChanged.connect(onStateChanged); onStateChanged(resource.state); } if (soundPlaying && soundPlaying.playing) { debugPrint("URL changed, stopping current ambient sound"); soundPlaying.stop(); soundPlaying = false; } } } this.preload = function(entityID) { // Load the sound and range from the entity userData fields, and note the position of the entity. debugPrint("Ambient sound preload " + entityID); zoneProperties = Entities.getEntityProperties(entityID, ["position", "dimensions", "rotation", "userData"]); var position = Entities.getEntityProperties(entityID, "position").position; batData = getPropertiesFromNamedObjects("bat", position, BAT_ORBIT_RADIUS*2, ["position"]); tornadoData = getPropertiesFromNamedObjects("bat_ambient_zone", position, BAT_ORBIT_RADIUS*2, ["position"]); entity = entityID; _this = this; var props = Entities.getEntityProperties(entity, [ "userData" ]); var data = {}; if (props.userData) { data = JSON.parse(props.userData); } var changed = false; for(var p in DEFAULT_USERDATA) { if (!(p in data)) { data[p] = DEFAULT_USERDATA[p]; changed = true; } } if (changed) { debugPrint("applying default values to userData"); Entities.editEntity(entity, { userData: JSON.stringify(data) }); } this.updateSettings(); // Subscribe to toggle notifications using entity ID as a channel name Messages.subscribe(entity); Messages.messageReceived.connect(this, "_onMessageReceived"); }; this._onMessageReceived = function(channel, message, sender, local) { // Handle incoming toggle notifications if (channel === entity && message === "toggled") { debugPrint("received " + message + " from " + sender); this.updateSettings(); } }; this.maybeUpdate = function() { // Every UPDATE_INTERVAL_MSECS, update the volume of the ambient sound based on distance from my avatar _this.updateSettings(); var HYSTERESIS_FRACTION = 0.1; var props = Entities.getEntityProperties(entity, [ "position", "rotation" ]); if (disabled || !props.position) { _this.cleanup(); return; } center = props.position; rotation = props.rotation; var distance = Vec3.length(Vec3.subtract(MyAvatar.position, center)); if (distance <= range) { var volume = (1.0 - distance / range) * maxVolume; soundOptions.orientation = rotation; soundOptions.volume = volume; soundOptions.localOnly = true; if (!soundPlaying && ambientSound && ambientSound.downloaded && batData.length > 0 && tornadoData.length > 0) { debugPrint("Starting ambient sound: " + soundURL + " (duration: " + ambientSound.duration + ")"); soundPlaying = Audio.playSound(ambientSound, soundOptions); if (!isSomeAvatarOtherThanMeStillInsideTheObject(zoneProperties)) { console.log("BATDATA appear", JSON.stringify(batData)); var angularVelocity = { x: 0, y: 6*Math.PI, z: 0 }; Entities.callEntityServerMethod(tornadoData.id, "spinTheBats", [angularVelocity]); batData.forEach(function(element){ console.log("BATDATA Element appear", JSON.stringify(element)); var delay = 1; Script.setTimeout(function(){ var isTrue = true; Entities.callEntityServerMethod(element.id, "apparateBats", [isTrue]); }, delay*200); delay += 1; }); } } else if (soundPlaying && soundPlaying.playing) { soundPlaying.setOptions(soundOptions); } } else if (soundPlaying && soundPlaying.playing && (distance > range * HYSTERESIS_FRACTION) && batData.length > 0 && tornadoData.length > 0) { soundPlaying.stop(); soundPlaying = false; console.log("BATDATA disappear", JSON.stringify(batData)); if (!isSomeAvatarOtherThanMeStillInsideTheObject(zoneProperties)) { Script.setTimeout(function(){ batData.forEach(function(element){ console.log("BATDATA Element disappear", JSON.stringify(element)); var isTrue = false; Entities.callEntityServerMethod(element.id, "apparateBats", [isTrue]); }); }, 6000); } debugPrint("Out of range, stopping ambient sound: " + soundURL); } }; this.unload = function(entityID) { debugPrint("Ambient sound unload"); this.cleanup(); Messages.unsubscribe(entity); Messages.messageReceived.disconnect(this, "_onMessageReceived"); }; this.cleanup = function() { if (checkTimer) { Script.clearInterval(checkTimer); checkTimer = false; } if (soundPlaying && soundPlaying.playing) { soundPlaying.stop(); soundPlaying = false; } }; })