Audio asset volume + VU

This also fixes a bug where multiple sounds would play at the same time making clipping even worse...

Some more details here:

https://2dimensions.slack.com/archives/CHMAP278R/p1712443028936919

Diffs=
f832e2617 Audio asset volume + VU (#6985)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index bd039cb..2928304 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-522a31bc24b4f8179b9a4f27d9a8154af385767a
+f832e26172cfbd11d52114ebb667e13418a5b1cd
diff --git a/build/rive_build_config.lua b/build/rive_build_config.lua
index 93de598..49fa644 100644
--- a/build/rive_build_config.lua
+++ b/build/rive_build_config.lua
@@ -135,7 +135,7 @@
     staticruntime('on') -- Match Skia's /MT flag for link compatibility
     runtime('Release') -- Use /MT even in debug (/MTd is incompatible with Skia)
     architecture('x64')
-    defines({ '_USE_MATH_DEFINES' })
+    defines({ '_USE_MATH_DEFINES', 'NOMINMAX' })
 end
 
 filter({ 'system:windows', 'options:toolset=clang' })
diff --git a/dev/core_generator/lib/src/definition.dart b/dev/core_generator/lib/src/definition.dart
index 4e02bf6..eeac898 100644
--- a/dev/core_generator/lib/src/definition.dart
+++ b/dev/core_generator/lib/src/definition.dart
@@ -30,11 +30,24 @@
       properties.where((property) => property.getExportType().storesData);
 
   Definition? _extensionOf;
+  Definition? _rawExtensionOf;
   Key? _key;
   bool _isAbstract = false;
   bool _editorOnly = false;
   bool _forRuntime = true;
   bool get forRuntime => _forRuntime;
+
+  Definition? getRuntimeExtensionOf(Definition? definition) {
+    var extensionOf = definition;
+    if (extensionOf != null) {
+      if (extensionOf._forRuntime) {
+        return extensionOf;
+      }
+      return getRuntimeExtensionOf(extensionOf._extensionOf);
+    }
+    return extensionOf;
+  }
+
   static Definition? make(String filename) {
     var definition = definitions[filename];
     if (definition != null) {
@@ -61,7 +74,8 @@
   Definition.fromFilename(this._filename, Map<String, dynamic> data) {
     dynamic extendsFilename = data['extends'];
     if (extendsFilename is String) {
-      _extensionOf = Definition.make(extendsFilename);
+      _rawExtensionOf = Definition.make(extendsFilename);
+      _extensionOf = getRuntimeExtensionOf(_rawExtensionOf);
     }
     dynamic nameValue = data['name'];
     if (nameValue is String) {
diff --git a/dev/defs/artboard.json b/dev/defs/artboard.json
index 8b1f39a..628ce9b 100644
--- a/dev/defs/artboard.json
+++ b/dev/defs/artboard.json
@@ -123,17 +123,6 @@
       "description": "List of selected animations",
       "runtime": false,
       "coop": false
-    },
-    "viewModelId": {
-      "type": "Id",
-      "typeRuntime": "uint",
-      "initialValue": "Core.missingId",
-      "initialValueRuntime": "-1",
-      "key": {
-        "int": 434,
-        "string": "viewmodelid"
-      },
-      "description": "The view model attached to this artboard data context."
     }
   }
 }
\ No newline at end of file
diff --git a/dev/defs/assets/audio_asset.json b/dev/defs/assets/audio_asset.json
index 667cf27..aed1f62 100644
--- a/dev/defs/assets/audio_asset.json
+++ b/dev/defs/assets/audio_asset.json
@@ -4,5 +4,57 @@
         "int": 406,
         "string": "audioasset"
     },
-    "extends": "assets/file_asset.json"
+    "extends": "assets/export_audio.json",
+    "properties": {
+        "sampleRate": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 473,
+                "string": "samplerate"
+            },
+            "description": "Sample rate of the source audio file.",
+            "runtime": false
+        },
+        "channels": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 474,
+                "string": "channels"
+            },
+            "description": "Channel count of the source audio file.",
+            "runtime": false
+        },
+        "durationSeconds": {
+            "type": "double",
+            "initialValue": "0",
+            "key": {
+                "int": 475,
+                "string": "durationseconds"
+            },
+            "description": "Duration in seconds of the source audio file.",
+            "runtime": false
+        },
+        "formatValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 476,
+                "string": "formatvalue"
+            },
+            "description": "Container format value of the source audio file.",
+            "runtime": false
+        },
+        "isCustom": {
+            "type": "bool",
+            "initialValue": "true",
+            "key": {
+                "int": 426,
+                "string": "iscustom"
+            },
+            "description": "True if a custom asset, i.e the user has uploaded it",
+            "runtime": false
+        }
+    }
 }
