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