diff --git a/Core/GDCore/Project/ResourcesManager.cpp b/Core/GDCore/Project/ResourcesManager.cpp
index e31ad179e647..6c80981f785d 100644
--- a/Core/GDCore/Project/ResourcesManager.cpp
+++ b/Core/GDCore/Project/ResourcesManager.cpp
@@ -184,11 +184,17 @@ std::map<gd::String, gd::PropertyDescriptor> AudioResource::GetProperties()
     const {
   std::map<gd::String, gd::PropertyDescriptor> properties;
   properties[_("Preload as sound")]
+      .SetDescription(_("Loads the fully decoded file into cache, so it can be played right away as Sound with no further delays."))
       .SetValue(preloadAsSound ? "true" : "false")
       .SetType("Boolean");
   properties[_("Preload as music")]
+      .SetDescription(_("Prepares the file for immediate streaming as Music (does not wait for complete download)."))
       .SetValue(preloadAsMusic ? "true" : "false")
       .SetType("Boolean");
+  properties[_("Preload in cache")]
+      .SetDescription(_("Loads the complete file into cache, but does not decode it into memory until requested."))
+      .SetValue(preloadInCache ? "true" : "false")
+      .SetType("Boolean");
 
   return properties;
 }
@@ -199,6 +205,8 @@ bool AudioResource::UpdateProperty(const gd::String& name,
     preloadAsSound = value == "1";
   else if (name == _("Preload as music"))
     preloadAsMusic = value == "1";
+  else if (name == _("Preload in cache"))
+    preloadInCache = value == "1";
 
   return true;
 }
@@ -569,6 +577,7 @@ void AudioResource::UnserializeFrom(const SerializerElement& element) {
   SetFile(element.GetStringAttribute("file"));
   SetPreloadAsMusic(element.GetBoolAttribute("preloadAsMusic"));
   SetPreloadAsSound(element.GetBoolAttribute("preloadAsSound"));
+  SetPreloadInCache(element.GetBoolAttribute("preloadInCache"));
 }
 
 void AudioResource::SerializeTo(SerializerElement& element) const {
@@ -576,6 +585,7 @@ void AudioResource::SerializeTo(SerializerElement& element) const {
   element.SetAttribute("file", GetFile());
   element.SetAttribute("preloadAsMusic", PreloadAsMusic());
   element.SetAttribute("preloadAsSound", PreloadAsSound());
+  element.SetAttribute("preloadInCache", PreloadInCache());
 }
 
 void FontResource::SetFile(const gd::String& newFile) {
diff --git a/Core/GDCore/Project/ResourcesManager.h b/Core/GDCore/Project/ResourcesManager.h
index fd62a6c1cd91..a2b9a41b6b23 100644
--- a/Core/GDCore/Project/ResourcesManager.h
+++ b/Core/GDCore/Project/ResourcesManager.h
@@ -223,7 +223,7 @@ class GD_CORE_API ImageResource : public Resource {
  */
 class GD_CORE_API AudioResource : public Resource {
  public:
-  AudioResource() : Resource(), preloadAsMusic(false), preloadAsSound(false) {
+  AudioResource() : Resource(), preloadAsMusic(false), preloadAsSound(false), preloadInCache(false) {
     SetKind("audio");
   };
   virtual ~AudioResource(){};
@@ -263,10 +263,21 @@ class GD_CORE_API AudioResource : public Resource {
    */
   void SetPreloadAsSound(bool enable = true) { preloadAsSound = enable; }
 
+  /**
+   * \brief Return true if the audio resource should be preloaded in cache (without decoding into memory).
+   */
+  bool PreloadInCache() const { return preloadInCache; }
+
+  /**
+   * \brief Set if the audio resource should be preloaded in cache (without decoding into memory).
+   */
+  void SetPreloadInCache(bool enable = true) { preloadInCache = enable; }
+
  private:
   gd::String file;
   bool preloadAsSound;
   bool preloadAsMusic;
+  bool preloadInCache;
 };
 
 /**
diff --git a/GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts b/GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts
index a4795c8a15df..94202489d39c 100644
--- a/GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts
+++ b/GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts
@@ -802,24 +802,41 @@ namespace gdjs {
         if (!filesToLoad.length) return;
         const file = filesToLoad.shift()!;
         const fileData = files[file][0];
-        if (!fileData.preloadAsSound && !fileData.preloadAsMusic) {
-          onLoad();
-        } else if (fileData.preloadAsSound && fileData.preloadAsMusic) {
-          let loadedOnce = false;
-          const callback = (_, error) => {
-            if (!loadedOnce) {
-              loadedOnce = true;
-              return;
-            }
+
+        let loadCounter = 0;
+        const callback = (_?: any, error?: string) => {
+          loadCounter--;
+          if (!loadCounter) {
             onLoad(_, error);
-          };
+          }
+        };
 
+        if (fileData.preloadAsMusic) {
+          loadCounter++;
           preloadAudioFile(file, callback, /* isMusic= */ true);
+        }
+
+        if (fileData.preloadAsSound) {
+          loadCounter++;
           preloadAudioFile(file, callback, /* isMusic= */ false);
-        } else if (fileData.preloadAsSound) {
-          preloadAudioFile(file, onLoad, /* isMusic= */ false);
-        } else if (fileData.preloadAsMusic)
-          preloadAudioFile(file, onLoad, /* isMusic= */ true);
+        } else if (fileData.preloadInCache) {
+          // preloading as sound already does a XHR request, hence "else if"
+          loadCounter++;
+          const sound = new XMLHttpRequest();
+          sound.addEventListener('load', callback);
+          sound.addEventListener('error', (_) =>
+            callback(_, 'XHR error: ' + file)
+          );
+          sound.addEventListener('abort', (_) =>
+            callback(_, 'XHR abort: ' + file)
+          );
+          sound.open('GET', file);
+          sound.send();
+        }
+
+        if (!loadCounter) {
+          onLoad();
+        }
       };
       loadNextFile();
     }
diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts
index db9d58f289fc..afd62e1d84e8 100644
--- a/GDJS/Runtime/types/project-data.d.ts
+++ b/GDJS/Runtime/types/project-data.d.ts
@@ -220,6 +220,7 @@ declare interface ResourceData {
   disablePreload?: boolean;
   preloadAsSound?: boolean;
   preloadAsMusic?: boolean;
+  preloadInCache?: boolean;
 }
 
 declare type ResourceKind =