\ No newline at end of file
diff --git a/dev/defs/assets/export_audio.json b/dev/defs/assets/export_audio.json
new file mode 100644
index 0000000..37c747c
--- /dev/null
+++ b/dev/defs/assets/export_audio.json
@@ -0,0 +1,40 @@
+{
+    "name": "ExportAudio",
+    "key": {
+        "int": 422,
+        "string": "exportaudio"
+    },
+    "abstract": true,
+    "extends": "assets/file_asset.json",
+    "properties": {
+        "exportFormatValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 527,
+                "string": "formatvalue"
+            },
+            "description": "Start seconds of this clip",
+            "runtime": false
+        },
+        "exportQualityValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 528,
+                "string": "qualityvalue"
+            },
+            "description": "Start seconds of this clip",
+            "runtime": false
+        },
+        "volume": {
+            "type": "double",
+            "initialValue": "1",
+            "key": {
+                "int": 530,
+                "string": "volume"
+            },
+            "description": "Volume applied to all instances of this audio asset."
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua
index b9079eb..bef580b 100644
--- a/dev/test/premake5.lua
+++ b/dev/test/premake5.lua
@@ -6,6 +6,7 @@
     'WITH_RIVE_TOOLS',
     'WITH_RIVE_TEXT',
     'WITH_RIVE_AUDIO',
+    'WITH_RIVE_AUDIO_TOOLS',
 })
 
 dofile(path.join(path.getabsolute('../../'), 'premake5_v2.lua'))
diff --git a/include/rive/assets/export_audio.hpp b/include/rive/assets/export_audio.hpp
new file mode 100644
index 0000000..7642863
--- /dev/null
+++ b/include/rive/assets/export_audio.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_EXPORT_AUDIO_HPP_
+#define _RIVE_EXPORT_AUDIO_HPP_
+#include "rive/generated/assets/export_audio_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ExportAudio : public ExportAudioBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_engine.hpp b/include/rive/audio/audio_engine.hpp
index de7b21d..e23f539 100644
--- a/include/rive/audio/audio_engine.hpp
+++ b/include/rive/audio/audio_engine.hpp
@@ -11,16 +11,18 @@
 typedef struct ma_engine ma_engine;
 typedef struct ma_sound ma_sound;
 typedef struct ma_device ma_device;
