feat: add artboard volume to command queue/server (#12816) 88115b0949

Co-authored-by: David Skuza <david@rive.app>
diff --git a/.rive_head b/.rive_head
index 8797046..71e5f7a 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-61822bb24793050f151e291788b912a2fea1cffb
+88115b09496d99f10462da6e4ececcbd87988090
diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp
index efd415c..4a3ebb7 100644
--- a/include/rive/command_queue.hpp
+++ b/include/rive/command_queue.hpp
@@ -254,6 +254,11 @@
             uint64_t requestId,
             std::vector<std::string> stateMachineNames)
         {}
+
+        virtual void onArtboardVolumeReceived(const ArtboardHandle,
+                                              uint64_t requestId,
+                                              float volume)
+        {}
     };
 
     struct ViewModelInstanceData
@@ -681,6 +686,11 @@
                                            std::string path,
                                            uint64_t requestId = 0);
 
+    void setArtboardVolume(ArtboardHandle,
+                           float volume,
+                           uint64_t requestId = 0);
+    void requestArtboardVolume(ArtboardHandle, uint64_t requestId = 0);
+
     void requestStateMachineNames(ArtboardHandle, uint64_t requestId = 0);
     void requestDefaultViewModelInfo(ArtboardHandle,
                                      FileHandle,
@@ -881,6 +891,8 @@
         getViewModelListSize,
         clearViewModelList,
         listFileAssets,
+        setArtboardVolume,
+        getArtboardVolume
     };
 
     enum class Message
@@ -920,7 +932,8 @@
         imageError,
         audioError,
         fontError,
-        stateMachineError
+        stateMachineError,
+        artboardVolumeReceived
     };
 
     friend class CommandServer;
diff --git a/src/command_queue.cpp b/src/command_queue.cpp
index 01ad3f8..cbf1bac 100644
--- a/src/command_queue.cpp
+++ b/src/command_queue.cpp
@@ -173,6 +173,26 @@
     m_commandStream << requestId;
 }
 
+void CommandQueue::setArtboardVolume(ArtboardHandle artboardHandle,
+                                     float volume,
+                                     uint64_t requestId)
+{
+    AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable);
+    m_commandStream << Command::setArtboardVolume;
+    m_commandStream << artboardHandle;
+    m_commandStream << volume;
+    m_commandStream << requestId;
+}
+
+void CommandQueue::requestArtboardVolume(ArtboardHandle artboardHandle,
+                                         uint64_t requestId)
+{
+    AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable);
+    m_commandStream << Command::getArtboardVolume;
+    m_commandStream << artboardHandle;
+    m_commandStream << requestId;
+}
+
 void CommandQueue::deleteArtboard(ArtboardHandle artboardHandle,
                                   uint64_t requestId)
 {
@@ -1249,6 +1269,34 @@
 
                 break;
             }
+            case Message::artboardVolumeReceived:
+            {
+                ArtboardHandle handle;
+                uint64_t requestId;
+                float volume;
+                m_messageStream >> handle;
+                m_messageStream >> requestId;
+                m_messageStream >> volume;
+                lock.unlock();
+
+                if (m_globalArtboardListener)
+                {
+                    m_globalArtboardListener->onArtboardVolumeReceived(
+                        handle,
+                        requestId,
+                        volume);
+                }
+
+                auto itr = m_artboardListeners.find(handle);
+                if (itr != m_artboardListeners.end())
+                {
+                    itr->second->onArtboardVolumeReceived(itr->first,
+                                                          requestId,
+                                                          volume);
+                }
+
+                break;
+            }
             case Message::viewModelInstanceViewModelNameReceived:
             {
                 ViewModelInstanceHandle handle;
diff --git a/src/command_server.cpp b/src/command_server.cpp
index 410dfcb..7ee5189 100644
--- a/src/command_server.cpp
+++ b/src/command_server.cpp
@@ -1059,6 +1059,64 @@
             }
             break;
 
+            case CommandQueue::Command::setArtboardVolume:
+            {
+                ArtboardHandle handle;
+                float volume;
+                uint64_t requestId;
+                commandStream >> handle;
+                commandStream >> volume;
+                commandStream >> requestId;
+                lock.unlock();
+
+                if (auto artboardInstance = getArtboardInstance(handle))
+                {
+                    artboardInstance->volume(volume);
+                }
+                else
+                {
+                    ErrorReporter<ArtboardHandle>(
+                        this,
+                        handle,
+                        requestId,
+                        CommandQueue::Message::artboardError)
+                        << "artboard " << handle
+                        << " not found when trying to set artboard volume";
+                }
+            }
+            break;
+
+            case CommandQueue::Command::getArtboardVolume:
+            {
+                ArtboardHandle handle;
+                uint64_t requestId;
+                commandStream >> handle;
+                commandStream >> requestId;
+                lock.unlock();
+                auto artboard = getArtboardInstance(handle);
+                if (artboard)
+                {
+                    std::unique_lock<std::mutex> messageLock(
+                        m_commandQueue->m_messageMutex);
+                    messageStream
+                        << CommandQueue::Message::artboardVolumeReceived;
+                    messageStream << handle;
+                    messageStream << requestId;
+                    messageStream << artboard->volume();
+                }
+                else
+                {
+                    ErrorReporter<ArtboardHandle>(
+                        this,
+                        handle,
+                        requestId,
+                        CommandQueue::Message::artboardError)
+                        << "Invalid artboard handle " << handle
+                        << " when getting artboard volume";
+                }
+                break;
+            }
+
             case CommandQueue::Command::deleteArtboard:
             {
                 ArtboardHandle handle;
diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp
index 28b69d7..6efb0d9 100644
--- a/tests/unit_tests/runtime/command_queue_test.cpp
+++ b/tests/unit_tests/runtime/command_queue_test.cpp
@@ -2369,6 +2369,73 @@
     serverThread.join();
 }
 
