From 878774b5d3b5d9f4ea70ef184a4d6119d6bd3c32 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Sun, 24 Mar 2024 13:31:16 -0700 Subject: [PATCH] sound entities --- assignment-client/src/audio/AudioMixer.cpp | 1 + assignment-client/src/avatars/AvatarMixer.cpp | 1 + .../src/entities/EntityServer.cpp | 8 + .../src/scripts/EntityScriptServer.cpp | 1 + libraries/audio/src/AudioInjector.cpp | 3 + libraries/entities/CMakeLists.txt | 2 +- .../entities/src/EntityItemProperties.cpp | 139 +++++++ libraries/entities/src/EntityItemProperties.h | 13 + libraries/entities/src/EntityPropertyFlags.h | 10 + .../entities/src/EntityScriptingInterface.cpp | 26 ++ .../entities/src/EntityScriptingInterface.h | 21 + libraries/entities/src/EntityTree.h | 4 + libraries/entities/src/EntityTreeElement.cpp | 8 +- libraries/entities/src/EntityTypes.cpp | 2 + libraries/entities/src/EntityTypes.h | 3 + libraries/entities/src/SoundEntityItem.cpp | 371 ++++++++++++++++++ libraries/entities/src/SoundEntityItem.h | 100 +++++ libraries/networking/src/udt/PacketHeaders.h | 1 + libraries/physics/CMakeLists.txt | 1 + scripts/system/assets/images/tools/sound.svg | 258 ++++++++++++ .../create/assets/data/createAppTooltips.json | 24 ++ .../create/assets/images/icon-sound.svg | 84 ++++ scripts/system/create/edit.js | 19 +- .../system/create/entityList/entityList.js | 4 +- .../create/entityList/html/js/entityList.js | 1 + .../html/js/entityProperties.js | 57 +++ .../entityProperties/html/tabs/sound.png | Bin 0 -> 684 bytes .../entitySelectionTool.js | 2 +- .../system/create/modules/brokenURLReport.js | 11 + scripts/system/create/qml/EditTabView.qml | 12 + .../system/create/qml/EditToolsTabView.qml | 12 + scripts/system/create/qml/icons/sound.svg | 59 +++ scripts/system/html/js/includes.js | 1 + 33 files changed, 1249 insertions(+), 10 deletions(-) create mode 100644 libraries/entities/src/SoundEntityItem.cpp create mode 100644 libraries/entities/src/SoundEntityItem.h create mode 100644 scripts/system/assets/images/tools/sound.svg create mode 100644 scripts/system/create/assets/images/icon-sound.svg create mode 100644 scripts/system/create/entityProperties/html/tabs/sound.png create mode 100644 scripts/system/create/qml/icons/sound.svg diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 92719abef6..83fe2cc08f 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -816,6 +816,7 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) { void AudioMixer::setupEntityQuery() { _entityViewer.init(); EntityTreePointer entityTree = _entityViewer.getTree(); + entityTree->setIsServer(true); DependencyManager::registerInheritance(); DependencyManager::set(entityTree); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 8d322c36f2..81a9b89b0a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -1081,6 +1081,7 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); EntityTreePointer entityTree = _entityViewer.getTree(); + entityTree->setIsServer(true); DependencyManager::registerInheritance(); DependencyManager::set(entityTree); diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index d27a69ff7c..9cbaaa0ea1 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -16,9 +16,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -49,6 +51,9 @@ EntityServer::EntityServer(ReceivedMessage& message) : DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache ctor DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityClone, @@ -71,6 +76,8 @@ EntityServer::~EntityServer() { void EntityServer::aboutToFinish() { DependencyManager::get()->cleanup(); + DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); OctreeServer::aboutToFinish(); @@ -90,6 +97,7 @@ OctreePointer EntityServer::createTree() { EntityTreePointer tree = std::make_shared(true); tree->createRootElement(); tree->addNewlyCreatedHook(this); + tree->setIsEntityServer(true); if (!_entitySimulation) { SimpleEntitySimulationPointer simpleSimulation { new SimpleEntitySimulation() }; simpleSimulation->setEntityTree(tree); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index b16e4561d6..3f49ecd889 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -313,6 +313,7 @@ void EntityScriptServer::run() { entityScriptingInterface->setEntityTree(_entityViewer.getTree()); auto treePtr = _entityViewer.getTree(); + treePtr->setIsServer(true); DependencyManager::set(treePtr); if (!_entitySimulation) { diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 2df766377f..8c3a6b118e 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -132,9 +132,11 @@ void AudioInjector::restart() { bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { AudioInjectorOptions options; + uint32_t numBytes; withWriteLock([&] { _state = AudioInjectorState::NotFinished; options = _options; + numBytes = _audioData->getNumBytes(); }); int byteOffset = 0; @@ -142,6 +144,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInj int numChannels = options.ambisonic ? 4 : (options.stereo ? 2 : 1); byteOffset = (int)(AudioConstants::SAMPLE_RATE * options.secondOffset * numChannels); byteOffset *= AudioConstants::SAMPLE_SIZE; + byteOffset = byteOffset % numBytes; } _currentSendOffset = byteOffset; diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index e04d9f9fa8..830cecd1ed 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -12,7 +12,7 @@ include_hifi_library_headers(image) include_hifi_library_headers(ktx) include_hifi_library_headers(material-networking) include_hifi_library_headers(procedural) -link_hifi_libraries(shared shaders networking octree avatars graphics model-networking script-engine) +link_hifi_libraries(audio shared shaders networking octree avatars graphics model-networking script-engine) if (WIN32) add_compile_definitions(_USE_MATH_DEFINES) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5db64d12e4..a32dcf78c0 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -690,6 +690,16 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_GIZMO_TYPE, gizmoType); changedProperties += _ring.getChangedProperties(); + // Sound + CHECK_PROPERTY_CHANGE(PROP_SOUND_URL, soundURL); + CHECK_PROPERTY_CHANGE(PROP_SOUND_VOLUME, volume); + CHECK_PROPERTY_CHANGE(PROP_SOUND_TIME_OFFSET, timeOffset); + CHECK_PROPERTY_CHANGE(PROP_SOUND_PITCH, pitch); + CHECK_PROPERTY_CHANGE(PROP_SOUND_PLAYING, playing); + CHECK_PROPERTY_CHANGE(PROP_SOUND_LOOP, loop); + CHECK_PROPERTY_CHANGE(PROP_SOUND_POSITIONAL, positional); + CHECK_PROPERTY_CHANGE(PROP_SOUND_LOCAL_ONLY, localOnly); + return changedProperties; } @@ -883,6 +893,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @see {@link Entities.EntityProperties-PolyLine|EntityProperties-PolyLine} * @see {@link Entities.EntityProperties-PolyVox|EntityProperties-PolyVox} * @see {@link Entities.EntityProperties-Shape|EntityProperties-Shape} + * @see {@link Entities.EntityProperties-Sound|EntityProperties-Sound} * @see {@link Entities.EntityProperties-Sphere|EntityProperties-Sphere} * @see {@link Entities.EntityProperties-Text|EntityProperties-Text} * @see {@link Entities.EntityProperties-Web|EntityProperties-Web} @@ -1340,6 +1351,34 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * }); */ +/*@jsdoc + * The "Sound" {@link Entities.EntityType|EntityType} plays a sound from a URL. It has properties in addition to + * the common {@link Entities.EntityProperties|EntityProperties}. + * + * @typedef {object} Entities.EntityProperties-Sound + * @property {string} soundURL="" - The URL of the sound to play, as a wav, mp3, or raw file. Supports stereo and ambisonic. + * @property {boolean} playing=true - Whether or not the sound should play. + * @property {number} volume=1.0 - The volume of the sound, from 0 to 1. + * @property {number} pitch=1.0 - The relative sample rate at which to resample the sound, within +/- 2 octaves. + * @property {number} timeOffset=0.0 - The time (in seconds) at which to start playback within the sound file. If looping, + * this only affects the first loop. + * @property {boolean} loop=true - Whether or not to loop the sound. + * @property {boolean} positional=true - Whether or not the volume of the sound should decay with distance. + * @property {boolean} localOnly=false - Whether or not the sound should play locally for everyone (unsynced), or synchronously + * for everyone via the Entity Mixer. + * @example Create a Sound entity. + * var entity = Entities.addEntity({ + * type: "Sound", + * soundURL: "https://themushroomkingdom.net/sounds/wav/lm/lm_gold_mouse.wav", + * positional: true, + * volume: 0.75, + * localOnly: true, + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.75, z: -4 })), + * rotation: MyAvatar.orientation, + * lifetime: 300 // Delete after 5 minutes. + * }); + */ + /*@jsdoc * The "Sphere" {@link Entities.EntityType|EntityType} is the same as the "Shape" * {@link Entities.EntityType|EntityType} except that its shape value is always set to "Sphere" @@ -1961,6 +2000,18 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s _ring.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); } + // Sound only + if (_type == EntityTypes::Sound) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_URL, soundURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_VOLUME, volume); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_TIME_OFFSET, timeOffset); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_PITCH, pitch); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_PLAYING, playing); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_LOOP, loop); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_POSITIONAL, positional); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOUND_LOCAL_ONLY, localOnly); + } + /*@jsdoc * The axis-aligned bounding box of an entity. * @typedef {object} Entities.BoundingBox @@ -2292,6 +2343,16 @@ void EntityItemProperties::copyFromScriptValue(const ScriptValue& object, bool h COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(gizmoType, GizmoType); _ring.copyFromScriptValue(object, namesSet, _defaultSettings); + // Sound + COPY_PROPERTY_FROM_QSCRIPTVALUE(soundURL, QString, setSoundURL); + COPY_PROPERTY_FROM_QSCRIPTVALUE(volume, float, setVolume); + COPY_PROPERTY_FROM_QSCRIPTVALUE(timeOffset, float, setTimeOffset); + COPY_PROPERTY_FROM_QSCRIPTVALUE(pitch, float, setPitch); + COPY_PROPERTY_FROM_QSCRIPTVALUE(playing, bool, setPlaying); + COPY_PROPERTY_FROM_QSCRIPTVALUE(loop, bool, setLoop); + COPY_PROPERTY_FROM_QSCRIPTVALUE(positional, bool, setPositional); + COPY_PROPERTY_FROM_QSCRIPTVALUE(localOnly, bool, setLocalOnly); + // Handle conversions from old 'textures' property to "imageURL" if (namesSet.contains("textures")) { ScriptValue V = object.property("textures"); @@ -2578,6 +2639,16 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(gizmoType); _ring.merge(other._ring); + // Sound + COPY_PROPERTY_IF_CHANGED(soundURL); + COPY_PROPERTY_IF_CHANGED(volume); + COPY_PROPERTY_IF_CHANGED(timeOffset); + COPY_PROPERTY_IF_CHANGED(pitch); + COPY_PROPERTY_IF_CHANGED(playing); + COPY_PROPERTY_IF_CHANGED(loop); + COPY_PROPERTY_IF_CHANGED(positional); + COPY_PROPERTY_IF_CHANGED(localOnly); + _lastEdited = usecTimestampNow(); } @@ -3002,6 +3073,16 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ADD_GROUP_PROPERTY_TO_MAP(PROP_MAJOR_TICK_MARKS_COLOR, Ring, ring, MajorTickMarksColor, majorTickMarksColor); ADD_GROUP_PROPERTY_TO_MAP(PROP_MINOR_TICK_MARKS_COLOR, Ring, ring, MinorTickMarksColor, minorTickMarksColor); } + + // Sound + ADD_PROPERTY_TO_MAP(PROP_SOUND_URL, SoundURL, soundURL, QString); + ADD_PROPERTY_TO_MAP_WITH_RANGE(PROP_SOUND_VOLUME, Volume, volume, float, 0.0f, 1.0f); + ADD_PROPERTY_TO_MAP(PROP_SOUND_TIME_OFFSET, TimeOffset, timeOffset, float); + ADD_PROPERTY_TO_MAP_WITH_RANGE(PROP_SOUND_PITCH, Pitch, pitch, float, 1.0f / 16.0f, 16.0f); + ADD_PROPERTY_TO_MAP(PROP_SOUND_PLAYING, Playing, playing, bool); + ADD_PROPERTY_TO_MAP(PROP_SOUND_LOOP, Loop, loop, bool); + ADD_PROPERTY_TO_MAP(PROP_SOUND_POSITIONAL, Positional, positional, bool); + ADD_PROPERTY_TO_MAP(PROP_SOUND_LOCAL_ONLY, LocalOnly, localOnly, bool); }); auto iter = _propertyInfos.find(propertyName); @@ -3449,6 +3530,17 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy _staticRing.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); } + + if (properties.getType() == EntityTypes::Sound) { + APPEND_ENTITY_PROPERTY(PROP_SOUND_URL, properties.getSoundURL()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_VOLUME, properties.getVolume()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_TIME_OFFSET, properties.getTimeOffset()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_PITCH, properties.getPitch()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_PLAYING, properties.getPlaying()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_LOOP, properties.getLoop()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_POSITIONAL, properties.getPositional()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_LOCAL_ONLY, properties.getLocalOnly()); + } } if (propertyCount > 0) { @@ -3911,6 +4003,17 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int properties.getRing().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); } + if (properties.getType() == EntityTypes::Sound) { + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_URL, QString, setSoundURL); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_VOLUME, float, setVolume); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_TIME_OFFSET, float, setTimeOffset); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_PITCH, float, setPitch); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_PLAYING, bool, setPlaying); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_LOOP, bool, setLoop); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_POSITIONAL, bool, setPositional); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOUND_LOCAL_ONLY, bool, setLocalOnly); + } + return valid; } @@ -4240,6 +4343,16 @@ void EntityItemProperties::markAllChanged() { // Gizmo _gizmoTypeChanged = true; _ring.markAllChanged(); + + // Sound + _soundURLChanged = true; + _volumeChanged = true; + _timeOffsetChanged = true; + _pitchChanged = true; + _playingChanged = true; + _loopChanged = true; + _positionalChanged = true; + _localOnlyChanged = true; } // The minimum bounding box for the entity. @@ -4945,6 +5058,32 @@ QList EntityItemProperties::listChangedProperties() { } getRing().listChangedProperties(out); + // Sound + if (soundURLChanged()) { + out += "soundURL"; + } + if (volumeChanged()) { + out += "volume"; + } + if (timeOffsetChanged()) { + out += "timeOffset"; + } + if (pitchChanged()) { + out += "pitch"; + } + if (playingChanged()) { + out += "playing"; + } + if (loopChanged()) { + out += "loop"; + } + if (positionalChanged()) { + out += "positional"; + } + if (localOnlyChanged()) { + out += "localOnly"; + } + return out; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 6340313f2d..961049b1c7 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -52,6 +52,7 @@ #include "PolyVoxEntityItem.h" #include "GridEntityItem.h" #include "GizmoEntityItem.h" +#include "SoundEntityItem.h" #include "LightEntityItem.h" #include "ZoneEntityItem.h" @@ -124,6 +125,8 @@ class EntityItemProperties { friend class LightEntityItem; friend class ZoneEntityItem; friend class MaterialEntityItem; + friend class SoundEntityItem; + public: static bool blobToProperties(ScriptEngine& scriptEngine, const QByteArray& blob, EntityItemProperties& properties); static void propertiesToBlob(ScriptEngine& scriptEngine, const QUuid& myAvatarID, const EntityItemProperties& properties, @@ -407,6 +410,16 @@ public: DEFINE_PROPERTY_REF_ENUM(PROP_GIZMO_TYPE, GizmoType, gizmoType, GizmoType, GizmoType::RING); DEFINE_PROPERTY_GROUP(Ring, ring, RingGizmoPropertyGroup); + // Sound + DEFINE_PROPERTY_REF(PROP_SOUND_URL, SoundURL, soundURL, QString, ""); + DEFINE_PROPERTY(PROP_SOUND_VOLUME, Volume, volume, float, 1.0f); + DEFINE_PROPERTY(PROP_SOUND_TIME_OFFSET, TimeOffset, timeOffset, float, 0.0f); + DEFINE_PROPERTY(PROP_SOUND_PITCH, Pitch, pitch, float, 1.0f); + DEFINE_PROPERTY(PROP_SOUND_PLAYING, Playing, playing, bool, true); + DEFINE_PROPERTY(PROP_SOUND_LOOP, Loop, loop, bool, true); + DEFINE_PROPERTY(PROP_SOUND_POSITIONAL, Positional, positional, bool, true); + DEFINE_PROPERTY(PROP_SOUND_LOCAL_ONLY, LocalOnly, localOnly, bool, false); + static QString getComponentModeAsString(uint32_t mode); public: diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index ca96c04fd8..591725c2ea 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -394,6 +394,16 @@ enum EntityPropertyList { PROP_MAJOR_TICK_MARKS_COLOR = PROP_DERIVED_17, PROP_MINOR_TICK_MARKS_COLOR = PROP_DERIVED_18, + // Sound + PROP_SOUND_URL = PROP_DERIVED_0, + PROP_SOUND_VOLUME = PROP_DERIVED_1, + PROP_SOUND_TIME_OFFSET = PROP_DERIVED_2, + PROP_SOUND_PITCH = PROP_DERIVED_3, + PROP_SOUND_PLAYING = PROP_DERIVED_4, + PROP_SOUND_POSITIONAL = PROP_DERIVED_5, + PROP_SOUND_LOOP = PROP_DERIVED_6, + PROP_SOUND_LOCAL_ONLY = PROP_DERIVED_7, + // WARNING!!! DO NOT ADD PROPS_xxx here unless you really really meant to.... Add them UP above }; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 4acf9b8d1b..f0da5f3cf3 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1871,6 +1871,7 @@ bool EntityScriptingInterface::setAllPoints(const QUuid& entityID, const QVector EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setPoints no entity with ID" << entityID; + return false; } EntityTypes::EntityType entityType = entity->getType(); @@ -1907,6 +1908,31 @@ bool EntityScriptingInterface::appendPoint(const QUuid& entityID, const glm::vec return false; } +bool EntityScriptingInterface::restartSound(const QUuid& entityID) { + PROFILE_RANGE(script_entities, __FUNCTION__); + + EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); + if (!entity) { + qCDebug(entities) << "EntityScriptingInterface::restartSound no entity with ID" << entityID; + // There is no entity + return false; + } + + EntityTypes::EntityType entityType = entity->getType(); + + if (entityType == EntityTypes::Sound) { + auto soundEntity = std::dynamic_pointer_cast(entity); + bool isPlaying = soundEntity->getPlaying(); + if (isPlaying) { + soundEntity->withWriteLock([&] { + soundEntity->restartSound(); + }); + } + return isPlaying; + } + + return false; +} bool EntityScriptingInterface::actionWorker(const QUuid& entityID, std::function actor) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index c677bdf0a1..59204bdd96 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1237,6 +1237,27 @@ public slots: */ Q_INVOKABLE bool appendPoint(const QUuid& entityID, const glm::vec3& point); + /*@jsdoc + * Restart a {@link Entities.EntityProperties-Sound|Sound} entity, locally only. It must also be localOnly. + * @function Entities.restartSound + * @param {Uuid} entityID - The ID of the {@link Entities.EntityProperties-Sound|Sound} entity. + * @example Play a sound once and repeat it every 3 seconds. + * var position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.5, z: -8 })); + * var sound = Entities.addEntity({ + * type: "Sound", + * position: position, + * soundURL: "https://themushroomkingdom.net/sounds/wav/lm/lm_gold_mouse.wav", + * positional: false, + * localOnly: true, + * loop: false, + * lifetime: 300 // Delete after 5 minutes. + * }); + * Script.setInterval(() => { + * Entities.restartSound(sound); + * }, 3000); + */ + Q_INVOKABLE bool restartSound(const QUuid& entityID); + /*@jsdoc * Dumps debug information about all entities in Interface's local in-memory tree of entities it knows about to the program * log. diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 1161bec6e9..a1f97ff621 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -257,6 +257,9 @@ public: void setIsServerlessMode(bool value) { _serverlessDomain = value; } bool isServerlessMode() const { return _serverlessDomain; } + void setIsEntityServer(bool value) { _entityServer = value; } + bool isEntityServer() const { return _entityServer; } + static void setGetEntityObjectOperator(std::function getEntityObjectOperator) { _getEntityObjectOperator = getEntityObjectOperator; } static QObject* getEntityObject(const QUuid& id); @@ -380,6 +383,7 @@ private: std::vector _staleProxies; bool _serverlessDomain { false }; + bool _entityServer { false }; std::map _namedPaths; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 32c791da33..b6f65cff65 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -257,8 +257,8 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori } } else { // if the entity type doesn't support a detailed intersection, then just return the non-AABox results - // Never intersect with particle entities - if (localDistance < distance && entity->getType() != EntityTypes::ParticleEffect) { + // Never intersect with particle or sound entities + if (localDistance < distance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::Sound)) { distance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 0.0f)); @@ -409,8 +409,8 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3 } } else { // if the entity type doesn't support a detailed intersection, then just return the non-AABox results - // Never intersect with particle entities - if (localDistance < parabolicDistance && entity->getType() != EntityTypes::ParticleEffect) { + // Never intersect with particle or sound entities + if (localDistance < parabolicDistance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::Sound)) { parabolicDistance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 0.0f)); diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index 95057bedbc..49109fd5dd 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -34,6 +34,7 @@ #include "LightEntityItem.h" #include "ZoneEntityItem.h" #include "MaterialEntityItem.h" +#include "SoundEntityItem.h" QMap EntityTypes::_typeToNameMap; QMap EntityTypes::_nameToTypeMap; @@ -59,6 +60,7 @@ REGISTER_ENTITY_TYPE(Gizmo) REGISTER_ENTITY_TYPE(Light) REGISTER_ENTITY_TYPE(Zone) REGISTER_ENTITY_TYPE(Material) +REGISTER_ENTITY_TYPE(Sound) bool EntityTypes::typeIsValid(EntityType type) { return type > EntityType::Unknown && type <= EntityType::NUM_TYPES; diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 441e77fccd..30d59727d4 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -86,6 +86,8 @@ public: * {@link Entities.EntityProperties-Zone|EntityProperties-Zone} * "Material"Modifies the existing materials on entities and avatars. * {@link Entities.EntityProperties-Material|EntityProperties-Material} + * "Sound"Plays a sound. + * {@link Entities.EntityProperties-Material|EntityProperties-Sound} * * * @typedef {string} Entities.EntityType @@ -108,6 +110,7 @@ public: Light, Zone, Material, + Sound, NUM_TYPES } EntityType; diff --git a/libraries/entities/src/SoundEntityItem.cpp b/libraries/entities/src/SoundEntityItem.cpp new file mode 100644 index 0000000000..575efabb2e --- /dev/null +++ b/libraries/entities/src/SoundEntityItem.cpp @@ -0,0 +1,371 @@ +// +// Created by HifiExperiments on 12/30/2023 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "SoundEntityItem.h" + +#include + +#include "EntitiesLogging.h" +#include "EntityItemProperties.h" +#include "EntityTree.h" +#include "EntityTreeElement.h" + +EntityItemPointer SoundEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + std::shared_ptr entity(new SoundEntityItem(entityID), [](SoundEntityItem* ptr) { ptr->deleteLater(); }); + entity->setProperties(properties); + return entity; +} + +// our non-pure virtual subclass for now... +SoundEntityItem::SoundEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { + _type = EntityTypes::Sound; +} + +SoundEntityItem::~SoundEntityItem() { + auto manager = DependencyManager::get(); + if (manager && _injector) { + manager->stop(_injector); + } +} + +EntityItemProperties SoundEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class + + COPY_ENTITY_PROPERTY_TO_PROPERTIES(soundURL, getURL); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(volume, getVolume); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(timeOffset, getTimeOffset); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(pitch, getPitch); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(playing, getPlaying); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(loop, getLoop); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(positional, getPositional); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(localOnly, getLocalOnly); + + return properties; +} + +bool SoundEntityItem::setSubClassProperties(const EntityItemProperties& properties) { + bool somethingChanged = false; + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(soundURL, setURL); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(volume, setVolume); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(timeOffset, setTimeOffset); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(pitch, setPitch); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(playing, setPlaying); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(loop, setLoop); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(positional, setPositional); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(localOnly, setLocalOnly); + + return somethingChanged; +} + +int SoundEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) { + + int bytesRead = 0; + const unsigned char* dataAt = data; + + READ_ENTITY_PROPERTY(PROP_SOUND_URL, QString, setURL); + READ_ENTITY_PROPERTY(PROP_SOUND_VOLUME, float, setVolume); + READ_ENTITY_PROPERTY(PROP_SOUND_TIME_OFFSET, float, setTimeOffset); + READ_ENTITY_PROPERTY(PROP_SOUND_PITCH, float, setPitch); + READ_ENTITY_PROPERTY(PROP_SOUND_PLAYING, bool, setPlaying); + READ_ENTITY_PROPERTY(PROP_SOUND_LOOP, bool, setLoop); + READ_ENTITY_PROPERTY(PROP_SOUND_POSITIONAL, bool, setPositional); + READ_ENTITY_PROPERTY(PROP_SOUND_LOCAL_ONLY, bool, setLocalOnly); + + return bytesRead; +} + +EntityPropertyFlags SoundEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { + EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + + requestedProperties += PROP_SOUND_URL; + requestedProperties += PROP_SOUND_VOLUME; + requestedProperties += PROP_SOUND_TIME_OFFSET; + requestedProperties += PROP_SOUND_PITCH; + requestedProperties += PROP_SOUND_PLAYING; + requestedProperties += PROP_SOUND_LOOP; + requestedProperties += PROP_SOUND_POSITIONAL; + requestedProperties += PROP_SOUND_LOCAL_ONLY; + + return requestedProperties; +} + +void SoundEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const { + + bool successPropertyFits = true; + + APPEND_ENTITY_PROPERTY(PROP_SOUND_URL, getURL()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_VOLUME, getVolume()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_TIME_OFFSET, getTimeOffset()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_PITCH, getPitch()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_PLAYING, getPlaying()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_LOOP, getLoop()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_POSITIONAL, getPositional()); + APPEND_ENTITY_PROPERTY(PROP_SOUND_LOCAL_ONLY, getLocalOnly()); +} + +void SoundEntityItem::debugDump() const { + quint64 now = usecTimestampNow(); + qCDebug(entities) << "SOUND EntityItem id:" << getEntityItemID() << "---------------------------------------------"; + qCDebug(entities) << " name:" << _name; + qCDebug(entities) << " url:" << _url; + qCDebug(entities) << " position:" << debugTreeVector(getWorldPosition()); + qCDebug(entities) << " dimensions:" << debugTreeVector(getScaledDimensions()); + qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); + qCDebug(entities) << "SOUND EntityItem Ptr:" << this; +} + +void SoundEntityItem::update(const quint64& now) { + withWriteLock([&] { + const auto tree = getTree(); + if (tree) { + _updateNeeded = false; + + if ((_localOnly && tree->getIsClient()) || (!_localOnly && (tree->isEntityServer() || tree->isServerlessMode()))) { + _sound = DependencyManager::get()->getSound(_url); + } + + if (_sound) { + if (_sound->isLoaded()) { + updateSound(true); + } else { + connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + } + } + } + }); +} + +void SoundEntityItem::setLocalPosition(const glm::vec3& value, bool tellPhysics) { + EntityItem::setLocalPosition(value, tellPhysics); + withWriteLock([&] { + updateSound(); + }); +} + +void SoundEntityItem::setLocalOrientation(const glm::quat& value) { + EntityItem::setLocalOrientation(value); + withWriteLock([&] { + updateSound(); + }); +} + +void SoundEntityItem::setURL(const QString& value) { + withWriteLock([&] { + if (value != _url) { + _url = value; + + const auto tree = getTree(); + if (!tree) { + _updateNeeded = true; + return; + } + + if ((_localOnly && tree->getIsClient()) || (!_localOnly && (tree->isEntityServer() || tree->isServerlessMode()))) { + _sound = DependencyManager::get()->getSound(_url); + } + + if (_sound) { + if (_sound->isLoaded()) { + updateSound(true); + } else { + connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + } + } + } + }); +} + +QString SoundEntityItem::getURL() const { + return resultWithReadLock([&] { + return _url; + }); +} + +void SoundEntityItem::setVolume(float value) { + withWriteLock([&] { + if (value != _volume) { + _volume = value; + updateSound(); + } + }); +} + +float SoundEntityItem::getVolume() const { + return resultWithReadLock([&] { + return _volume; + }); +} + +void SoundEntityItem::setTimeOffset(float value) { + withWriteLock([&] { + if (value != _timeOffset) { + _timeOffset = value; + updateSound(true); + } + }); +} + +float SoundEntityItem::getTimeOffset() const { + return resultWithReadLock([&] { + return _timeOffset; + }); +} + +void SoundEntityItem::setPitch(float value) { + withWriteLock([&] { + if (value != _pitch) { + _pitch = value; + updateSound(true); + } + }); +} + +float SoundEntityItem::getPitch() const { + return resultWithReadLock([&] { + return _pitch; + }); +} + +void SoundEntityItem::setPlaying(bool value) { + withWriteLock([&] { + if (value != _playing) { + _playing = value; + updateSound(); + } + }); +} + +bool SoundEntityItem::getPlaying() const { + return resultWithReadLock([&] { + return _playing; + }); +} + +void SoundEntityItem::setLoop(bool value) { + withWriteLock([&] { + if (value != _loop) { + _loop = value; + updateSound(true); + } + }); +} + +bool SoundEntityItem::getLoop() const { + return resultWithReadLock([&] { + return _loop; + }); +} + +void SoundEntityItem::setPositional(bool value) { + withWriteLock([&] { + if (value != _positional) { + _positional = value; + updateSound(); + } + }); +} + +bool SoundEntityItem::getPositional() const { + return resultWithReadLock([&] { + return _positional; + }); +} + +void SoundEntityItem::setLocalOnly(bool value) { + withWriteLock([&] { + if (value != _localOnly) { + _localOnly = value; + + const auto tree = getTree(); + if (!tree) { + _updateNeeded = true; + return; + } + + if ((_localOnly && tree->getIsClient()) || (!_localOnly && (tree->isEntityServer() || tree->isServerlessMode()))) { + _sound = DependencyManager::get()->getSound(_url); + } else { + _sound = nullptr; + + if (_injector) { + DependencyManager::get()->stop(_injector); + } + _injector = nullptr; + } + + if (_sound) { + if (_sound->isLoaded()) { + updateSound(true); + } else { + connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + } + } + } + }); +} + +bool SoundEntityItem::getLocalOnly() const { + return resultWithReadLock([&] { + return _localOnly; + }); +} + +bool SoundEntityItem::restartSound() { + if (!_sound) { + return false; + } + + AudioInjectorOptions options; + options.position = getWorldPosition(); + options.positionSet = _positional; + options.volume = _volume; + options.loop = _loop; + options.orientation = getWorldOrientation(); + options.localOnly = _localOnly; + options.secondOffset = _timeOffset; + options.pitch = _pitch; + + if (_injector) { + DependencyManager::get()->setOptionsAndRestart(_injector, options); + } else { + _injector = DependencyManager::get()->playSound(_sound, options); + } + + return true; +} + +void SoundEntityItem::updateSound(bool restart) { + if (!_sound) { + return; + } + + if (restart) { + if (_injector) { + DependencyManager::get()->stop(_injector); + } + _injector = nullptr; + } + + if (_playing) { + restartSound(); + } else { + if (_injector) { + DependencyManager::get()->stop(_injector); + } + } +} \ No newline at end of file diff --git a/libraries/entities/src/SoundEntityItem.h b/libraries/entities/src/SoundEntityItem.h new file mode 100644 index 0000000000..781c270d45 --- /dev/null +++ b/libraries/entities/src/SoundEntityItem.h @@ -0,0 +1,100 @@ +// +// Created by HifiExperiments on 12/30/2023 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SoundEntityItem_h +#define hifi_SoundEntityItem_h + +#include "EntityItem.h" + +#include +#include + +class SoundEntityItem : public EntityItem { +public: + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); + + SoundEntityItem(const EntityItemID& entityItemID); + ~SoundEntityItem(); + + ALLOW_INSTANTIATION // This class can be instantiated + + // methods for getting/setting all properties of an entity + EntityItemProperties getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const override; + bool setSubClassProperties(const EntityItemProperties& properties) override; + + EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; + + void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const override; + + int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) override; + + bool shouldBePhysical() const override { return false; } + + virtual void debugDump() const override; + + virtual bool supportsDetailedIntersection() const override { return false; } + + virtual void update(const quint64& now) override; + bool needsToCallUpdate() const override { return _updateNeeded; } + + void setLocalPosition(const glm::vec3& value, bool tellPhysics = true) override; + void setLocalOrientation(const glm::quat& value) override; + + void setURL(const QString& value); + QString getURL() const; + + void setVolume(float value); + float getVolume() const; + + void setTimeOffset(float value); + float getTimeOffset() const; + + void setPitch(float value); + float getPitch() const; + + void setPlaying(bool value); + bool getPlaying() const; + + void setLoop(bool value); + bool getLoop() const; + + void setPositional(bool value); + bool getPositional() const; + + void setLocalOnly(bool value); + bool getLocalOnly() const; + + bool restartSound(); + +protected: + void updateSound(bool restart = false); + + QString _url { "" }; + float _volume { 1.0f }; + float _timeOffset { 0.0f }; + float _pitch { 1.0f }; + bool _playing { true }; + bool _loop { true }; + bool _positional { true }; + bool _localOnly { false }; + + SharedSoundPointer _sound; + AudioInjectorPointer _injector; + bool _updateNeeded { false }; +}; + +#endif // hifi_SoundEntityItem_h diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 6f84f25e86..46e2fe61c7 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -294,6 +294,7 @@ enum class EntityVersion : PacketVersion { EntityTags, WantsKeyboardFocus, AudioZones, + SoundEntities, // Add new versions above here NUM_PACKET_TYPE, diff --git a/libraries/physics/CMakeLists.txt b/libraries/physics/CMakeLists.txt index 3237e712f9..ad6d5292b9 100644 --- a/libraries/physics/CMakeLists.txt +++ b/libraries/physics/CMakeLists.txt @@ -5,6 +5,7 @@ set(TARGET_NAME physics) setup_hifi_library() link_hifi_libraries(shared workload entities shaders) +include_hifi_library_headers(audio) include_hifi_library_headers(networking) include_hifi_library_headers(gpu) include_hifi_library_headers(avatars) diff --git a/scripts/system/assets/images/tools/sound.svg b/scripts/system/assets/images/tools/sound.svg new file mode 100644 index 0000000000..a71b449045 --- /dev/null +++ b/scripts/system/assets/images/tools/sound.svg @@ -0,0 +1,258 @@ + + + +SOUNDSOUNDSOUND + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/create/assets/data/createAppTooltips.json b/scripts/system/create/assets/data/createAppTooltips.json index b4597e9141..efb33fd422 100644 --- a/scripts/system/create/assets/data/createAppTooltips.json +++ b/scripts/system/create/assets/data/createAppTooltips.json @@ -706,5 +706,29 @@ }, "zTextureURL": { "tooltip": "The URL of the texture to map to surfaces perpendicular to the entity's local z-axis. JPG or PNG format." + }, + "soundURL": { + "tooltip": "The URL of the sound, as a wav, mp3, or raw file." + }, + "playing": { + "tooltip": "Whether or not the sound should play." + }, + "volume": { + "tooltip": "The volume of the sound." + }, + "pitch": { + "tooltip": "Alter the pitch of the sound, within +/- 2 octaves. The value is the relative sample rate at which to resample the sound." + }, + "timeOffset": { + "tooltip": "Starts playback from a specified time (seconds) within the sound file." + }, + "loop": { + "tooltip": "Whether or not the sound is played repeatedly." + }, + "positional": { + "tooltip": "Whether or not the sound volume drops off with distance." + }, + "localOnly": { + "tooltip": "Whether the sound should play locally for everyone separately, or globally via the audio mixer." } } diff --git a/scripts/system/create/assets/images/icon-sound.svg b/scripts/system/create/assets/images/icon-sound.svg new file mode 100644 index 0000000000..cf89956c41 --- /dev/null +++ b/scripts/system/create/assets/images/icon-sound.svg @@ -0,0 +1,84 @@ + +image/svg+xml diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index dabdfe34f9..ade2133803 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -51,6 +51,7 @@ var DEFAULT_IMAGE = Script.getExternalPath(Script.ExternalPaths.Assets, "Bazaar/Assets/Textures/Defaults/Interface/default_image.jpg"); var DEFAULT_PARTICLE = Script.getExternalPath(Script.ExternalPaths.Assets, "Bazaar/Assets/Textures/Defaults/Interface/default_particle.png"); + var DEFAULT_SOUND = "TODO"; var createToolsWindow = new CreateWindow( Script.resolvePath("qml/EditTools.qml"), @@ -94,8 +95,9 @@ var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); var ZONE_URL = Script.resolvePath("assets/images/icon-zone.svg"); var MATERIAL_URL = Script.resolvePath("assets/images/icon-material.svg"); + var SOUND_URL = Script.resolvePath("assets/images/icon-sound.svg"); - var entityIconOverlayManager = new EntityIconOverlayManager(["Light", "ParticleEffect", "Zone", "Material"], function(entityID) { + var entityIconOverlayManager = new EntityIconOverlayManager(["Light", "ParticleEffect", "Zone", "Material", "Sound"], function(entityID) { var properties = Entities.getEntityProperties(entityID, ["type", "isSpotlight", "parentID", "name"]); if (properties.type === "Light") { return { @@ -109,6 +111,8 @@ } else { return { imageURL: "" }; } + } else if (properties.type === "Sound") { + return { imageURL: SOUND_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 0) }; } else { return { imageURL: PARTICLE_SYSTEM_URL }; } @@ -492,6 +496,9 @@ exponent: 1.0, cutoff: 75.0, }, + Sound: { + soundURL: DEFAULT_SOUND + }, }; var toolBar = (function () { @@ -570,7 +577,7 @@ if (!properties.grab) { properties.grab = {}; if (Menu.isOptionChecked(MENU_CREATE_ENTITIES_GRABBABLE) && - !(properties.type === "Zone" || properties.type === "Light" + !(properties.type === "Zone" || properties.type === "Light" || properties.type === "Sound" || properties.type === "ParticleEffect" || properties.type === "Web")) { properties.grab.grabbable = true; } else { @@ -1056,6 +1063,12 @@ addButton("newPolyVoxButton", createNewEntityDialogButtonCallback("PolyVox")); + addButton("newSoundButton", function () { + createNewEntity({ + type: "Sound", + }); + }); + var deactivateCreateIfDesktopWindowsHidden = function() { if (!shouldUseEditTabletApp() && !entityListTool.isVisible() && !createToolsWindow.isVisible()) { that.setActive(false); @@ -2041,7 +2054,7 @@ var entityParentIDs = []; var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; - var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; + var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "Sound", "ParticleEffect"]; if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index 257f967852..17dd942e13 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -211,7 +211,7 @@ var EntityListTool = function(shouldUseEditTabletApp, selectionManager) { PROFILE("getMultipleProperties", function () { var multipleProperties = Entities.getMultipleEntityProperties(ids, ['position', 'name', 'type', 'locked', 'visible', 'renderInfo', 'modelURL', 'materialURL', 'imageURL', 'script', 'serverScripts', - 'skybox.url', 'ambientLight.url', 'created', 'lastEdited']); + 'skybox.url', 'ambientLight.url', 'soundURL', 'created', 'lastEdited']); for (var i = 0; i < multipleProperties.length; i++) { var properties = multipleProperties[i]; @@ -223,6 +223,8 @@ var EntityListTool = function(shouldUseEditTabletApp, selectionManager) { url = properties.materialURL; } else if (properties.type === "Image") { url = properties.imageURL; + } else if (properties.type === "Sound") { + url = properties.soundURL; } //print("Global object before getParentState call: " + JSON.stringify(globalThis)); var parentStatus = that.createApp.getParentState(ids[i]); diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index a64d95a64c..213e4ad09c 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -178,6 +178,7 @@ const FILTER_TYPES = [ "PolyVox", "Text", "Grid", + "Sound", ]; const DOUBLE_CLICK_TIMEOUT = 300; // ms diff --git a/scripts/system/create/entityProperties/html/js/entityProperties.js b/scripts/system/create/entityProperties/html/js/entityProperties.js index 8a1540d411..797156081e 100644 --- a/scripts/system/create/entityProperties/html/js/entityProperties.js +++ b/scripts/system/create/entityProperties/html/js/entityProperties.js @@ -1432,6 +1432,62 @@ const GROUPS = [ }, ] }, + { + id: "sound", + label: "SOUND", + properties: [ + { + label: "Sound", + type: "string", + propertyID: "soundURL", + placeholder: "URL", + }, + { + label: "Playing", + type: "bool", + propertyID: "playing", + }, + { + label: "Loop", + type: "bool", + propertyID: "loop", + }, + { + label: "Volume", + type: "number-draggable", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "volume", + }, + { + label: "Positional", + type: "bool", + propertyID: "positional", + }, + { + label: "Pitch", + type: "number-draggable", + min: 0.0625, + max: 16, + step: 0.1, + decimals: 2, + propertyID: "pitch", + }, + { + label: "Time Offset", + type: "number-draggable", + step: 0.1, + propertyID: "timeOffset", + }, + { + label: "Local Only", + type: "bool", + propertyID: "localOnly", + } + ] + }, { id: "spatial", label: "SPATIAL", @@ -1811,6 +1867,7 @@ const GROUPS_PER_TYPE = { PolyLine: [ 'base', 'spatial', 'behavior', 'scripts', 'collision', 'physics' ], PolyVox: [ 'base', 'polyvox', 'spatial', 'behavior', 'scripts', 'collision', 'physics' ], Grid: [ 'base', 'grid', 'spatial', 'behavior', 'scripts', 'physics' ], + Sound: [ 'base', 'sound', 'spatial', 'behavior', 'scripts', 'physics' ], Multiple: [ 'base', 'spatial', 'behavior', 'scripts', 'collision', 'physics' ], }; diff --git a/scripts/system/create/entityProperties/html/tabs/sound.png b/scripts/system/create/entityProperties/html/tabs/sound.png new file mode 100644 index 0000000000000000000000000000000000000000..218bfe7c4ffa41221ac8182017661d4136236684 GIT binary patch literal 684 zcmWlWTS!v@7=ZtCnLC@?JTmi!>G776wwkp<4Yy;?r9qx?0Lf1Qf~tG3&*=crs7!5Ye*L=f@aRZ`DG(YG5|)AiCqF8HplroJ(pC9)(^OjR#`}u65*vjjkLr`n?MirCR1TM4efkg8DkK^FV z!X_ZC;aZfgB)AIt8*on;S1naa+&HqV9MB~gH^O*Kvg1PWRV5HqdC)HEQcxaCU`(Im z;2h(T%QSwA&-mN@m`g)>d^w%64nc`EehRaCh-{vw5B;riSdH3H9&L%HMT>(u^amjq z#JUBlje^+#1Sj9{hM0yB@+3MIO8$)G36dvpZoIib5?Z2h7^yNW4=3q`z{9o`>QOc8 zJ|?k*I8+NSR_%?2!(Y;5RgydrNoNIAOm#6fzP^dp%gcbIvkFRd`n`GnV3WOLx-9`v zVhug-T-aiDVQMgpVrEj_AxzzqD$@aJt(?8@yercC&o@sE zpUvu9YM87&OMYrQusC`iMvR1u0JSoODnDDX23+y)209-HrESMWbz(Rf>6^&b`xY%H z__r4^xyv}$>s_PGs!`w^-!WD1!wqS7n@ubjo#sJ5l2(nLK~U>XAmG7p;d|iMo$19S P0D#Q&9PR6iWi9^!#`NqK literal 0 HcmV?d00001 diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js index 7a188f3dc0..5238b2d374 100644 --- a/scripts/system/create/entitySelectionTool/entitySelectionTool.js +++ b/scripts/system/create/entitySelectionTool/entitySelectionTool.js @@ -2241,7 +2241,7 @@ SelectionDisplay = (function() { Entities.editEntity(selectionBox, selectionBoxGeometry); // UPDATE ICON TRANSLATE HANDLE - if (SelectionManager.entityType === "ParticleEffect" || SelectionManager.entityType === "Light") { + if (SelectionManager.entityType === "ParticleEffect" || SelectionManager.entityType === "Light" || SelectionManager.entityType === "Sound") { var iconSelectionBoxGeometry = { position: position, rotation: rotation diff --git a/scripts/system/create/modules/brokenURLReport.js b/scripts/system/create/modules/brokenURLReport.js index c04612bb27..4224826023 100644 --- a/scripts/system/create/modules/brokenURLReport.js +++ b/scripts/system/create/modules/brokenURLReport.js @@ -352,6 +352,17 @@ function brokenURLReport(entityIDs) { }; brokenURLReportUrlList.push(brokenURLReportUrlEntry); } + if (properties.type === "Sound" && properties.soundURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "soundURL", + url: soundURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } } if (brokenURLReportUrlList.length === 0) { audioFeedback.confirmation(); diff --git a/scripts/system/create/qml/EditTabView.qml b/scripts/system/create/qml/EditTabView.qml index 96e66c109e..26cc90a357 100644 --- a/scripts/system/create/qml/EditTabView.qml +++ b/scripts/system/create/qml/EditTabView.qml @@ -190,6 +190,18 @@ TabBar { editTabView.currentIndex = 2 } } + + NewEntityButton { + icon: "icons/sound.svg" + text: "SOUND" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newSoundButton" } + }); + editTabView.currentIndex = 2 + } + } } HifiControls.Button { diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml index 998c3a3aac..445f084392 100644 --- a/scripts/system/create/qml/EditToolsTabView.qml +++ b/scripts/system/create/qml/EditToolsTabView.qml @@ -196,6 +196,18 @@ TabBar { editTabView.currentIndex = tabIndex.properties } } + + NewEntityButton { + icon: "icons/sound.svg" + text: "SOUND" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newSoundButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } } HifiControls.Button { diff --git a/scripts/system/create/qml/icons/sound.svg b/scripts/system/create/qml/icons/sound.svg new file mode 100644 index 0000000000..448156343d --- /dev/null +++ b/scripts/system/create/qml/icons/sound.svg @@ -0,0 +1,59 @@ + + + + + + + diff --git a/scripts/system/html/js/includes.js b/scripts/system/html/js/includes.js index c604115f91..69ae9bed2d 100644 --- a/scripts/system/html/js/includes.js +++ b/scripts/system/html/js/includes.js @@ -20,6 +20,7 @@ const ENTITY_TYPE_ICON = { PolyLine: "", Shape: "n", Sphere: "n", + Sound: "G", Text: "l", Web: "q", Zone: "o",