+typedef struct ma_node_base ma_node_base;
 
 namespace rive
 {
 class AudioSound;
 class AudioSource;
-
+class LevelsNode;
 class AudioEngine : public RefCnt<AudioEngine>
 {
     friend class AudioSound;
     friend class AudioSource;
+    friend class LevelsNode;
 
 public:
     static const uint32_t defaultNumChannels = 2;
@@ -48,17 +50,26 @@
     bool sumAudioFrames(float* frames, uint64_t numFrames);
 #endif
 
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    void initLevelMonitor();
+    void levels(Span<float> levels);
+    float level(uint32_t channel);
+#endif
+
 private:
     AudioEngine(ma_engine* engine);
     ma_device* m_device;
     ma_engine* m_engine;
 
     std::vector<rcp<AudioSound>> m_completedSounds;
-
-    void completeSound(rcp<AudioSound> sound);
-    void purgeCompletedSounds();
+    rcp<AudioSound> m_playingSoundsHead;
     static void SoundCompleted(void* pUserData, ma_sound* pSound);
 
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    void measureLevels(const float* frames, uint32_t frameCount);
+    std::vector<float> m_levels;
+    LevelsNode* m_levelMonitor = nullptr;
+#endif
 #ifdef EXTERNAL_RIVE_AUDIO_ENGINE
     std::vector<float> m_readFrames;
 #endif
diff --git a/include/rive/audio/audio_sound.hpp b/include/rive/audio/audio_sound.hpp
index 2a409f6..783183c 100644
--- a/include/rive/audio/audio_sound.hpp
+++ b/include/rive/audio/audio_sound.hpp
@@ -16,19 +16,26 @@
     bool seek(uint64_t timeInFrames);
     ~AudioSound();
     void stop(uint64_t fadeTimeInFrames = 0);
+    float volume();
+    void volume(float value);
+    bool completed() const;
 
 private:
-    AudioSound(rcp<AudioEngine> engine);
-    void complete();
+    AudioSound(AudioEngine* engine);
+    ma_decoder* decoder() { return &m_decoder; }
+    ma_audio_buffer* buffer() { return &m_buffer; }
+    ma_sound* sound() { return &m_sound; }
+    void dispose();
 
-    rcp<AudioEngine> m_engine;
     ma_decoder m_decoder;
     ma_audio_buffer m_buffer;
     ma_sound m_sound;
 
-    ma_decoder* decoder() { return &m_decoder; }
-    ma_audio_buffer* buffer() { return &m_buffer; }
-    ma_sound* sound() { return &m_sound; }
+    // This is storage used by the AudioEngine.
+    bool m_isDisposed;
+    rcp<AudioSound> m_nextPlaying;
+    rcp<AudioSound> m_prevPlaying;
+    AudioEngine* m_engine;
 };
 } // namespace rive
 
diff --git a/include/rive/generated/assets/audio_asset_base.hpp b/include/rive/generated/assets/audio_asset_base.hpp
index 6af1e4e..49f6578 100644
--- a/include/rive/generated/assets/audio_asset_base.hpp
+++ b/include/rive/generated/assets/audio_asset_base.hpp
@@ -1,12 +1,12 @@
 #ifndef _RIVE_AUDIO_ASSET_BASE_HPP_
 #define _RIVE_AUDIO_ASSET_BASE_HPP_