+class TestArtboardVolumeListener : public CommandQueue::ArtboardListener
+{
+public:
+    virtual void onArtboardVolumeReceived(const ArtboardHandle handle,
+                                          uint64_t requestId,
+                                          float volume) override
+    {
+        CHECK(requestId == m_requestId);
+        CHECK(handle == m_handle);
+        m_receivedVolume = volume;
+        m_hasCallback = true;
+    }
+
+    uint64_t m_requestId;
+    ArtboardHandle m_handle;
+    float m_receivedVolume = -1.0f;
+    bool m_hasCallback = false;
+};
+
+TEST_CASE("Set Artboard Volume / Get Artboard Volume", "[CommandQueue]")
+{
+    auto commandQueue = make_rcp<CommandQueue>();
+    std::thread serverThread(server_thread, commandQueue);
+
+    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
+    FileHandle fileHandle = commandQueue->loadFile(
+        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
+
+    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
+
+    commandQueue->setArtboardVolume(artboardHandle, 0.5f);
+
+    commandQueue->runOnce(
+        [artboardHandle = artboardHandle](CommandServer* server) {
+            auto artboard = server->getArtboardInstance(artboardHandle);
+            CHECK(artboard->volume() == 0.5f);
+        });
+
+    commandQueue->setArtboardVolume(artboardHandle, 0.0f);
+
+    commandQueue->runOnce(
+        [artboardHandle = artboardHandle](CommandServer* server) {
+            auto artboard = server->getArtboardInstance(artboardHandle);
+            CHECK(artboard->volume() == 0.0f);
+        });
+
+    // Test getter via listener
+    TestArtboardVolumeListener volumeListener;
+
+    auto listenerArtboard =
+        commandQueue->instantiateDefaultArtboard(fileHandle, &volumeListener);
+    volumeListener.m_handle = listenerArtboard;
+    volumeListener.m_requestId = 0x50;
+
+    commandQueue->setArtboardVolume(listenerArtboard, 0.75f);
+    commandQueue->requestArtboardVolume(listenerArtboard, 0x50);
+
+    wait_for_server(commandQueue.get());
+    commandQueue->processMessages();
+
+    CHECK(volumeListener.m_hasCallback);
+    CHECK(volumeListener.m_receivedVolume == 0.75f);
+
+    commandQueue->disconnect();
+    serverThread.join();
+}
+
 TEST_CASE("View Model Property Subscriptions", "[CommandQueue]")
 {
     auto commandQueue = make_rcp<CommandQueue>();
@@ -3335,11 +3402,13 @@
     {
         CHECK(handle == m_handle);
         CHECK(error.size());
+        m_requestIDs.push_back(requestId);
         ++m_receivedErrors;
     }
 
     size_t m_receivedErrors = 0;
     ArtboardHandle m_handle;
+    std::vector<uint64_t> m_requestIDs;
 };
 
 TEST_CASE("artboard errors", "[CommandQueue]")
@@ -3384,6 +3453,31 @@
     serverThread.join();
 }
 
+TEST_CASE("Set Artboard Volume / Get Artboard Volume errors on invalid handles",
+          "[CommandQueue]")
+{
+    auto commandQueue = make_rcp<CommandQueue>();
+    std::thread serverThread(server_thread, commandQueue);
+
+    TestArtboardErrorListener errorListener;
+    auto invalidHandle = reinterpret_cast<rive::ArtboardHandle>(0xFF);
+    errorListener.m_handle = invalidHandle;
+    commandQueue->setGlobalArtboardListener(&errorListener);
+
+    commandQueue->setArtboardVolume(invalidHandle, 0.5f, 0x51);
+    commandQueue->requestArtboardVolume(invalidHandle, 0x52);
+
+    wait_for_server(commandQueue.get());
+    commandQueue->processMessages();
+
+    CHECK(errorListener.m_receivedErrors == 2);
+    CHECK(errorListener.m_requestIDs[0] == 0x51);
+    CHECK(errorListener.m_requestIDs[1] == 0x52);
+
+    commandQueue->disconnect();
+    serverThread.join();
+}
+
 class TestArtboardListener : public CommandQueue::ArtboardListener
 {
 public: