diff --git a/.rive_head b/.rive_head index fc477217..8ed3cc47 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -b098ad23a15cc7b11f144e407374c83d5db0fee0 +73bf11db39230e7a6d302da9a27414d35bcbee99 diff --git a/build.sh b/build.sh index 8b2f42ba..ec40d626 100755 --- a/build.sh +++ b/build.sh @@ -32,8 +32,8 @@ if [ "$OPTION" = 'help' ]; then 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 d90d30e1..d17c9996 100644 --- a/build/premake5.lua +++ b/build/premake5.lua @@ -12,9 +12,19 @@ do 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 @@ do includedirs { '../include', harfbuzz .. '/src', - sheenbidi .. '/Headers' + sheenbidi .. '/Headers', + miniaudio } files {'../src/**.cpp'} flags { - 'FatalCompileWarnings', + 'FatalCompileWarnings' } filter {'system:macosx'} @@ -43,12 +54,27 @@ do } 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 @@ do 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 @@ do 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 @@ do 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 @@ newoption { 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 00000000..aa9e5b28 --- /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 00000000..667cf270 --- /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 00000000..3ee2a479 --- /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 030865e2..0018775f 100644 --- a/dev/test/premake5.lua +++ b/dev/test/premake5.lua @@ -18,6 +18,7 @@ configurations {'debug'} 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 @@ do 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 @@ do '../../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 @@ do 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 ff5ea093..a7cb9f6f 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 2a4838b5..a9c5ad52 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp @@ -5,7 +5,7 @@ #include #include #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 @@ class EventReport float m_secondsDelay; }; -class StateMachineInstance : public Scene, public KeyedCallbackReporter +class StateMachineInstance : public Scene { friend class SMIInput; friend class KeyedProperty; @@ -110,7 +110,7 @@ class StateMachineInstance : public Scene, public KeyedCallbackReporter 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 @@ class StateMachineInstance : public Scene, public KeyedCallbackReporter /// 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 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 00000000..d1de305f --- /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&, Factory*) override; + std::string fileExtension() const override; + +#ifdef WITH_RIVE_AUDIO +#ifdef TESTING + bool hasAudioSource() { return m_audioSource != nullptr; } +#endif + + rcp audioSource() { return m_audioSource; } + +private: + rcp 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 f1d587b9..a8ed721e 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 namespace rive @@ -20,7 +21,7 @@ class FileAsset : public FileAssetBase void decodeCdnUuid(Span value) override; void copyCdnUuid(const FileAssetBase& object) override; - virtual bool decode(Span, Factory*) = 0; + virtual bool decode(SimpleArray&, Factory*) = 0; virtual std::string fileExtension() const = 0; StatusCode import(ImportStack& importStack) override; const std::vector fileAssetReferencers() diff --git a/include/rive/assets/file_asset_contents.hpp b/include/rive/assets/file_asset_contents.hpp index c0afb30a..67457c02 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 +#include "rive/simple_array.hpp" namespace rive { class FileAssetContents : public FileAssetContentsBase { -private: - std::vector m_Bytes; - public: - Span bytes() const; + SimpleArray& bytes(); StatusCode import(ImportStack& importStack) override; void decodeBytes(Span value) override; void copyBytes(const FileAssetContentsBase& object) override; + +private: + SimpleArray m_bytes; }; } // namespace rive diff --git a/include/rive/assets/font_asset.hpp b/include/rive/assets/font_asset.hpp index e39fb72b..29fad8d7 100644 --- a/include/rive/assets/font_asset.hpp +++ b/include/rive/assets/font_asset.hpp @@ -9,7 +9,7 @@ namespace rive class FontAsset : public FontAssetBase { public: - bool decode(Span, Factory*) override; + bool decode(SimpleArray&, Factory*) override; std::string fileExtension() const override; const rcp font() const { return m_font; } void font(rcp font); diff --git a/include/rive/assets/image_asset.hpp b/include/rive/assets/image_asset.hpp index 93243fe2..54a3856c 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 namespace rive @@ -19,7 +20,7 @@ class ImageAsset : public ImageAssetBase #ifdef TESTING std::size_t decodedByteSize = 0; #endif - bool decode(Span, Factory*) override; + bool decode(SimpleArray&, Factory*) override; std::string fileExtension() const override; RenderImage* renderImage() const { return m_RenderImage.get(); } void renderImage(rcp renderImage); diff --git a/include/rive/audio/audio_engine.hpp b/include/rive/audio/audio_engine.hpp new file mode 100644 index 00000000..b84a13fa --- /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 +#include +#include + +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 +{ + friend class AudioSound; + friend class AudioSource; + +public: + static const uint32_t defaultNumChannels = 2; + static const uint32_t defaultSampleRate = 48000; + + static rcp 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 play(rcp source, + uint64_t startTime, + uint64_t endTime, + uint64_t soundStartTime); + + static rcp 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> m_completedSounds; + + void completeSound(rcp 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 00000000..ab73fe54 --- /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 00000000..6243fd7f --- /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 + +namespace rive +{ +class AudioSource; +class AudioReader : public RefCnt +{ + friend class AudioSource; + +public: + uint64_t lengthInFrames(); + Span read(uint64_t frameCount); + ~AudioReader(); + uint32_t channels() const; + uint32_t sampleRate() const; + +private: + AudioReader(rcp audioSource, uint32_t channels); + ma_decoder* decoder(); + + rcp m_source; + uint32_t m_channels; + ma_decoder m_decoder; + std::vector 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 00000000..2a409f62 --- /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 +{ + friend class AudioEngine; + +public: + bool seek(uint64_t timeInFrames); + ~AudioSound(); + void stop(uint64_t fadeTimeInFrames = 0); + +private: + AudioSound(rcp engine); + void complete(); + + rcp 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 00000000..834741e0 --- /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 +{ +public: + // Makes an audio source that does not own it's backing bytes. + AudioSource(rive::Span fileBytes); + + // Makes an audio source whose backing bytes will be deleted when the + // AudioSource deletes. + AudioSource(rive::SimpleArray fileBytes); + + // Makes a buffered audio source whose backing bytes will be deleted when + // the AudioSource deletes. + AudioSource(rive::Span samples, uint32_t numChannels, uint32_t sampleRate); + + rcp makeReader(uint32_t numChannels, uint32_t sampleRate); + + uint32_t channels(); + uint32_t sampleRate(); + AudioFormat format() const; + const rive::Span bytes() const { return m_fileBytes; } + + const rive::Span bufferedSamples() const; + bool isBuffered() const { return m_isBuffered; } + +private: + bool m_isBuffered; + uint32_t m_channels; + uint32_t m_sampleRate; + rive::Span m_fileBytes; + rive::SimpleArray 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 00000000..cd67c6c5 --- /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 047aaa03..1c8ba627 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 00000000..6af1e4e8 --- /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 00000000..fece5634 --- /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 658b81b2..e8fe1081 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 @@ class CoreRegistry 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 @@ class CoreRegistry case FileAssetBase::assetIdPropertyKey: object->as()->assetId(value); break; + case AudioEventBase::assetIdPropertyKey: + object->as()->assetId(value); + break; } } static void setDouble(Core* object, int propertyKey, float value) @@ -1393,6 +1402,8 @@ class CoreRegistry return object->as()->styleId(); case FileAssetBase::assetIdPropertyKey: return object->as()->assetId(); + case AudioEventBase::assetIdPropertyKey: + return object->as()->assetId(); } return 0; } @@ -1837,6 +1848,7 @@ class CoreRegistry 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 f37271ae..6056c9c9 100644 --- a/include/rive/relative_local_asset_loader.hpp +++ b/include/rive/relative_local_asset_loader.hpp @@ -40,12 +40,11 @@ class RelativeLocalAssetLoader : public FileAssetLoader 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 bytes(length); + if (fread(bytes.data(), 1, length, fp) == length) { - asset.decode(Span(bytes, length), factory); + asset.decode(bytes, factory); } - delete[] bytes; return true; } }; diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp index dd4d6edc..21cb6066 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 namespace rive @@ -16,13 +18,13 @@ class SMIBool; 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 @@ class Scene 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 5a52b581..474b4916 100755 --- a/skia/renderer/build.sh +++ b/skia/renderer/build.sh @@ -42,7 +42,7 @@ if [ "$OPTION" = 'help' ]; then 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 7c1826f9..2376a520 100644 --- a/skia/renderer/build/premake5.lua +++ b/skia/renderer/build/premake5.lua @@ -35,10 +35,10 @@ do } flags { - 'FatalCompileWarnings', + 'FatalCompileWarnings' } - filter "system:windows" + filter 'system:windows' do architecture 'x64' defines {'_USE_MATH_DEFINES'} @@ -55,8 +55,7 @@ do 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 @@ do 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 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 { @@ -163,6 +169,23 @@ newoption { description = 'Enables text experiments' } +newoption { + trigger = 'with_rive_audio', + value = 'disabled', + description = 'The audio mode to use.', + allowed = { + { + 'disabled' + }, + { + 'system' + }, + { + 'external' + } + } +} + newoption { trigger = 'variant', value = 'type', diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp index 3c31b726..7e132d46 100644 --- a/src/animation/linear_animation_instance.cpp +++ b/src/animation/linear_animation_instance.cpp @@ -35,7 +35,7 @@ LinearAnimationInstance::~LinearAnimationInstance() {} 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 05fa3cb1..e9316768 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 @@ -681,15 +679,6 @@ const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const 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 events, NestedArtboard* source) { diff --git a/src/assets/audio_asset.cpp b/src/assets/audio_asset.cpp new file mode 100644 index 00000000..de59faea --- /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& bytes, Factory* factory) +{ +#ifdef WITH_RIVE_AUDIO + // Steal the bytes. + m_audioSource = rcp(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 829b709f..fb905730 100644 --- a/src/assets/file_asset_contents.cpp +++ b/src/assets/file_asset_contents.cpp @@ -19,7 +19,7 @@ StatusCode FileAssetContents::import(ImportStack& importStack) void FileAssetContents::decodeBytes(Span value) { - m_Bytes = std::vector(value.begin(), value.end()); + m_bytes = SimpleArray(value.data(), value.size()); } void FileAssetContents::copyBytes(const FileAssetContentsBase& object) @@ -28,4 +28,4 @@ void FileAssetContents::copyBytes(const FileAssetContentsBase& object) assert(false); } -Span FileAssetContents::bytes() const { return m_Bytes; } +SimpleArray& FileAssetContents::bytes() { return m_bytes; } diff --git a/src/assets/font_asset.cpp b/src/assets/font_asset.cpp index d87101dd..e499b281 100644 --- a/src/assets/font_asset.cpp +++ b/src/assets/font_asset.cpp @@ -6,7 +6,7 @@ using namespace rive; -bool FontAsset::decode(Span data, Factory* factory) +bool FontAsset::decode(SimpleArray& 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 37f2ad2c..cc590301 100644 --- a/src/assets/image_asset.cpp +++ b/src/assets/image_asset.cpp @@ -6,7 +6,7 @@ using namespace rive; ImageAsset::~ImageAsset() {} -bool ImageAsset::decode(Span data, Factory* factory) +bool ImageAsset::decode(SimpleArray& 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 00000000..d99fb617 --- /dev/null +++ b/src/audio/audio_engine.cpp @@ -0,0 +1,177 @@ +#ifdef WITH_RIVE_AUDIO +#ifdef __APPLE__ +#include +#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::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(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 AudioEngine::play(rcp source, + uint64_t startTime, + uint64_t endTime, + uint64_t soundStartTime) +{ + purgeCompletedSounds(); + + rive::rcp rcEngine = rive::rcp(this); + rcEngine->ref(); + rcp audioSound = rcp(new AudioSound(rcEngine)); + if (source->isBuffered()) + { + rive::Span 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 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::RuntimeEngine() +{ + static rcp 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 00000000..f272bea4 --- /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 00000000..bfc3d182 --- /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, 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 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(nullptr, 0); + } + return Span(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 00000000..21631f40 --- /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 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(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 00000000..8cc5b78e --- /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 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 fileBytes) : + m_isBuffered(false), m_channels(0), m_sampleRate(0), m_fileBytes(fileBytes) +{} + +AudioSource::AudioSource(rive::SimpleArray fileBytes) : + m_isBuffered(false), + m_channels(0), + m_sampleRate(0), + m_fileBytes(fileBytes.data(), fileBytes.size()), + m_ownedBytes(std::move(fileBytes)) +{} + +const rive::Span AudioSource::bufferedSamples() const +{ + assert(m_isBuffered); + return rive::Span((float*)m_ownedBytes.data(), m_ownedBytes.size() / sizeof(float)); +} + +class AudioSourceDecoder +{ +public: + AudioSourceDecoder(rive::Span 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 AudioSource::makeReader(uint32_t numChannels, uint32_t sampleRate) +{ + if (m_isBuffered) + { + return nullptr; + } + + rive::rcp rcSource = rive::rcp(this); + rcSource->ref(); + auto reader = rcp(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 00000000..3a6ead53 --- /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()) + { + FileAssetReferencer::setAsset(asset); + } +} + +Core* AudioEvent::clone() const +{ + AudioEvent* twin = AudioEventBase::clone()->as(); + 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 a6dee57c..183aa504 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 @@ ImportResult File::read(BinaryReader& reader, const RuntimeHeader& header) break; case ImageAsset::typeKey: case FontAsset::typeKey: + case AudioAsset::typeKey: { auto fa = object->as(); m_fileAssets.push_back(fa); @@ -288,6 +290,7 @@ ImportResult File::read(BinaryReader& reader, const RuntimeHeader& header) break; case ImageAsset::typeKey: case FontAsset::typeKey: + case AudioAsset::typeKey: stackObject = new FileAssetImporter(object->as(), 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 00000000..0b7504d4 --- /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 00000000..83ef4749 --- /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 935aad31..1165e0d9 100644 --- a/src/importers/file_asset_importer.cpp +++ b/src/importers/file_asset_importer.cpp @@ -24,7 +24,6 @@ void FileAssetImporter::onFileAssetContents(std::unique_ptr c StatusCode FileAssetImporter::resolve() { - Span bytes; if (m_Content != nullptr) { @@ -41,7 +40,7 @@ StatusCode FileAssetImporter::resolve() // 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 e568553e..2bf0a09a 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 @@ SMIInput* Scene::input(size_t index) const { return nullptr; } 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 fac4a595..021f84ba 100644 --- a/src/shapes/image.cpp +++ b/src/shapes/image.cpp @@ -12,7 +12,6 @@ using namespace rive; 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 00000000..1a116207 Binary files /dev/null and b/test/assets/audio/song.mp3 differ diff --git a/test/assets/audio/what.wav b/test/assets/audio/what.wav new file mode 100644 index 00000000..c108b6ad Binary files /dev/null and b/test/assets/audio/what.wav differ diff --git a/test/assets/sound.riv b/test/assets/sound.riv new file mode 100644 index 00000000..6178de5b Binary files /dev/null and b/test/assets/sound.riv differ diff --git a/test/audio_test.cpp b/test/audio_test.cpp new file mode 100644 index 00000000..f62e8446 --- /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 + +using namespace rive; + +TEST_CASE("audio engine initializes", "[audio]") +{ + rcp engine = AudioEngine::Make(2, 44100); + REQUIRE(engine != nullptr); +} + +static std::vector 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 bytes(length); + REQUIRE(fread(bytes.data(), 1, length, fp) == length); + fclose(fp); + + return bytes; +} + +TEST_CASE("audio source can be opened", "[audio]") +{ + rcp engine = AudioEngine::Make(2, 44100); + REQUIRE(engine != nullptr); + auto file = loadFile("../../test/assets/audio/what.wav"); + auto span = Span(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(); + REQUIRE(audioEvents.size() == 1); + + auto audioEvent = audioEvents[0]; + REQUIRE(audioEvent->asset() != nullptr); + REQUIRE(audioEvent->asset()->hasAudioSource()); + + // auto textObjects = artboard->find(); + // REQUIRE(textObjects.size() == 5); + + // auto styleObjects = artboard->find(); + // REQUIRE(styleObjects.size() == 13); + + // auto runObjects = artboard->find(); + // 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 9038efa8..d47310d6 100755 --- a/viewer/build/macosx/build_viewer.sh +++ b/viewer/build/macosx/build_viewer.sh @@ -56,7 +56,7 @@ export PREMAKE=$DEPENDENCIES/bin/premake5 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 6c061813..72be65b4 100644 --- a/viewer/build/premake5_viewer.lua +++ b/viewer/build/premake5_viewer.lua @@ -19,7 +19,6 @@ else 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 @@ do 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 @@ do rive .. '/skia/renderer/include', -- for font backends dependencies, dependencies .. '/sokol', - dependencies .. '/imgui' + dependencies .. '/imgui', + miniaudio } links { @@ -116,7 +116,7 @@ do do includedirs { rive_tess .. '/include', - rive .. '/decoders/include', + rive .. '/decoders/include' } defines { 'RIVE_RENDERER_TESS' @@ -225,7 +225,7 @@ do } links { 'skia', - 'rive_skia_renderer', + 'rive_skia_renderer' } end