-#include "rive/assets/file_asset.hpp"
+#include "rive/assets/export_audio.hpp"
 namespace rive
 {
-class AudioAssetBase : public FileAsset
+class AudioAssetBase : public ExportAudio
 {
 protected:
-    typedef FileAsset Super;
+    typedef ExportAudio Super;
 
 public:
     static const uint16_t typeKey = 406;
@@ -18,6 +18,7 @@
         switch (typeKey)
         {
             case AudioAssetBase::typeKey:
+            case ExportAudioBase::typeKey:
             case FileAssetBase::typeKey:
             case AssetBase::typeKey:
                 return true;
diff --git a/include/rive/generated/assets/export_audio_base.hpp b/include/rive/generated/assets/export_audio_base.hpp
new file mode 100644
index 0000000..12cb58c
--- /dev/null
+++ b/include/rive/generated/assets/export_audio_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_EXPORT_AUDIO_BASE_HPP_
+#define _RIVE_EXPORT_AUDIO_BASE_HPP_
+#include "rive/assets/file_asset.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class ExportAudioBase : public FileAsset
+{
+protected:
+    typedef FileAsset Super;
+
+public:
+    static const uint16_t typeKey = 422;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ExportAudioBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t volumePropertyKey = 530;
+
+private:
+    float m_Volume = 1.0f;
+
+public:
+    inline float volume() const { return m_Volume; }
+    void volume(float value)
+    {
+        if (m_Volume == value)
+        {
+            return;
+        }
+        m_Volume = value;
+        volumeChanged();
+    }
+
+    void copy(const ExportAudioBase& object)
+    {
+        m_Volume = object.m_Volume;
+        FileAsset::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case volumePropertyKey:
+                m_Volume = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return FileAsset::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void volumeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index e8fe108..a911f8c 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -66,6 +66,7 @@
 #include "rive/assets/asset.hpp"
 #include "rive/assets/audio_asset.hpp"
 #include "rive/assets/drawable_asset.hpp"
+#include "rive/assets/export_audio.hpp"
 #include "rive/assets/file_asset.hpp"
 #include "rive/assets/file_asset_contents.hpp"
 #include "rive/assets/folder.hpp"
@@ -1086,6 +1087,9 @@
             case DrawableAssetBase::widthPropertyKey:
                 object->as<DrawableAssetBase>()->width(value);
                 break;
+            case ExportAudioBase::volumePropertyKey:
+                object->as<ExportAudioBase>()->volume(value);
+                break;
         }
     }
     static void setBool(Core* object, int propertyKey, bool value)
@@ -1673,6 +1677,8 @@
                 return object->as<DrawableAssetBase>()->height();
             case DrawableAssetBase::widthPropertyKey:
                 return object->as<DrawableAssetBase>()->width();
+            case ExportAudioBase::volumePropertyKey:
+                return object->as<ExportAudioBase>()->volume();
         }
         return 0.0f;
     }
@@ -1981,6 +1987,7 @@
             case TextBase::paragraphSpacingPropertyKey:
             case DrawableAssetBase::heightPropertyKey:
             case DrawableAssetBase::widthPropertyKey:
+            case ExportAudioBase::volumePropertyKey:
                 return CoreDoubleType::id;
             case TransformComponentConstraintBase::offsetPropertyKey:
             case TransformComponentConstraintBase::doesCopyPropertyKey:
diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp
index c85eee1..268e22e 100644
--- a/src/audio/audio_engine.cpp
+++ b/src/audio/audio_engine.cpp
@@ -18,14 +18,136 @@
 #include "rive/audio/audio_sound.hpp"
 #include "rive/audio/audio_source.hpp"
 
+#include <algorithm>
+
 using namespace rive;
 
 void AudioEngine::SoundCompleted(void* pUserData, ma_sound* pSound)
 {
     AudioSound* audioSound = (AudioSound*)pUserData;
-    audioSound->complete();
+
+    auto next = audioSound->m_nextPlaying;
+    auto prev = audioSound->m_prevPlaying;
+    if (next != nullptr)
+    {
+        next->m_prevPlaying = prev;
+    }
+    if (prev != nullptr)
+    {
+        prev->m_nextPlaying = next;
+    }
+
+    auto engine = audioSound->m_engine;
+    if (engine->m_playingSoundsHead.get() == audioSound)
+    {
+        engine->m_playingSoundsHead = next;
+    }
+
+    // Unlink audio sound.
+    engine->m_completedSounds.push_back(ref_rcp(audioSound));
+    audioSound->m_nextPlaying = nullptr;
+    audioSound->m_prevPlaying = nullptr;
 }
 
+#ifdef WITH_RIVE_AUDIO_TOOLS
+namespace rive
+{
+class LevelsNode
+{
+public:
+    ma_node_base base;
+    AudioEngine* engine;
+    static void measureLevels(ma_node* pNode,
+                              const float** ppFramesIn,
+                              ma_uint32* pFrameCountIn,
+                              float** ppFramesOut,
+                              ma_uint32* pFrameCountOut)
+    {
+        const float* frames = ppFramesIn[0];
+
+        ma_uint32 frameCount = pFrameCountIn[0];
+
+        static_cast<LevelsNode*>(pNode)->engine->measureLevels(frames, (uint32_t)frameCount);
+    }
+};
+} // namespace rive
+
+void AudioEngine::measureLevels(const float* frames, uint32_t frameCount)
+{
+    uint32_t channelCount = channels();
+
+    for (uint32_t i = 0; i < frameCount; i++)
+    {
+        for (uint32_t c = 0; c < channelCount; c++)
+        {
+            float sample = *frames++;
+            m_levels[c] = std::max(m_levels[c], sample);
+        }
+    }
+}
+
+static ma_node_vtable measure_levels_vtable = {LevelsNode::measureLevels,
+                                               nullptr,
+                                               1,
+                                               1,
+                                               MA_NODE_FLAG_PASSTHROUGH};
+
+void AudioEngine::initLevelMonitor()
+{
+    if (m_levelMonitor == nullptr)
+    {
+        m_levelMonitor = new LevelsNode();
+        m_levelMonitor->engine = this;
+
+        ma_node_config nodeConfig = ma_node_config_init();
+        nodeConfig.vtable = &measure_levels_vtable;
+        uint32_t channelCount = channels();
+        nodeConfig.pInputChannels = &channelCount;
+        nodeConfig.pOutputChannels = &channelCount;
+        m_levels.resize(channelCount);
+
+        auto graph = ma_engine_get_node_graph(m_engine);
+        if (ma_node_init(graph, &nodeConfig, nullptr, &m_levelMonitor->base) != MA_SUCCESS)
+        {
+            delete m_levelMonitor;
+            m_levelMonitor = nullptr;
+            return;
+        }
+        if (ma_node_attach_output_bus(&m_levelMonitor->base,
+                                      0,
+                                      ma_node_graph_get_endpoint(graph),
+                                      0) != MA_SUCCESS)
+        {
+            ma_node_uninit(&m_levelMonitor->base, nullptr);
+            delete m_levelMonitor;
+            m_levelMonitor = nullptr;
+            return;
+        }
+    }
+}
+
+void AudioEngine::levels(Span<float> levels)
+{
+    int size = std::min((int)m_levels.size(), (int)levels.size());
+    for (int i = 0; i < size; i++)
+    {
+        levels[i] = m_levels[i];
+        m_levels[i] = 0.0f;
+    }
+}
+
+float AudioEngine::level(uint32_t channel)
+{
+    if (channel < m_levels.size())
+    {
+        float value = m_levels[channel];
+        m_levels[channel] = 0.0f;
+        return value;
+    }
+    return 0.0f;
+}
+#endif
+
 rcp<AudioEngine> AudioEngine::Make(uint32_t numChannels, uint32_t sampleRate)
 {
     ma_engine_config engineConfig = ma_engine_config_init();
@@ -60,11 +182,15 @@
                                   uint64_t endTime,
                                   uint64_t soundStartTime)
 {
-    purgeCompletedSounds();
+    // We have to dispose completed sounds out of the completed callback. So we
+    // do it on next play or at destruct.
+    for (auto sound : m_completedSounds)
+    {
+        sound->dispose();
+    }
+    m_completedSounds.clear();
 
-    rive::rcp<rive::AudioEngine> rcEngine = rive::rcp<rive::AudioEngine>(this);
-    rcEngine->ref();
-    rcp<AudioSound> audioSound = rcp<AudioSound>(new AudioSound(rcEngine));
+    rcp<AudioSound> audioSound = rcp<AudioSound>(new AudioSound(this));
     if (source->isBuffered())
     {
         rive::Span<float> samples = source->bufferedSamples();
@@ -115,9 +241,6 @@
         audioSound->seek(soundStartTime);
     }
 
-    // one extra ref for sound as we're waiting for playback to complete.
-    audioSound->ref();
-
     ma_sound_set_end_callback(audioSound->sound(), SoundCompleted, audioSound.get());
 
     if (startTime != 0)
@@ -128,30 +251,58 @@
     {
         ma_sound_set_stop_time_in_pcm_frames(audioSound->sound(), endTime);
     }
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    if (m_levelMonitor != nullptr)
+    {
+        ma_node_attach_output_bus(audioSound->sound(), 0, m_levelMonitor, 0);
+    }
+#endif
     if (ma_sound_start(audioSound->sound()) != MA_SUCCESS)
     {
         fprintf(stderr, "AudioSource::play - failed to start sound\n");
         return nullptr;
     }
 
-    return audioSound;
-}
-
-void AudioEngine::completeSound(rcp<AudioSound> sound) { m_completedSounds.push_back(sound); }
-
-void AudioEngine::purgeCompletedSounds()
-{
-    for (auto sound : m_completedSounds)
+    if (m_playingSoundsHead != nullptr)
     {
-        sound->unref();
+        m_playingSoundsHead->m_prevPlaying = audioSound;
     }
-    m_completedSounds.clear();
+    audioSound->m_nextPlaying = m_playingSoundsHead;
+    m_playingSoundsHead = audioSound;
+
+    return audioSound;
 }
 
 AudioEngine::~AudioEngine()
 {
+    auto sound = m_playingSoundsHead;
+    while (sound != nullptr)
+    {
+        sound->dispose();
+
+        auto next = sound->m_nextPlaying;
+        sound->m_nextPlaying = nullptr;
+        sound->m_prevPlaying = nullptr;
+        sound = next;
+    }
+
+    for (auto sound : m_completedSounds)
+    {
+        sound->dispose();
+    }
+    m_completedSounds.clear();
+
     ma_engine_uninit(m_engine);
     delete m_engine;
+
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    if (m_levelMonitor != nullptr)
+    {
+        ma_node_uninit(&m_levelMonitor->base, nullptr);
+        delete m_levelMonitor;
+    }
+
+#endif
 }
 
 uint64_t AudioEngine::timeInFrames()
diff --git a/src/audio/audio_sound.cpp b/src/audio/audio_sound.cpp
index 21631f4..e355aad 100644
--- a/src/audio/audio_sound.cpp
+++ b/src/audio/audio_sound.cpp
@@ -6,19 +6,42 @@
 
 using namespace rive;
 
-AudioSound::AudioSound(rcp<AudioEngine> engine) :
-    m_engine(std::move(engine)), m_decoder({}), m_buffer({}), m_sound({})
+AudioSound::AudioSound(AudioEngine* engine) :
+    m_decoder({}), m_buffer({}), m_sound({}), m_isDisposed(false), m_engine(engine)
 {}
 
-AudioSound::~AudioSound()
+void AudioSound::dispose()
 {
+    if (m_isDisposed)
+    {
+        return;
+    }
+    m_isDisposed = true;
     ma_sound_uninit(&m_sound);
     ma_decoder_uninit(&m_decoder);
     ma_audio_buffer_uninit(&m_buffer);
 }
 
+float AudioSound::volume() { return ma_sound_get_volume(&m_sound); }
+void AudioSound::volume(float value) { ma_sound_set_volume(&m_sound, value); }
+
+bool AudioSound::completed() const
+{
+    if (m_isDisposed)
+    {
+        return true;
+    }
+    return (bool)ma_sound_at_end(&m_sound);
+}
+
+AudioSound::~AudioSound() { dispose(); }
+
 void AudioSound::stop(uint64_t fadeTimeInFrames)
 {
+    if (m_isDisposed)
+    {
+        return;
+    }
     if (fadeTimeInFrames == 0)
     {
         ma_sound_stop(&m_sound);
@@ -29,15 +52,12 @@
     }
 }
 
-void AudioSound::complete()
-{
-    auto sound = rcp<AudioSound>(this);
-    sound->ref();
-    m_engine->completeSound(sound);
-}
-
 bool AudioSound::seek(uint64_t timeInFrames)
 {
+    if (m_isDisposed)
+    {
+        return false;
+    }
     return ma_sound_seek_to_pcm_frame(&m_sound, (ma_uint64)timeInFrames) == MA_SUCCESS;
 }
 
diff --git a/src/audio_event.cpp b/src/audio_event.cpp
index 8ea32c6..1ba130b 100644
--- a/src/audio_event.cpp
+++ b/src/audio_event.cpp
@@ -28,7 +28,11 @@
 #endif
                                              AudioEngine::RuntimeEngine();
 
-    engine->play(audioSource, engine->timeInFrames(), 0, 0);
+    auto sound = engine->play(audioSource, engine->timeInFrames(), 0, 0);
+    if (audioAsset->volume() != 1.0f)
+    {
+        sound->volume(audioAsset->volume());
+    }
 #endif
 }
 
