// 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 = Script.getExternalPath(Script.ExternalPaths.HF_Content, "/ken/samples/forest_ambiX.wav"); var DEFAULT_VOLUME = 1.0; var DEFAULT_USERDATA = { soundURL: DEFAULT_URL, range: DEFAULT_RANGE, maxVolume: DEFAULT_VOLUME, disabled: true, grab: { triggerable: true } }; 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 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"); this.cleanup(); return; } if (data.soundURL && !(soundURL === data.soundURL)) { soundURL = data.soundURL; debugPrint("Read ambient sound URL"); } 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" in data && !(disabled === data.disabled)) { disabled = data.disabled; debugPrint("Read ambient disabled state: " + disabled); this._updateColor(disabled); } } 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"); } } resource.stateChanged.connect(onStateChanged); onStateChanged(resource.state); } if (soundPlaying && soundPlaying.playing) { debugPrint("URL changed, stopping current ambient sound"); soundPlaying.stop(); soundPlaying = false; } } } this.clickDownOnEntity = function(entityID, mouseEvent) { if (mouseEvent.isPrimaryButton) { this._toggle("primary click"); } }; this.startFarTrigger = function() { this._toggle("far click"); }; this._toggle = function(hint) { // Toggle between ON/OFF state, but only if not in edit mode if (Settings.getValue("io.highfidelity.isEditing")) { return; } var props = Entities.getEntityProperties(entity, [ "userData" ]); if (!props.userData) { debugPrint("userData is empty; ignoring " + hint); this.cleanup(); return; } var data = JSON.parse(props.userData); data.disabled = !data.disabled; debugPrint(hint + " -- triggering ambient sound " + (data.disabled ? "OFF" : "ON")); this.cleanup(); // Save the userData and notify nearby listeners of the change Entities.editEntity(entity, { userData: JSON.stringify(data) }); Messages.sendMessage(entity, "toggled"); }; this._updateColor = function(disabled) { // Update Shape or Text Entity color based on ON/OFF status var props = Entities.getEntityProperties(entity, [ "color", "textColor" ]); var targetColor = disabled ? COLOR_OFF : COLOR_ON; var currentColor = props.textColor || props.color; var newProps = props.textColor ? { textColor: targetColor } : { color: targetColor }; if (currentColor.red !== targetColor.red || currentColor.green !== targetColor.green || currentColor.blue !== targetColor.blue) { Entities.editEntity(entity, newProps); } }; 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); entity = entityID; _this = this; var props = Entities.getEntityProperties(entity, [ "userData", "grab.triggerable" ]); 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) }); } if (!props.grab.triggerable) { Entities.editEntity(entity, { grab: { triggerable: true } }); } this._updateColor(data.disabled); 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; if (!soundPlaying && ambientSound && ambientSound.downloaded) { debugPrint("Starting ambient sound: (duration: " + ambientSound.duration + ")"); soundPlaying = Audio.playSound(ambientSound, soundOptions); } else if (soundPlaying && soundPlaying.playing) { soundPlaying.setOptions(soundOptions); } } else if (soundPlaying && soundPlaying.playing && (distance > range * HYSTERESIS_FRACTION)) { soundPlaying.stop(); soundPlaying = false; debugPrint("Out of range, stopping ambient sound"); } }; 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; } };})
// 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 = Script.getExternalPath(Script.ExternalPaths.HF_Content, "/ken/samples/forest_ambiX.wav");
var DEFAULT_VOLUME = 1.0;
var DEFAULT_USERDATA = {
soundURL: DEFAULT_URL,
range: DEFAULT_RANGE,
maxVolume: DEFAULT_VOLUME,
disabled: true,
grab: { triggerable: true }
};
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 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");
this.cleanup();
return;
}
if (data.soundURL && !(soundURL === data.soundURL)) {
soundURL = data.soundURL;
debugPrint("Read ambient sound URL");
}
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" in data && !(disabled === data.disabled)) {
disabled = data.disabled;
debugPrint("Read ambient disabled state: " + disabled);
this._updateColor(disabled);
}
}
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");
}
}
resource.stateChanged.connect(onStateChanged);
onStateChanged(resource.state);
}
if (soundPlaying && soundPlaying.playing) {
debugPrint("URL changed, stopping current ambient sound");
soundPlaying.stop();
soundPlaying = false;
}
}
}
this.clickDownOnEntity = function(entityID, mouseEvent) {
if (mouseEvent.isPrimaryButton) {
this._toggle("primary click");
}
};
this.startFarTrigger = function() {
this._toggle("far click");
};
this._toggle = function(hint) {
// Toggle between ON/OFF state, but only if not in edit mode
if (Settings.getValue("io.highfidelity.isEditing")) {
return;
}
var props = Entities.getEntityProperties(entity, [ "userData" ]);
if (!props.userData) {
debugPrint("userData is empty; ignoring " + hint);
this.cleanup();
return;
}
var data = JSON.parse(props.userData);
data.disabled = !data.disabled;
debugPrint(hint + " -- triggering ambient sound " + (data.disabled ? "OFF" : "ON"));
this.cleanup();
// Save the userData and notify nearby listeners of the change
Entities.editEntity(entity, {
userData: JSON.stringify(data)
});
Messages.sendMessage(entity, "toggled");
};
this._updateColor = function(disabled) {
// Update Shape or Text Entity color based on ON/OFF status
var props = Entities.getEntityProperties(entity, [ "color", "textColor" ]);
var targetColor = disabled ? COLOR_OFF : COLOR_ON;
var currentColor = props.textColor || props.color;
var newProps = props.textColor ? { textColor: targetColor } : { color: targetColor };
if (currentColor.red !== targetColor.red ||
currentColor.green !== targetColor.green ||
currentColor.blue !== targetColor.blue) {
Entities.editEntity(entity, newProps);
}
};
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);
entity = entityID;
_this = this;
var props = Entities.getEntityProperties(entity, [ "userData", "grab.triggerable" ]);
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) });
}
if (!props.grab.triggerable) {
Entities.editEntity(entity, { grab: { triggerable: true } });
}
this._updateColor(data.disabled);
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;
if (!soundPlaying && ambientSound && ambientSound.downloaded) {
debugPrint("Starting ambient sound: (duration: " + ambientSound.duration + ")");
soundPlaying = Audio.playSound(ambientSound, soundOptions);
} else if (soundPlaying && soundPlaying.playing) {
soundPlaying.setOptions(soundOptions);
}
} else if (soundPlaying && soundPlaying.playing && (distance > range * HYSTERESIS_FRACTION)) {
soundPlaying.stop();
soundPlaying = false;
debugPrint("Out of range, stopping ambient sound");
}
};
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;
}
};
})