Audio engine Adds an audio engine abstraction (implemented with miniaudio) in Rive. We can selectively replace it with other abstractions later if we find miniaudio isn't well suited everywhere but I'm confident it will be based on flexibility with getting it working in the recorder. Adds audio support in: - packages/rive_common - WASM: rive_audio_wasm.dart - FFI: rive_audio_ffi.dart - packages/runtime - packages/recorder Subsequent PR will add support in: - packages/rive_unity - This is getting meaty enough... - packages/rive_flutter - This requires publishing rive_common so I want to make sure this is reviewed and accepted before publishing - I'd also prefer to merge layout constraints into rive_common before this lands. Editor features: - Updated preview window: <img width="248" alt="CleanShot 2024-01-15 at 14 44 31@2x" src="https://github.com/rive-app/rive/assets/454182/a9588be6-8370-4e22-ab32-af1e9ed71183"> - Preview waveforms in the timeline: <img width="651" alt="CleanShot 2024-01-15 at 14 44 53@2x" src="https://github.com/rive-app/rive/assets/454182/2710667f-838f-483d-9647-e2bcb9e0237a"> - Subsequent PR will also use threads in web editor build (currently uses a time/frame based decoder to offload ui). I can't do that until I can verify the Shared Memory access is granted once rive_common lands in pub.dev and then we can push to UAT. Diffs= 73bf11db3 Audio engine (#6454) Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index fc47721..8ed3cc4 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -b098ad23a15cc7b11f144e407374c83d5db0fee0 +73bf11db39230e7a6d302da9a27414d35bcbee99
diff --git a/build.sh b/build.sh index 8b2f42b..ec40d62 100755 --- a/build.sh +++ b/build.sh
@@ -32,8 +32,8 @@ else build() { echo "Building Rive for platform=$platform option=$OPTION" - echo premake5 gmake2 --with_rive_text "$1" - PREMAKE="premake5 gmake2 --with_rive_text $1" + echo premake5 gmake2 --with_rive_text --with_rive_audio=system "$1" + PREMAKE="premake5 gmake2 --with_rive_text --with_rive_audio=system $1" eval "$PREMAKE" if [ "$OPTION" = "clean" ]; then make clean
diff --git a/build/premake5.lua b/build/premake5.lua index d90d30e..d17c999 100644 --- a/build/premake5.lua +++ b/build/premake5.lua
@@ -12,9 +12,19 @@ defines {'WITH_RIVE_TEXT'} end filter {} +filter {'options:with_rive_audio=system'} +do + defines {'WITH_RIVE_AUDIO'} +end +filter {'options:with_rive_audio=external'} +do + defines {'WITH_RIVE_AUDIO', 'EXTERNAL_RIVE_AUDIO_ENGINE', 'MA_NO_DEVICE_IO'} +end +filter {} dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_harfbuzz.lua')) dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_sheenbidi.lua')) +dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_miniaudio.lua')) project 'rive' do @@ -26,13 +36,14 @@ includedirs { '../include', harfbuzz .. '/src', - sheenbidi .. '/Headers' + sheenbidi .. '/Headers', + miniaudio } files {'../src/**.cpp'} flags { - 'FatalCompileWarnings', + 'FatalCompileWarnings' } filter {'system:macosx'} @@ -43,12 +54,27 @@ } end - filter {'toolset:not msc'} + -- filter {'toolset:not msc', 'files:../src/audio/audio_engine.cpp'} + filter {'system:not windows', 'files:../src/audio/audio_engine.cpp'} do buildoptions { - '-Wimplicit-int-conversion', + '-Wno-implicit-int-conversion' } end + + filter {'system:windows', 'files:../src/audio/audio_engine.cpp'} + do + -- Too many warnings from miniaudio.h + removeflags {'FatalCompileWarnings'} + end + + -- filter 'files:../src/audio/audio_engine.cpp' + -- do + -- buildoptions { + -- '-Wno-implicit-int-conversion' + -- } + -- end + filter {'system:macosx', 'options:variant=runtime'} do buildoptions { @@ -64,7 +90,8 @@ filter {'system:ios'} do - buildoptions {'-flto=full'} + buildoptions {'-flto=full', '-Wno-implicit-int-conversion'} + files {'../src/audio/audio_engine.m'} end filter 'system:windows' @@ -76,8 +103,7 @@ filter {'system:ios', 'options:variant=system'} do buildoptions { - '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot ' .. - (os.getenv('IOS_SYSROOT') or '') + '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot ' .. (os.getenv('IOS_SYSROOT') or '') } end @@ -121,9 +147,9 @@ objdir '%{cfg.system}/arm64/obj/%{cfg.buildcfg}' end - filter "system:emscripten" + filter 'system:emscripten' do - buildoptions {"-pthread"} + buildoptions {'-pthread'} end filter 'configurations:debug' @@ -161,3 +187,20 @@ trigger = 'with_rive_text', description = 'Compiles in text features.' } + +newoption { + trigger = 'with_rive_audio', + value = 'disabled', + description = 'The audio mode to use.', + allowed = { + { + 'disabled' + }, + { + 'system' + }, + { + 'external' + } + } +}
diff --git a/dependencies/premake5_miniaudio.lua b/dependencies/premake5_miniaudio.lua new file mode 100644 index 0000000..aa9e5b2 --- /dev/null +++ b/dependencies/premake5_miniaudio.lua
@@ -0,0 +1,2 @@ +local dependency = require 'dependency' +miniaudio = dependency.github('rive-app/miniaudio', 'rive')
diff --git a/dev/defs/assets/audio_asset.json b/dev/defs/assets/audio_asset.json new file mode 100644 index 0000000..667cf27 --- /dev/null +++ b/dev/defs/assets/audio_asset.json
@@ -0,0 +1,8 @@ +{ + "name": "AudioAsset", + "key": { + "int": 406, + "string": "audioasset" + }, + "extends": "assets/file_asset.json" +} \ No newline at end of file
diff --git a/dev/defs/audio_event.json b/dev/defs/audio_event.json new file mode 100644 index 0000000..3ee2a47 --- /dev/null +++ b/dev/defs/audio_event.json
@@ -0,0 +1,21 @@ +{ + "name": "AudioEvent", + "key": { + "int": 407, + "string": "audioevent" + }, + "extends": "event.json", + "properties": { + "assetId": { + "type": "Id", + "typeRuntime": "uint", + "initialValue": "Core.missingId", + "initialValueRuntime": "-1", + "key": { + "int": 408, + "string": "assetid" + }, + "description": "Audio asset to play when event fires" + } + } +} \ No newline at end of file
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua index 030865e..0018775 100644 --- a/dev/test/premake5.lua +++ b/dev/test/premake5.lua
@@ -18,6 +18,7 @@ dofile(path.join(path.getabsolute('../../dependencies/'), 'premake5_harfbuzz.lua')) dofile(path.join(path.getabsolute('../../dependencies/'), 'premake5_sheenbidi.lua')) +dofile(path.join(path.getabsolute('../../dependencies/'), 'premake5_miniaudio.lua')) project('tests') do @@ -28,13 +29,14 @@ objdir 'build/obj/%{cfg.buildcfg}' flags {'FatalWarnings'} buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti'} - exceptionhandling "On" + exceptionhandling 'On' includedirs { './include', '../../include', harfbuzz .. '/src', - sheenbidi .. '/Headers' + sheenbidi .. '/Headers', + miniaudio } links { 'rive_harfbuzz', @@ -47,7 +49,35 @@ '../../utils/**.cpp' -- no_op utils } - defines {'TESTING', 'ENABLE_QUERY_FLAT_VERTICES', 'WITH_RIVE_TOOLS', 'WITH_RIVE_TEXT'} + defines {'TESTING', 'ENABLE_QUERY_FLAT_VERTICES', 'WITH_RIVE_TOOLS', 'WITH_RIVE_TEXT', 'WITH_RIVE_AUDIO'} + + filter {'system:windows', 'files:../../src/audio/audio_engine.cpp'} + do + -- Too many warnings from miniaudio.h + removeflags {'FatalCompileWarnings'} + removebuildoptions {'-Wall'} + end + + filter {'system:windows', 'toolset:clang'} + do + -- Too many warnings from miniaudio.h + buildoptions { + '-Wno-nonportable-system-include-path', + '-Wno-zero-as-null-pointer-constant', + '-Wno-missing-prototypes', + '-Wno-cast-qual', + '-Wno-format-nonliteral', + '-Wno-cast-align', + '-Wno-covered-switch-default', + '-Wno-comma', + '-Wno-tautological-type-limit-compare', + '-Wno-extra-semi-stmt', + '-Wno-tautological-constant-out-of-range-compare', + '-Wno-implicit-fallthrough', + '-Wno-implicit-int-conversion', + '-Wno-undef' + } + end filter 'configurations:debug' do @@ -55,6 +85,12 @@ symbols 'On' end + filter 'system:linux' + do + defines {'EXTERNAL_RIVE_AUDIO_ENGINE'} + links {'dl', 'pthread'} + end + filter 'system:windows' do removebuildoptions {
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp index ff5ea09..a7cb9f6 100644 --- a/include/rive/animation/linear_animation_instance.hpp +++ b/include/rive/animation/linear_animation_instance.hpp
@@ -2,12 +2,12 @@ #define _RIVE_LINEAR_ANIMATION_INSTANCE_HPP_ #include "rive/artboard.hpp" +#include "rive/core/field_types/core_callback_type.hpp" #include "rive/scene.hpp" namespace rive { class LinearAnimation; -class KeyedCallbackReporter; class LinearAnimationInstance : public Scene {
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index 2a4838b..a9c5ad5 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -5,7 +5,7 @@ #include <stddef.h> #include <vector> #include "rive/animation/linear_animation_instance.hpp" -#include "rive/animation/keyed_callback_reporter.hpp" +#include "rive/core/field_types/core_callback_type.hpp" #include "rive/listener_type.hpp" #include "rive/scene.hpp" @@ -37,7 +37,7 @@ float m_secondsDelay; }; -class StateMachineInstance : public Scene, public KeyedCallbackReporter +class StateMachineInstance : public Scene { friend class SMIInput; friend class KeyedProperty; @@ -110,7 +110,7 @@ NestedArtboard* parentNestedArtboard() { return m_parentNestedArtboard; } /// Tracks an event that reported, will be cleared at the end of the next advance. - void reportEvent(Event* event, float secondsDelay = 0.0f); + void reportEvent(Event* event, float secondsDelay = 0.0f) override; /// Gets the number of events that reported since the last advance. std::size_t reportedEventCount() const; @@ -118,12 +118,6 @@ /// Gets a reported event at an index < reportedEventCount(). const EventReport reportedEventAt(std::size_t index) const; - /// Report which time based events have elapsed on a timeline within this - /// state machine. - void reportKeyedCallback(uint32_t objectId, - uint32_t propertyKey, - float elapsedSeconds) override; - private: std::vector<EventReport> m_reportedEvents; const StateMachine* m_machine;
diff --git a/include/rive/assets/audio_asset.hpp b/include/rive/assets/audio_asset.hpp new file mode 100644 index 0000000..d1de305 --- /dev/null +++ b/include/rive/assets/audio_asset.hpp
@@ -0,0 +1,29 @@ +#ifndef _RIVE_AUDIO_ASSET_HPP_ +#define _RIVE_AUDIO_ASSET_HPP_ +#include "rive/generated/assets/audio_asset_base.hpp" +#include "rive/audio/audio_source.hpp" + +namespace rive +{ +class AudioAsset : public AudioAssetBase +{ +public: + AudioAsset(); + ~AudioAsset() override; + bool decode(SimpleArray<uint8_t>&, Factory*) override; + std::string fileExtension() const override; + +#ifdef WITH_RIVE_AUDIO +#ifdef TESTING + bool hasAudioSource() { return m_audioSource != nullptr; } +#endif + + rcp<AudioSource> audioSource() { return m_audioSource; } + +private: + rcp<AudioSource> m_audioSource; +#endif +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/assets/file_asset.hpp b/include/rive/assets/file_asset.hpp index f1d587b..a8ed721 100644 --- a/include/rive/assets/file_asset.hpp +++ b/include/rive/assets/file_asset.hpp
@@ -3,6 +3,7 @@ #include "rive/assets/file_asset_referencer.hpp" #include "rive/generated/assets/file_asset_base.hpp" #include "rive/span.hpp" +#include "rive/simple_array.hpp" #include <string> namespace rive @@ -20,7 +21,7 @@ void decodeCdnUuid(Span<const uint8_t> value) override; void copyCdnUuid(const FileAssetBase& object) override; - virtual bool decode(Span<const uint8_t>, Factory*) = 0; + virtual bool decode(SimpleArray<uint8_t>&, Factory*) = 0; virtual std::string fileExtension() const = 0; StatusCode import(ImportStack& importStack) override; const std::vector<FileAssetReferencer*> fileAssetReferencers()
diff --git a/include/rive/assets/file_asset_contents.hpp b/include/rive/assets/file_asset_contents.hpp index c0afb30..67457c0 100644 --- a/include/rive/assets/file_asset_contents.hpp +++ b/include/rive/assets/file_asset_contents.hpp
@@ -2,19 +2,20 @@ #define _RIVE_FILE_ASSET_CONTENTS_HPP_ #include "rive/generated/assets/file_asset_contents_base.hpp" #include <cstdint> +#include "rive/simple_array.hpp" namespace rive { class FileAssetContents : public FileAssetContentsBase { -private: - std::vector<uint8_t> m_Bytes; - public: - Span<const uint8_t> bytes() const; + SimpleArray<uint8_t>& bytes(); StatusCode import(ImportStack& importStack) override; void decodeBytes(Span<const uint8_t> value) override; void copyBytes(const FileAssetContentsBase& object) override; + +private: + SimpleArray<uint8_t> m_bytes; }; } // namespace rive
diff --git a/include/rive/assets/font_asset.hpp b/include/rive/assets/font_asset.hpp index e39fb72..29fad8d 100644 --- a/include/rive/assets/font_asset.hpp +++ b/include/rive/assets/font_asset.hpp
@@ -9,7 +9,7 @@ class FontAsset : public FontAssetBase { public: - bool decode(Span<const uint8_t>, Factory*) override; + bool decode(SimpleArray<uint8_t>&, Factory*) override; std::string fileExtension() const override; const rcp<Font> font() const { return m_font; } void font(rcp<Font> font);
diff --git a/include/rive/assets/image_asset.hpp b/include/rive/assets/image_asset.hpp index 93243fe..54a3856 100644 --- a/include/rive/assets/image_asset.hpp +++ b/include/rive/assets/image_asset.hpp
@@ -3,6 +3,7 @@ #include "rive/generated/assets/image_asset_base.hpp" #include "rive/renderer.hpp" +#include "rive/simple_array.hpp" #include <string> namespace rive @@ -19,7 +20,7 @@ #ifdef TESTING std::size_t decodedByteSize = 0; #endif - bool decode(Span<const uint8_t>, Factory*) override; + bool decode(SimpleArray<uint8_t>&, Factory*) override; std::string fileExtension() const override; RenderImage* renderImage() const { return m_RenderImage.get(); } void renderImage(rcp<RenderImage> renderImage);
diff --git a/include/rive/audio/audio_engine.hpp b/include/rive/audio/audio_engine.hpp new file mode 100644 index 0000000..b84a13f --- /dev/null +++ b/include/rive/audio/audio_engine.hpp
@@ -0,0 +1,64 @@ +#ifdef WITH_RIVE_AUDIO +#ifndef _RIVE_AUDIO_ENGINE_HPP_ +#define _RIVE_AUDIO_ENGINE_HPP_ + +#include "rive/refcnt.hpp" +#include "rive/span.hpp" +#include <vector> +#include <stdio.h> +#include <cstdint> + +typedef struct ma_engine ma_engine; +typedef struct ma_sound ma_sound; +typedef struct ma_device ma_device; + +namespace rive +{ +class AudioSound; +class AudioSource; + +class AudioEngine : public RefCnt<AudioEngine> +{ + friend class AudioSound; + friend class AudioSource; + +public: + static const uint32_t defaultNumChannels = 2; + static const uint32_t defaultSampleRate = 48000; + + static rcp<AudioEngine> Make(uint32_t numChannels, uint32_t sampleRate); + + ma_device* device() { return m_device; } + ma_engine* engine() { return m_engine; } + + uint32_t channels() const; + uint32_t sampleRate() const; + uint64_t timeInFrames(); + + ~AudioEngine(); + rcp<AudioSound> play(rcp<AudioSource> source, + uint64_t startTime, + uint64_t endTime, + uint64_t soundStartTime); + + static rcp<AudioEngine> RuntimeEngine(); + +#ifdef EXTERNAL_RIVE_AUDIO_ENGINE + bool readAudioFrames(float* frames, uint64_t numFrames, uint64_t* framesRead = nullptr); +#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(); + static void SoundCompleted(void* pUserData, ma_sound* pSound); +}; +} // namespace rive + +#endif +#endif \ No newline at end of file
diff --git a/include/rive/audio/audio_format.hpp b/include/rive/audio/audio_format.hpp new file mode 100644 index 0000000..ab73fe5 --- /dev/null +++ b/include/rive/audio/audio_format.hpp
@@ -0,0 +1,17 @@ +#ifdef WITH_RIVE_AUDIO +#ifndef _RIVE_AUDIO_FORMAT_HPP_ +#define _RIVE_AUDIO_FORMAT_HPP_ +namespace rive +{ +enum class AudioFormat : unsigned int +{ + unknown = 0, + wav, + flac, + mp3, + vorbis, + buffered +}; +} +#endif +#endif \ No newline at end of file
diff --git a/include/rive/audio/audio_reader.hpp b/include/rive/audio/audio_reader.hpp new file mode 100644 index 0000000..6243fd7 --- /dev/null +++ b/include/rive/audio/audio_reader.hpp
@@ -0,0 +1,37 @@ +#ifdef WITH_RIVE_AUDIO +#ifndef _RIVE_AUDIO_READER_HPP_ +#define _RIVE_AUDIO_READER_HPP_ + +#include "miniaudio.h" +#include "rive/refcnt.hpp" +#include "rive/span.hpp" +#include <vector> + +namespace rive +{ +class AudioSource; +class AudioReader : public RefCnt<AudioReader> +{ + friend class AudioSource; + +public: + uint64_t lengthInFrames(); + Span<float> read(uint64_t frameCount); + ~AudioReader(); + uint32_t channels() const; + uint32_t sampleRate() const; + +private: + AudioReader(rcp<AudioSource> audioSource, uint32_t channels); + ma_decoder* decoder(); + + rcp<AudioSource> m_source; + uint32_t m_channels; + ma_decoder m_decoder; + std::vector<float> m_readBuffer; +}; + +} // namespace rive + +#endif +#endif \ No newline at end of file
diff --git a/include/rive/audio/audio_sound.hpp b/include/rive/audio/audio_sound.hpp new file mode 100644 index 0000000..2a409f6 --- /dev/null +++ b/include/rive/audio/audio_sound.hpp
@@ -0,0 +1,36 @@ +#ifdef WITH_RIVE_AUDIO +#ifndef _RIVE_AUDIO_SOUND_HPP_ +#define _RIVE_AUDIO_SOUND_HPP_ + +#include "miniaudio.h" +#include "rive/refcnt.hpp" + +namespace rive +{ +class AudioEngine; +class AudioSound : public RefCnt<AudioSound> +{ + friend class AudioEngine; + +public: + bool seek(uint64_t timeInFrames); + ~AudioSound(); + void stop(uint64_t fadeTimeInFrames = 0); + +private: + AudioSound(rcp<AudioEngine> engine); + void complete(); + + 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; } +}; +} // namespace rive + +#endif +#endif \ No newline at end of file
diff --git a/include/rive/audio/audio_source.hpp b/include/rive/audio/audio_source.hpp new file mode 100644 index 0000000..834741e --- /dev/null +++ b/include/rive/audio/audio_source.hpp
@@ -0,0 +1,50 @@ +#ifdef WITH_RIVE_AUDIO +#ifndef _RIVE_AUDIO_SOURCE_HPP_ +#define _RIVE_AUDIO_SOURCE_HPP_ + +#include "miniaudio.h" +#include "rive/refcnt.hpp" +#include "rive/span.hpp" +#include "rive/simple_array.hpp" +#include "rive/audio/audio_format.hpp" + +namespace rive +{ +class AudioEngine; +class AudioReader; +class AudioSound; +class AudioSource : public RefCnt<AudioSource> +{ +public: + // Makes an audio source that does not own it's backing bytes. + AudioSource(rive::Span<uint8_t> fileBytes); + + // Makes an audio source whose backing bytes will be deleted when the + // AudioSource deletes. + AudioSource(rive::SimpleArray<uint8_t> fileBytes); + + // Makes a buffered audio source whose backing bytes will be deleted when + // the AudioSource deletes. + AudioSource(rive::Span<float> samples, uint32_t numChannels, uint32_t sampleRate); + + rcp<AudioReader> makeReader(uint32_t numChannels, uint32_t sampleRate); + + uint32_t channels(); + uint32_t sampleRate(); + AudioFormat format() const; + const rive::Span<uint8_t> bytes() const { return m_fileBytes; } + + const rive::Span<float> bufferedSamples() const; + bool isBuffered() const { return m_isBuffered; } + +private: + bool m_isBuffered; + uint32_t m_channels; + uint32_t m_sampleRate; + rive::Span<uint8_t> m_fileBytes; + rive::SimpleArray<uint8_t> m_ownedBytes; +}; +} // namespace rive + +#endif +#endif \ No newline at end of file
diff --git a/include/rive/audio_event.hpp b/include/rive/audio_event.hpp new file mode 100644 index 0000000..cd67c6c --- /dev/null +++ b/include/rive/audio_event.hpp
@@ -0,0 +1,24 @@ +#ifndef _RIVE_AUDIO_EVENT_HPP_ +#define _RIVE_AUDIO_EVENT_HPP_ +#include "rive/generated/audio_event_base.hpp" +#include "rive/assets/file_asset_referencer.hpp" + +namespace rive +{ +class AudioAsset; +class AudioEvent : public AudioEventBase, public FileAssetReferencer +{ +public: + StatusCode import(ImportStack& importStack) override; + void setAsset(FileAsset* asset) override; + uint32_t assetId() override; + void trigger(const CallbackData& value) override; + +#ifdef TESTING + AudioAsset* asset() const { return (AudioAsset*)m_fileAsset; } +#endif + Core* clone() const override; +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/core/field_types/core_callback_type.hpp b/include/rive/core/field_types/core_callback_type.hpp index 047aaa0..1c8ba62 100644 --- a/include/rive/core/field_types/core_callback_type.hpp +++ b/include/rive/core/field_types/core_callback_type.hpp
@@ -3,18 +3,25 @@ namespace rive { -class StateMachineInstance; +class Event; +class CallbackContext +{ +public: + virtual ~CallbackContext() {} + virtual void reportEvent(Event* event, float secondsDelay = 0.0f) {} +}; + class CallbackData { public: - StateMachineInstance* context() const { return m_context; } + CallbackContext* context() const { return m_context; } float delaySeconds() const { return m_delaySeconds; } - CallbackData(StateMachineInstance* context, float delaySeconds) : + CallbackData(CallbackContext* context, float delaySeconds) : m_context(context), m_delaySeconds(delaySeconds) {} private: - StateMachineInstance* m_context; + CallbackContext* m_context; float m_delaySeconds; }; } // namespace rive
diff --git a/include/rive/generated/assets/audio_asset_base.hpp b/include/rive/generated/assets/audio_asset_base.hpp new file mode 100644 index 0000000..6af1e4e --- /dev/null +++ b/include/rive/generated/assets/audio_asset_base.hpp
@@ -0,0 +1,37 @@ +#ifndef _RIVE_AUDIO_ASSET_BASE_HPP_ +#define _RIVE_AUDIO_ASSET_BASE_HPP_ +#include "rive/assets/file_asset.hpp" +namespace rive +{ +class AudioAssetBase : public FileAsset +{ +protected: + typedef FileAsset Super; + +public: + static const uint16_t typeKey = 406; + + /// Helper to quickly determine if a core object extends another without RTTI + /// at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case AudioAssetBase::typeKey: + case FileAssetBase::typeKey: + case AssetBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + Core* clone() const override; + +protected: +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/generated/audio_event_base.hpp b/include/rive/generated/audio_event_base.hpp new file mode 100644 index 0000000..fece563 --- /dev/null +++ b/include/rive/generated/audio_event_base.hpp
@@ -0,0 +1,73 @@ +#ifndef _RIVE_AUDIO_EVENT_BASE_HPP_ +#define _RIVE_AUDIO_EVENT_BASE_HPP_ +#include "rive/core/field_types/core_uint_type.hpp" +#include "rive/event.hpp" +namespace rive +{ +class AudioEventBase : public Event +{ +protected: + typedef Event Super; + +public: + static const uint16_t typeKey = 407; + + /// Helper to quickly determine if a core object extends another without RTTI + /// at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case AudioEventBase::typeKey: + case EventBase::typeKey: + case ContainerComponentBase::typeKey: + case ComponentBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + static const uint16_t assetIdPropertyKey = 408; + +private: + uint32_t m_AssetId = -1; + +public: + inline uint32_t assetId() const { return m_AssetId; } + void assetId(uint32_t value) + { + if (m_AssetId == value) + { + return; + } + m_AssetId = value; + assetIdChanged(); + } + + Core* clone() const override; + void copy(const AudioEventBase& object) + { + m_AssetId = object.m_AssetId; + Event::copy(object); + } + + bool deserialize(uint16_t propertyKey, BinaryReader& reader) override + { + switch (propertyKey) + { + case assetIdPropertyKey: + m_AssetId = CoreUintType::deserialize(reader); + return true; + } + return Event::deserialize(propertyKey, reader); + } + +protected: + virtual void assetIdChanged() {} +}; +} // 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 658b81b..e8fe108 100644 --- a/include/rive/generated/core_registry.hpp +++ b/include/rive/generated/core_registry.hpp
@@ -64,12 +64,14 @@ #include "rive/animation/transition_value_condition.hpp" #include "rive/artboard.hpp" #include "rive/assets/asset.hpp" +#include "rive/assets/audio_asset.hpp" #include "rive/assets/drawable_asset.hpp" #include "rive/assets/file_asset.hpp" #include "rive/assets/file_asset_contents.hpp" #include "rive/assets/folder.hpp" #include "rive/assets/font_asset.hpp" #include "rive/assets/image_asset.hpp" +#include "rive/audio_event.hpp" #include "rive/backboard.hpp" #include "rive/bones/bone.hpp" #include "rive/bones/cubic_weight.hpp" @@ -366,8 +368,12 @@ return new ImageAsset(); case FontAssetBase::typeKey: return new FontAsset(); + case AudioAssetBase::typeKey: + return new AudioAsset(); case FileAssetContentsBase::typeKey: return new FileAssetContents(); + case AudioEventBase::typeKey: + return new AudioEvent(); } return nullptr; } @@ -678,6 +684,9 @@ case FileAssetBase::assetIdPropertyKey: object->as<FileAssetBase>()->assetId(value); break; + case AudioEventBase::assetIdPropertyKey: + object->as<AudioEventBase>()->assetId(value); + break; } } static void setDouble(Core* object, int propertyKey, float value) @@ -1393,6 +1402,8 @@ return object->as<TextValueRunBase>()->styleId(); case FileAssetBase::assetIdPropertyKey: return object->as<FileAssetBase>()->assetId(); + case AudioEventBase::assetIdPropertyKey: + return object->as<AudioEventBase>()->assetId(); } return 0; } @@ -1837,6 +1848,7 @@ case TextBase::originValuePropertyKey: case TextValueRunBase::styleIdPropertyKey: case FileAssetBase::assetIdPropertyKey: + case AudioEventBase::assetIdPropertyKey: return CoreUintType::id; case CustomPropertyNumberBase::propertyValuePropertyKey: case ConstraintBase::strengthPropertyKey:
diff --git a/include/rive/relative_local_asset_loader.hpp b/include/rive/relative_local_asset_loader.hpp index f37271a..6056c9c 100644 --- a/include/rive/relative_local_asset_loader.hpp +++ b/include/rive/relative_local_asset_loader.hpp
@@ -40,12 +40,11 @@ fseek(fp, 0, SEEK_END); const size_t length = ftell(fp); fseek(fp, 0, SEEK_SET); - uint8_t* bytes = new uint8_t[length]; - if (fread(bytes, 1, length, fp) == length) + SimpleArray<uint8_t> bytes(length); + if (fread(bytes.data(), 1, length, fp) == length) { - asset.decode(Span<const uint8_t>(bytes, length), factory); + asset.decode(bytes, factory); } - delete[] bytes; return true; } };
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp index dd4d6ed..21cb606 100644 --- a/include/rive/scene.hpp +++ b/include/rive/scene.hpp
@@ -4,6 +4,8 @@ #include "rive/animation/loop.hpp" #include "rive/math/aabb.hpp" #include "rive/math/vec2d.hpp" +#include "rive/animation/keyed_callback_reporter.hpp" +#include "rive/core/field_types/core_callback_type.hpp" #include <string> namespace rive @@ -16,13 +18,13 @@ class SMINumber; class SMITrigger; -class Scene +class Scene : public KeyedCallbackReporter, public CallbackContext { protected: Scene(ArtboardInstance*); public: - virtual ~Scene() {} + ~Scene() override {} Scene(Scene const& lhs) : m_artboardInstance(lhs.m_artboardInstance) {} @@ -54,6 +56,12 @@ virtual SMINumber* getNumber(const std::string&) const; virtual SMITrigger* getTrigger(const std::string&) const; + /// Report which time based events have elapsed on a timeline within this + /// state machine. + void reportKeyedCallback(uint32_t objectId, + uint32_t propertyKey, + float elapsedSeconds) override; + protected: ArtboardInstance* m_artboardInstance; };
diff --git a/skia/renderer/build.sh b/skia/renderer/build.sh index 5a52b58..474b491 100755 --- a/skia/renderer/build.sh +++ b/skia/renderer/build.sh
@@ -42,7 +42,7 @@ else build() { echo "Building Rive Renderer for platform=$platform option=$OPTION" - PREMAKE="premake5 --scripts=../../../build gmake2 --with_rive_text $1" + PREMAKE="premake5 --scripts=../../../build gmake2 --with_rive_text --with_rive_audio=system $1" eval "$PREMAKE" if [ "$OPTION" = "clean" ]; then make clean
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua index 7c1826f..2376a52 100644 --- a/skia/renderer/build/premake5.lua +++ b/skia/renderer/build/premake5.lua
@@ -35,10 +35,10 @@ } flags { - 'FatalCompileWarnings', + 'FatalCompileWarnings' } - filter "system:windows" + filter 'system:windows' do architecture 'x64' defines {'_USE_MATH_DEFINES'} @@ -55,8 +55,7 @@ do links {} buildoptions { - '-fembed-bitcode -arch arm64 -arch x86_64 -isysroot ' .. - (os.getenv('MACOS_SYSROOT') or '') + '-fembed-bitcode -arch arm64 -arch x86_64 -isysroot ' .. (os.getenv('MACOS_SYSROOT') or '') } end @@ -76,8 +75,7 @@ filter {'system:ios', 'options:variant=system'} do buildoptions { - '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot ' .. - (os.getenv('IOS_SYSROOT') or '') + '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot ' .. (os.getenv('IOS_SYSROOT') or '') } end @@ -156,6 +154,14 @@ do defines {'WITH_RIVE_TEXT'} end + filter {'options:with_rive_audio=system'} + do + defines {'WITH_RIVE_AUDIO'} + end + filter {'options:with_rive_audio=external'} + do + defines {'WITH_RIVE_AUDIO', 'EXTERNAL_RIVE_AUDIO_ENGINE', 'MA_NO_DEVICE_IO'} + end end newoption { @@ -164,6 +170,23 @@ } newoption { + trigger = 'with_rive_audio', + value = 'disabled', + description = 'The audio mode to use.', + allowed = { + { + 'disabled' + }, + { + 'system' + }, + { + 'external' + } + } +} + +newoption { trigger = 'variant', value = 'type', description = 'Choose a particular variant to build',
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp index 3c31b72..7e132d4 100644 --- a/src/animation/linear_animation_instance.cpp +++ b/src/animation/linear_animation_instance.cpp
@@ -35,7 +35,7 @@ bool LinearAnimationInstance::advanceAndApply(float seconds) { - bool more = this->advance(seconds); + bool more = this->advance(seconds, this); this->apply(); m_artboardInstance->advance(seconds); return more;
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 05fa3cb..e931676 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -22,8 +22,6 @@ #include "rive/nested_animation.hpp" #include "rive/nested_artboard.hpp" #include "rive/shapes/shape.hpp" -#include "rive/core/field_types/core_callback_type.hpp" -#include "rive/generated/core_registry.hpp" #include "rive/math/math_types.hpp" #include <unordered_map> @@ -681,15 +679,6 @@ return m_reportedEvents[index]; } -void StateMachineInstance::reportKeyedCallback(uint32_t objectId, - uint32_t propertyKey, - float elapsedSeconds) -{ - auto coreObject = m_artboardInstance->resolve(objectId); - CallbackData data(this, elapsedSeconds); - CoreRegistry::setCallback(coreObject, propertyKey, data); -} - void StateMachineInstance::notifyEventListeners(std::vector<EventReport> events, NestedArtboard* source) {
diff --git a/src/assets/audio_asset.cpp b/src/assets/audio_asset.cpp new file mode 100644 index 0000000..de59fae --- /dev/null +++ b/src/assets/audio_asset.cpp
@@ -0,0 +1,19 @@ +#include "rive/assets/audio_asset.hpp" +#include "rive/factory.hpp" + +using namespace rive; + +AudioAsset::AudioAsset() {} + +AudioAsset::~AudioAsset() {} + +bool AudioAsset::decode(SimpleArray<uint8_t>& bytes, Factory* factory) +{ +#ifdef WITH_RIVE_AUDIO + // Steal the bytes. + m_audioSource = rcp<AudioSource>(new AudioSource(std::move(bytes))); +#endif + return true; +} + +std::string AudioAsset::fileExtension() const { return "wav"; } \ No newline at end of file
diff --git a/src/assets/file_asset_contents.cpp b/src/assets/file_asset_contents.cpp index 829b709..fb90573 100644 --- a/src/assets/file_asset_contents.cpp +++ b/src/assets/file_asset_contents.cpp
@@ -19,7 +19,7 @@ void FileAssetContents::decodeBytes(Span<const uint8_t> value) { - m_Bytes = std::vector<uint8_t>(value.begin(), value.end()); + m_bytes = SimpleArray<uint8_t>(value.data(), value.size()); } void FileAssetContents::copyBytes(const FileAssetContentsBase& object) @@ -28,4 +28,4 @@ assert(false); } -Span<const uint8_t> FileAssetContents::bytes() const { return m_Bytes; } +SimpleArray<uint8_t>& FileAssetContents::bytes() { return m_bytes; }
diff --git a/src/assets/font_asset.cpp b/src/assets/font_asset.cpp index d87101d..e499b28 100644 --- a/src/assets/font_asset.cpp +++ b/src/assets/font_asset.cpp
@@ -6,7 +6,7 @@ using namespace rive; -bool FontAsset::decode(Span<const uint8_t> data, Factory* factory) +bool FontAsset::decode(SimpleArray<uint8_t>& data, Factory* factory) { font(factory->decodeFont(data)); return m_font != nullptr;
diff --git a/src/assets/image_asset.cpp b/src/assets/image_asset.cpp index 37f2ad2..cc59030 100644 --- a/src/assets/image_asset.cpp +++ b/src/assets/image_asset.cpp
@@ -6,7 +6,7 @@ ImageAsset::~ImageAsset() {} -bool ImageAsset::decode(Span<const uint8_t> data, Factory* factory) +bool ImageAsset::decode(SimpleArray<uint8_t>& data, Factory* factory) { #ifdef TESTING decodedByteSize = data.size();
diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp new file mode 100644 index 0000000..d99fb61 --- /dev/null +++ b/src/audio/audio_engine.cpp
@@ -0,0 +1,177 @@ +#ifdef WITH_RIVE_AUDIO +#ifdef __APPLE__ +#include <TargetConditionals.h> +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE +// Don't define MINIAUDIO_IMPLEMENTATION ON iOS +#elif TARGET_OS_MAC +#define MINIAUDIO_IMPLEMENTATION +#else +#error "Unknown Apple platform" +#endif +#else +#define MINIAUDIO_IMPLEMENTATION +#endif +#include "miniaudio.h" + +#include "rive/audio/audio_engine.hpp" +#include "rive/audio/audio_sound.hpp" +#include "rive/audio/audio_source.hpp" + +using namespace rive; + +void AudioEngine::SoundCompleted(void* pUserData, ma_sound* pSound) +{ + AudioSound* audioSound = (AudioSound*)pUserData; + audioSound->complete(); +} + +rcp<AudioEngine> AudioEngine::Make(uint32_t numChannels, uint32_t sampleRate) +{ + ma_engine_config engineConfig = ma_engine_config_init(); + engineConfig.channels = numChannels; + engineConfig.sampleRate = sampleRate; + +#ifdef EXTERNAL_RIVE_AUDIO_ENGINE + engineConfig.noDevice = MA_TRUE; +#endif + + ma_engine* engine = new ma_engine(); + + if (ma_engine_init(&engineConfig, engine) != MA_SUCCESS) + { + fprintf(stderr, "AudioEngine::Make - failed to init engine\n"); + delete engine; + return nullptr; + } + + return rcp<AudioEngine>(new AudioEngine(engine)); +} + +uint32_t AudioEngine::channels() const { return ma_engine_get_channels(m_engine); } +uint32_t AudioEngine::sampleRate() const { return ma_engine_get_sample_rate(m_engine); } + +AudioEngine::AudioEngine(ma_engine* engine) : + m_device(ma_engine_get_device(engine)), m_engine(engine) +{} + +rcp<AudioSound> AudioEngine::play(rcp<AudioSource> source, + uint64_t startTime, + uint64_t endTime, + uint64_t soundStartTime) +{ + purgeCompletedSounds(); + + rive::rcp<rive::AudioEngine> rcEngine = rive::rcp<rive::AudioEngine>(this); + rcEngine->ref(); + rcp<AudioSound> audioSound = rcp<AudioSound>(new AudioSound(rcEngine)); + if (source->isBuffered()) + { + rive::Span<float> samples = source->bufferedSamples(); + ma_audio_buffer_config config = + ma_audio_buffer_config_init(ma_format_f32, + source->channels(), + samples.size() / source->channels(), + (const void*)samples.data(), + nullptr); + if (ma_audio_buffer_init(&config, audioSound->buffer()) != MA_SUCCESS) + { + fprintf(stderr, "AudioSource::play - Failed to initialize audio buffer.\n"); + return nullptr; + } + if (ma_sound_init_from_data_source(m_engine, + audioSound->buffer(), + MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION, + nullptr, + audioSound->sound()) != MA_SUCCESS) + { + return nullptr; + } + } + else + { + ma_decoder_config config = ma_decoder_config_init(ma_format_f32, channels(), sampleRate()); + auto sourceBytes = source->bytes(); + if (ma_decoder_init_memory(sourceBytes.data(), + sourceBytes.size(), + &config, + audioSound->decoder()) != MA_SUCCESS) + { + fprintf(stderr, "AudioSource::play - Failed to initialize decoder.\n"); + return nullptr; + } + if (ma_sound_init_from_data_source(m_engine, + &audioSound->m_decoder, + MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION, + nullptr, + audioSound->sound()) != MA_SUCCESS) + { + return nullptr; + } + } + + if (soundStartTime != 0) + { + 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) + { + ma_sound_set_start_time_in_pcm_frames(audioSound->sound(), startTime); + } + if (endTime != 0) + { + ma_sound_set_stop_time_in_pcm_frames(audioSound->sound(), endTime); + } + 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) + { + sound->unref(); + } + m_completedSounds.clear(); +} + +AudioEngine::~AudioEngine() +{ + ma_engine_uninit(m_engine); + delete m_engine; +} + +uint64_t AudioEngine::timeInFrames() +{ + return (uint64_t)ma_engine_get_time_in_pcm_frames(m_engine); +} + +rcp<AudioEngine> AudioEngine::RuntimeEngine() +{ + static rcp<AudioEngine> engine = AudioEngine::Make(defaultNumChannels, defaultSampleRate); + return engine; +} + +#ifdef EXTERNAL_RIVE_AUDIO_ENGINE +bool AudioEngine::readAudioFrames(float* frames, uint64_t numFrames, uint64_t* framesRead) +{ + return ma_engine_read_pcm_frames(m_engine, + (void*)frames, + (ma_uint64)numFrames, + (ma_uint64*)framesRead) == MA_SUCCESS; +} +#endif + +#endif \ No newline at end of file
diff --git a/src/audio/audio_engine.m b/src/audio/audio_engine.m new file mode 100644 index 0000000..f272bea --- /dev/null +++ b/src/audio/audio_engine.m
@@ -0,0 +1,4 @@ +#ifdef WITH_RIVE_AUDIO +#define MINIAUDIO_IMPLEMENTATION +#include "miniaudio.h" +#endif \ No newline at end of file
diff --git a/src/audio/audio_reader.cpp b/src/audio/audio_reader.cpp new file mode 100644 index 0000000..bfc3d18 --- /dev/null +++ b/src/audio/audio_reader.cpp
@@ -0,0 +1,46 @@ +#ifdef WITH_RIVE_AUDIO +#include "rive/audio/audio_reader.hpp" +#include "rive/audio/audio_source.hpp" +#include "rive/audio/audio_engine.hpp" + +using namespace rive; + +AudioReader::AudioReader(rcp<AudioSource> audioSource, uint32_t channels) : + m_source(std::move(audioSource)), m_channels(channels), m_decoder({}) +{} + +AudioReader::~AudioReader() { ma_decoder_uninit(&m_decoder); } + +uint32_t AudioReader::channels() const { return m_channels; } +uint32_t AudioReader::sampleRate() const { return m_source->sampleRate(); } +ma_decoder* AudioReader::decoder() { return &m_decoder; } + +uint64_t AudioReader::lengthInFrames() +{ + ma_uint64 length; + + ma_result result = ma_data_source_get_length_in_pcm_frames(&m_decoder, &length); + if (result != MA_SUCCESS) + { + fprintf(stderr, + "AudioReader::lengthInFrames - audioSourceLength failed to determine length\n"); + return 0; + } + return (uint64_t)length; +} + +Span<float> AudioReader::read(uint64_t frameCount) +{ + m_readBuffer.resize(frameCount * m_channels); + + ma_uint64 framesRead; + if (ma_data_source_read_pcm_frames(&m_decoder, + m_readBuffer.data(), + (ma_uint64)frameCount, + &framesRead) != MA_SUCCESS) + { + return Span<float>(nullptr, 0); + } + return Span<float>(m_readBuffer.data(), framesRead * m_channels); +} +#endif \ No newline at end of file
diff --git a/src/audio/audio_sound.cpp b/src/audio/audio_sound.cpp new file mode 100644 index 0000000..21631f4 --- /dev/null +++ b/src/audio/audio_sound.cpp
@@ -0,0 +1,44 @@ +#ifdef WITH_RIVE_AUDIO +#include "rive/audio/audio_sound.hpp" +#include "rive/audio/audio_engine.hpp" +#include "rive/audio/audio_reader.hpp" +#include "rive/audio/audio_source.hpp" + +using namespace rive; + +AudioSound::AudioSound(rcp<AudioEngine> engine) : + m_engine(std::move(engine)), m_decoder({}), m_buffer({}), m_sound({}) +{} + +AudioSound::~AudioSound() +{ + ma_sound_uninit(&m_sound); + ma_decoder_uninit(&m_decoder); + ma_audio_buffer_uninit(&m_buffer); +} + +void AudioSound::stop(uint64_t fadeTimeInFrames) +{ + if (fadeTimeInFrames == 0) + { + ma_sound_stop(&m_sound); + } + else + { + ma_sound_stop_with_fade_in_pcm_frames(&m_sound, fadeTimeInFrames); + } +} + +void AudioSound::complete() +{ + auto sound = rcp<AudioSound>(this); + sound->ref(); + m_engine->completeSound(sound); +} + +bool AudioSound::seek(uint64_t timeInFrames) +{ + return ma_sound_seek_to_pcm_frame(&m_sound, (ma_uint64)timeInFrames) == MA_SUCCESS; +} + +#endif \ No newline at end of file
diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp new file mode 100644 index 0000000..8cc5b78 --- /dev/null +++ b/src/audio/audio_source.cpp
@@ -0,0 +1,150 @@ +#ifdef WITH_RIVE_AUDIO +#include "rive/audio/audio_source.hpp" +#include "rive/audio/audio_engine.hpp" +#include "rive/audio/audio_sound.hpp" +#include "rive/audio/audio_reader.hpp" +#include "rive/audio/audio_reader.hpp" + +using namespace rive; + +AudioSource::AudioSource(rive::Span<float> samples, uint32_t numChannels, uint32_t sampleRate) : + m_isBuffered(true), + m_channels(numChannels), + m_sampleRate(sampleRate), + m_ownedBytes((uint8_t*)samples.data(), samples.size() * sizeof(float)) +{ + assert(numChannels != 0); + assert(sampleRate != 0); +} + +AudioSource::AudioSource(rive::Span<uint8_t> fileBytes) : + m_isBuffered(false), m_channels(0), m_sampleRate(0), m_fileBytes(fileBytes) +{} + +AudioSource::AudioSource(rive::SimpleArray<uint8_t> fileBytes) : + m_isBuffered(false), + m_channels(0), + m_sampleRate(0), + m_fileBytes(fileBytes.data(), fileBytes.size()), + m_ownedBytes(std::move(fileBytes)) +{} + +const rive::Span<float> AudioSource::bufferedSamples() const +{ + assert(m_isBuffered); + return rive::Span<float>((float*)m_ownedBytes.data(), m_ownedBytes.size() / sizeof(float)); +} + +class AudioSourceDecoder +{ +public: + AudioSourceDecoder(rive::Span<uint8_t> fileBytes) : m_decoder({}) + { + ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0); + if (ma_decoder_init_memory(fileBytes.data(), fileBytes.size(), &config, &m_decoder) != + MA_SUCCESS) + { + fprintf(stderr, "AudioSourceDecoder - Failed to initialize decoder.\n"); + } + } + + ~AudioSourceDecoder() { ma_decoder_uninit(&m_decoder); } + + uint32_t channels() { return (uint32_t)m_decoder.outputChannels; } + + uint32_t sampleRate() { return (uint32_t)m_decoder.outputSampleRate; } + +private: + ma_decoder m_decoder; +}; + +uint32_t AudioSource::channels() +{ + if (m_channels != 0) + { + return m_channels; + } + AudioSourceDecoder audioDecoder(m_fileBytes); + return m_channels = audioDecoder.channels(); +} + +uint32_t AudioSource::sampleRate() +{ + if (m_sampleRate != 0) + { + return m_sampleRate; + } + AudioSourceDecoder audioDecoder(m_fileBytes); + return m_sampleRate = audioDecoder.sampleRate(); +} + +AudioFormat AudioSource::format() const +{ + if (m_isBuffered) + { + return AudioFormat::buffered; + } + ma_decoder decoder; + ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0); + if (ma_decoder_init_memory(m_fileBytes.data(), m_fileBytes.size(), &config, &decoder) != + MA_SUCCESS) + { + fprintf(stderr, "AudioSource::format - Failed to initialize decoder.\n"); + return AudioFormat::unknown; + } + ma_encoding_format encodingFormat; + + AudioFormat format = AudioFormat::unknown; + if (ma_decoder_get_encoding_format(&decoder, &encodingFormat) == MA_SUCCESS) + { + switch (encodingFormat) + { + case ma_encoding_format_mp3: + format = AudioFormat::mp3; + break; + case ma_encoding_format_wav: + format = AudioFormat::wav; + break; + case ma_encoding_format_vorbis: + format = AudioFormat::vorbis; + break; + case ma_encoding_format_flac: + format = AudioFormat::flac; + break; + default: + format = AudioFormat::unknown; + break; + } + } + + ma_decoder_uninit(&decoder); + + return format; +} + +rcp<AudioReader> AudioSource::makeReader(uint32_t numChannels, uint32_t sampleRate) +{ + if (m_isBuffered) + { + return nullptr; + } + + rive::rcp<rive::AudioSource> rcSource = rive::rcp<rive::AudioSource>(this); + rcSource->ref(); + auto reader = rcp<AudioReader>(new AudioReader(rcSource, numChannels)); + + ma_decoder_config config = ma_decoder_config_init(ma_format_f32, numChannels, sampleRate); + + if (ma_decoder_init_memory(m_fileBytes.data(), + m_fileBytes.size(), + &config, + reader->decoder()) != MA_SUCCESS) + { + fprintf(stderr, "AudioSource::makeReader - Failed to initialize decoder.\n"); + return nullptr; + } + + return reader; +} + +#endif \ No newline at end of file
diff --git a/src/audio_event.cpp b/src/audio_event.cpp new file mode 100644 index 0000000..3a6ead5 --- /dev/null +++ b/src/audio_event.cpp
@@ -0,0 +1,56 @@ +#include "rive/audio_event.hpp" +#include "rive/assets/audio_asset.hpp" +#include "rive/audio/audio_engine.hpp" +#include "rive/audio/audio_sound.hpp" + +using namespace rive; + +void AudioEvent::trigger(const CallbackData& value) +{ + Super::trigger(value); + +#ifdef WITH_RIVE_AUDIO + auto audioAsset = (AudioAsset*)m_fileAsset; + if (audioAsset == nullptr) + { + return; + } + auto audioSource = audioAsset->audioSource(); + if (audioSource == nullptr) + { + return; + } + auto engine = AudioEngine::RuntimeEngine(); + engine->play(audioSource, engine->timeInFrames(), 0, 0); +#endif +} + +StatusCode AudioEvent::import(ImportStack& importStack) +{ + auto result = registerReferencer(importStack); + if (result != StatusCode::Ok) + { + return result; + } + return Super::import(importStack); +} + +void AudioEvent::setAsset(FileAsset* asset) +{ + if (asset->is<AudioAsset>()) + { + FileAssetReferencer::setAsset(asset); + } +} + +Core* AudioEvent::clone() const +{ + AudioEvent* twin = AudioEventBase::clone()->as<AudioEvent>(); + if (m_fileAsset != nullptr) + { + twin->setAsset(m_fileAsset); + } + return twin; +} + +uint32_t AudioEvent::assetId() { return AudioEventBase::assetId(); } \ No newline at end of file
diff --git a/src/file.cpp b/src/file.cpp index a6dee57..183aa50 100644 --- a/src/file.cpp +++ b/src/file.cpp
@@ -27,6 +27,7 @@ #include "rive/animation/blend_state_1d.hpp" #include "rive/animation/blend_state_direct.hpp" #include "rive/assets/file_asset.hpp" +#include "rive/assets/audio_asset.hpp" #include "rive/assets/file_asset_contents.hpp" // Default namespace for Rive Cpp code @@ -211,6 +212,7 @@ break; case ImageAsset::typeKey: case FontAsset::typeKey: + case AudioAsset::typeKey: { auto fa = object->as<FileAsset>(); m_fileAssets.push_back(fa); @@ -288,6 +290,7 @@ break; case ImageAsset::typeKey: case FontAsset::typeKey: + case AudioAsset::typeKey: stackObject = new FileAssetImporter(object->as<FileAsset>(), m_assetLoader, m_factory); stackType = FileAsset::typeKey;
diff --git a/src/generated/assets/audio_asset_base.cpp b/src/generated/assets/audio_asset_base.cpp new file mode 100644 index 0000000..0b7504d --- /dev/null +++ b/src/generated/assets/audio_asset_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/assets/audio_asset_base.hpp" +#include "rive/assets/audio_asset.hpp" + +using namespace rive; + +Core* AudioAssetBase::clone() const +{ + auto cloned = new AudioAsset(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/generated/audio_event_base.cpp b/src/generated/audio_event_base.cpp new file mode 100644 index 0000000..83ef474 --- /dev/null +++ b/src/generated/audio_event_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/audio_event_base.hpp" +#include "rive/audio_event.hpp" + +using namespace rive; + +Core* AudioEventBase::clone() const +{ + auto cloned = new AudioEvent(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/importers/file_asset_importer.cpp b/src/importers/file_asset_importer.cpp index 935aad3..1165e0d 100644 --- a/src/importers/file_asset_importer.cpp +++ b/src/importers/file_asset_importer.cpp
@@ -24,7 +24,6 @@ StatusCode FileAssetImporter::resolve() { - Span<const uint8_t> bytes; if (m_Content != nullptr) { @@ -41,7 +40,7 @@ // If we do not, but we have found in band contents, load those else if (bytes.size() > 0) { - m_FileAsset->decode(bytes, m_Factory); + m_FileAsset->decode(m_Content->bytes(), m_Factory); } // Note that it's ok for an asset to not resolve (or to resolve async).
diff --git a/src/scene.cpp b/src/scene.cpp index e568553..2bf0a09 100644 --- a/src/scene.cpp +++ b/src/scene.cpp
@@ -1,6 +1,6 @@ #include "rive/artboard.hpp" #include "rive/scene.hpp" - +#include "rive/generated/core_registry.hpp" using namespace rive; Scene::Scene(ArtboardInstance* abi) : m_artboardInstance(abi) @@ -23,3 +23,10 @@ SMIBool* Scene::getBool(const std::string&) const { return nullptr; } SMINumber* Scene::getNumber(const std::string&) const { return nullptr; } SMITrigger* Scene::getTrigger(const std::string&) const { return nullptr; } + +void Scene::reportKeyedCallback(uint32_t objectId, uint32_t propertyKey, float elapsedSeconds) +{ + auto coreObject = m_artboardInstance->resolve(objectId); + CallbackData data(this, elapsedSeconds); + CoreRegistry::setCallback(coreObject, propertyKey, data); +}
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp index fac4a59..021f84b 100644 --- a/src/shapes/image.cpp +++ b/src/shapes/image.cpp
@@ -12,7 +12,6 @@ void Image::draw(Renderer* renderer) { - rive::ImageAsset* asset = this->imageAsset(); if (asset == nullptr || renderOpacity() == 0.0f) {
diff --git a/test/assets/audio/song.mp3 b/test/assets/audio/song.mp3 new file mode 100644 index 0000000..1a11620 --- /dev/null +++ b/test/assets/audio/song.mp3 Binary files differ
diff --git a/test/assets/audio/what.wav b/test/assets/audio/what.wav new file mode 100644 index 0000000..c108b6a --- /dev/null +++ b/test/assets/audio/what.wav Binary files differ
diff --git a/test/assets/sound.riv b/test/assets/sound.riv new file mode 100644 index 0000000..6178de5 --- /dev/null +++ b/test/assets/sound.riv Binary files differ
diff --git a/test/audio_test.cpp b/test/audio_test.cpp new file mode 100644 index 0000000..f62e844 --- /dev/null +++ b/test/audio_test.cpp
@@ -0,0 +1,86 @@ +#include "rive/audio/audio_engine.hpp" +#include "rive/audio/audio_source.hpp" +#include "rive/audio/audio_reader.hpp" +#include "rive/audio_event.hpp" +#include "rive/assets/audio_asset.hpp" +#include "rive_file_reader.hpp" +#include "catch.hpp" +#include <string> + +using namespace rive; + +TEST_CASE("audio engine initializes", "[audio]") +{ + rcp<AudioEngine> engine = AudioEngine::Make(2, 44100); + REQUIRE(engine != nullptr); +} + +static std::vector<uint8_t> loadFile(const char* filename) +{ + FILE* fp = fopen(filename, "rb"); + REQUIRE(fp != nullptr); + + fseek(fp, 0, SEEK_END); + const size_t length = ftell(fp); + fseek(fp, 0, SEEK_SET); + std::vector<uint8_t> bytes(length); + REQUIRE(fread(bytes.data(), 1, length, fp) == length); + fclose(fp); + + return bytes; +} + +TEST_CASE("audio source can be opened", "[audio]") +{ + rcp<AudioEngine> engine = AudioEngine::Make(2, 44100); + 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); + + // Try some different sample rates. + { + auto reader = audioSource.makeReader(2, 44100); + REQUIRE(reader != nullptr); + REQUIRE(reader->lengthInFrames() == 9688); + } + { + auto reader = audioSource.makeReader(1, 48000); + REQUIRE(reader != nullptr); + REQUIRE(reader->lengthInFrames() == 10544); + } + { + auto reader = audioSource.makeReader(2, 32000); + REQUIRE(reader != nullptr); + REQUIRE(reader->lengthInFrames() == 7029); + } +} + +TEST_CASE("file with audio loads correctly", "[text]") +{ + auto file = ReadRiveFile("../../test/assets/sound.riv"); + auto artboard = file->artboard(); + REQUIRE(artboard != nullptr); + + auto audioEvents = artboard->find<AudioEvent>(); + REQUIRE(audioEvents.size() == 1); + + auto audioEvent = audioEvents[0]; + REQUIRE(audioEvent->asset() != nullptr); + REQUIRE(audioEvent->asset()->hasAudioSource()); + + // auto textObjects = artboard->find<rive::Text>(); + // REQUIRE(textObjects.size() == 5); + + // auto styleObjects = artboard->find<rive::TextStyle>(); + // REQUIRE(styleObjects.size() == 13); + + // auto runObjects = artboard->find<rive::TextValueRun>(); + // REQUIRE(runObjects.size() == 22); + + // artboard->advance(0.0f); + // rive::NoOpRenderer renderer; + // artboard->draw(&renderer); +} \ No newline at end of file
diff --git a/viewer/build/macosx/build_viewer.sh b/viewer/build/macosx/build_viewer.sh index 9038efa..d47310d 100755 --- a/viewer/build/macosx/build_viewer.sh +++ b/viewer/build/macosx/build_viewer.sh
@@ -56,7 +56,7 @@ pushd .. -$PREMAKE --scripts=../../build --file=./premake5_viewer.lua gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools --with_rive_text +$PREMAKE --scripts=../../build --file=./premake5_viewer.lua gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools --with_rive_text --with_rive_audio=system for var in "$@"; do if [[ $var = "clean" ]]; then
diff --git a/viewer/build/premake5_viewer.lua b/viewer/build/premake5_viewer.lua index 6c06181..72be65b 100644 --- a/viewer/build/premake5_viewer.lua +++ b/viewer/build/premake5_viewer.lua
@@ -19,7 +19,6 @@ dofile(path.join(path.getabsolute(rive) .. '/build', 'premake5.lua')) end - dofile(path.join(path.getabsolute(rive) .. '/cg_renderer/build', 'premake5.lua')) project 'rive_viewer' @@ -34,7 +33,7 @@ targetdir('%{cfg.system}/bin/%{cfg.buildcfg}/' .. _OPTIONS.renderer .. '/' .. _OPTIONS.graphics) objdir('%{cfg.system}/obj/%{cfg.buildcfg}/' .. _OPTIONS.renderer .. '/' .. _OPTIONS.graphics) - defines {'WITH_RIVE_TEXT'} + defines {'WITH_RIVE_TEXT', 'WITH_RIVE_AUDIO'} includedirs { '../include', @@ -42,7 +41,8 @@ rive .. '/skia/renderer/include', -- for font backends dependencies, dependencies .. '/sokol', - dependencies .. '/imgui' + dependencies .. '/imgui', + miniaudio } links { @@ -116,7 +116,7 @@ do includedirs { rive_tess .. '/include', - rive .. '/decoders/include', + rive .. '/decoders/include' } defines { 'RIVE_RENDERER_TESS' @@ -225,7 +225,7 @@ } links { 'skia', - 'rive_skia_renderer', + 'rive_skia_renderer' } end