diff --git a/test/audio_test.cpp b/test/audio_test.cpp
index f62e844..06ffa30 100644
--- a/test/audio_test.cpp
+++ b/test/audio_test.cpp
@@ -1,5 +1,6 @@
 #include "rive/audio/audio_engine.hpp"
 #include "rive/audio/audio_source.hpp"
+#include "rive/audio/audio_sound.hpp"
 #include "rive/audio/audio_reader.hpp"
 #include "rive/audio_event.hpp"
 #include "rive/assets/audio_asset.hpp"
@@ -36,26 +37,43 @@
     REQUIRE(engine != nullptr);
     auto file = loadFile("../../test/assets/audio/what.wav");
     auto span = Span<uint8_t>(file);
-    AudioSource audioSource(span);
-    REQUIRE(audioSource.channels() == 2);
-    REQUIRE(audioSource.sampleRate() == 44100);
+    rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+    REQUIRE(audioSource->channels() == 2);
+    REQUIRE(audioSource->sampleRate() == 44100);
 
     // Try some different sample rates.
     {
-        auto reader = audioSource.makeReader(2, 44100);
+        auto reader = audioSource->makeReader(2, 44100);
         REQUIRE(reader != nullptr);
         REQUIRE(reader->lengthInFrames() == 9688);
     }
     {
-        auto reader = audioSource.makeReader(1, 48000);
+        auto reader = audioSource->makeReader(1, 48000);
         REQUIRE(reader != nullptr);
         REQUIRE(reader->lengthInFrames() == 10544);
     }
     {
-        auto reader = audioSource.makeReader(2, 32000);
+        auto reader = audioSource->makeReader(2, 32000);
         REQUIRE(reader != nullptr);
         REQUIRE(reader->lengthInFrames() == 7029);
     }
+
+    float channels[2] = {0, 0};
+    engine->initLevelMonitor();
+    engine->levels(Span<float>(&channels[0], 2));
+    REQUIRE(channels[0] == 0);
+    REQUIRE(channels[1] == 0);
+
+    auto sound = engine->play(audioSource, 0, 0, 0);
+    float frames[512 * 2] = {};
+    engine->readAudioFrames(frames, 512);
+    engine->levels(Span<float>(&channels[0], 2));
+    REQUIRE(channels[0] != 0);
+    REQUIRE(channels[1] != 0);
+
+    engine->readAudioFrames(frames, 512);
+    REQUIRE(engine->level(0) != 0);
+    REQUIRE(engine->level(1) != 0);
 }
 
 TEST_CASE("file with audio loads correctly", "[text]")
@@ -83,4 +101,50 @@
     // artboard->advance(0.0f);
     // rive::NoOpRenderer renderer;
     // artboard->draw(&renderer);
-}
\ No newline at end of file
+}
+
+TEST_CASE("audio sound can outlive engine", "[audio]")
+{
+    rcp<AudioSound> sound;
+    {
+        rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+        REQUIRE(engine != nullptr);
+        auto file = loadFile("../../test/assets/audio/what.wav");
+        auto span = Span<uint8_t>(file);
+        rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+        REQUIRE(audioSource->channels() == 2);
+        REQUIRE(audioSource->sampleRate() == 44100);
+
+        sound = engine->play(audioSource, 0, 0, 0);
+        float frames[512 * 2] = {};
+        engine->readAudioFrames(frames, 512);
+    }
+    sound->stop();
+}
+
+TEST_CASE("many audio sounds can outlive engine", "[audio]")
+{
+    std::vector<rcp<AudioSound>> sounds;
+    {
+        rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+        REQUIRE(engine != nullptr);
+        auto file = loadFile("../../test/assets/audio/what.wav");
+        auto span = Span<uint8_t>(file);
+        rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+        REQUIRE(audioSource->channels() == 2);
+        REQUIRE(audioSource->sampleRate() == 44100);
+
+        for (int i = 0; i < 20; i++)
+        {
+            sounds.emplace_back(engine->play(audioSource, 0, 0, 0));
+        }
+        float frames[512 * 2] = {};
+        engine->readAudioFrames(frames, 512);
+    }
+    for (auto sound : sounds)
+    {
+        sound->stop();
+    }
+}
+
+// TODO check if sound->stop calls completed callback!!!
\ No newline at end of file