|  | #ifdef WITH_RIVE_AUDIO | 
|  | #include "rive/math/simd.hpp" | 
|  | #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" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <cmath> | 
|  |  | 
|  | using namespace rive; | 
|  |  | 
|  | void AudioEngine::SoundCompleted(void* pUserData, ma_sound* pSound) | 
|  | { | 
|  | AudioSound* audioSound = (AudioSound*)pUserData; | 
|  | auto engine = audioSound->m_engine; | 
|  | engine->soundCompleted(ref_rcp(audioSound)); | 
|  | } | 
|  |  | 
|  | void AudioEngine::unlinkSound(rcp<AudioSound> sound) | 
|  | { | 
|  | auto next = sound->m_nextPlaying; | 
|  | auto prev = sound->m_prevPlaying; | 
|  | if (next != nullptr) | 
|  | { | 
|  | next->m_prevPlaying = prev; | 
|  | } | 
|  | if (prev != nullptr) | 
|  | { | 
|  | prev->m_nextPlaying = next; | 
|  | } | 
|  |  | 
|  | if (m_playingSoundsHead == sound) | 
|  | { | 
|  | m_playingSoundsHead = next; | 
|  | } | 
|  |  | 
|  | sound->m_nextPlaying = nullptr; | 
|  | sound->m_prevPlaying = nullptr; | 
|  | } | 
|  |  | 
|  | void AudioEngine::soundCompleted(rcp<AudioSound> sound) | 
|  | { | 
|  | std::unique_lock<std::mutex> lock(m_mutex); | 
|  | m_completedSounds.push_back(sound); | 
|  | unlinkSound(sound); | 
|  | } | 
|  |  | 
|  | #ifdef WITH_RIVE_AUDIO_TOOLS | 
|  | namespace rive | 
|  | { | 
|  | class LevelsNode | 
|  | { | 
|  | public: | 
|  | ma_node_base base; | 
|  | AudioEngine* engine; | 
|  | static void measureLevels(ma_node* pNode, | 
|  | const float** ppFramesIn, | 
|  | ma_uint32* pFrameCountIn, | 
|  | float** ppFramesOut, | 
|  | ma_uint32* pFrameCountOut) | 
|  | { | 
|  | const float* frames = ppFramesIn[0]; | 
|  |  | 
|  | ma_uint32 frameCount = pFrameCountIn[0]; | 
|  |  | 
|  | static_cast<LevelsNode*>(pNode)->engine->measureLevels(frames, (uint32_t)frameCount); | 
|  | } | 
|  | }; | 
|  | } // namespace rive | 
|  |  | 
|  | void AudioEngine::measureLevels(const float* frames, uint32_t frameCount) | 
|  | { | 
|  | uint32_t channelCount = channels(); | 
|  |  | 
|  | for (uint32_t i = 0; i < frameCount; i++) | 
|  | { | 
|  | for (uint32_t c = 0; c < channelCount; c++) | 
|  | { | 
|  | float sample = *frames++; | 
|  | m_levels[c] = std::max(m_levels[c], sample); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static ma_node_vtable measure_levels_vtable = {LevelsNode::measureLevels, | 
|  | nullptr, | 
|  | 1, | 
|  | 1, | 
|  | MA_NODE_FLAG_PASSTHROUGH}; | 
|  |  | 
|  | void AudioEngine::initLevelMonitor() | 
|  | { | 
|  | if (m_levelMonitor == nullptr) | 
|  | { | 
|  | m_levelMonitor = new LevelsNode(); | 
|  | m_levelMonitor->engine = this; | 
|  |  | 
|  | ma_node_config nodeConfig = ma_node_config_init(); | 
|  | nodeConfig.vtable = &measure_levels_vtable; | 
|  | uint32_t channelCount = channels(); | 
|  | nodeConfig.pInputChannels = &channelCount; | 
|  | nodeConfig.pOutputChannels = &channelCount; | 
|  | m_levels.resize(channelCount); | 
|  |  | 
|  | auto graph = ma_engine_get_node_graph(m_engine); | 
|  | if (ma_node_init(graph, &nodeConfig, nullptr, &m_levelMonitor->base) != MA_SUCCESS) | 
|  | { | 
|  | delete m_levelMonitor; | 
|  | m_levelMonitor = nullptr; | 
|  | return; | 
|  | } | 
|  | if (ma_node_attach_output_bus(&m_levelMonitor->base, | 
|  | 0, | 
|  | ma_node_graph_get_endpoint(graph), | 
|  | 0) != MA_SUCCESS) | 
|  | { | 
|  | ma_node_uninit(&m_levelMonitor->base, nullptr); | 
|  | delete m_levelMonitor; | 
|  | m_levelMonitor = nullptr; | 
|  | return; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void AudioEngine::levels(Span<float> levels) | 
|  | { | 
|  | int size = std::min((int)m_levels.size(), (int)levels.size()); | 
|  | for (int i = 0; i < size; i++) | 
|  | { | 
|  | levels[i] = m_levels[i]; | 
|  | m_levels[i] = 0.0f; | 
|  | } | 
|  | } | 
|  |  | 
|  | float AudioEngine::level(uint32_t channel) | 
|  | { | 
|  | if (channel < m_levels.size()) | 
|  | { | 
|  | float value = m_levels[channel]; | 
|  | m_levels[channel] = 0.0f; | 
|  | return value; | 
|  | } | 
|  | return 0.0f; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | void AudioEngine::start() { ma_engine_start(m_engine); } | 
|  | void AudioEngine::stop() { ma_engine_stop(m_engine); } | 
|  |  | 
|  | rcp<AudioEngine> AudioEngine::Make(uint32_t numChannels, uint32_t sampleRate) | 
|  | { | 
|  | // I _think_ MA_NO_DEVICE_IO is defined when building for Unity; otherwise, it seems to pass | 
|  | // "standard" building When defined, pContext is unavailable, which causes build errors when | 
|  | // building Unity for iOS. - David | 
|  | #if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \ | 
|  | !defined(MA_NO_DEVICE_IO) | 
|  | // Used for configuration only, and isn't referenced past the usage of ma_context_init; thus, | 
|  | // can be locally scoped. Uses the "logical" defaults from miniaudio, and updates only what we | 
|  | // need. This should automatically set available backends in priority order based on the target | 
|  | // it's built for, which in the case of Apple is Core Audio first. | 
|  | ma_context_config contextConfig = ma_context_config_init(); | 
|  | contextConfig.coreaudio.sessionCategoryOptions = ma_ios_session_category_option_mix_with_others; | 
|  |  | 
|  | // We only need to initialize space for the context if we're targeting Apple platforms | 
|  | ma_context* context = (ma_context*)malloc(sizeof(ma_context)); | 
|  |  | 
|  | if (ma_context_init(NULL, 0, &contextConfig, context) != MA_SUCCESS) | 
|  | { | 
|  | free(context); | 
|  | context = nullptr; | 
|  | } | 
|  | #else | 
|  | ma_context* context = nullptr; | 
|  | #endif | 
|  |  | 
|  | ma_engine_config engineConfig = ma_engine_config_init(); | 
|  | engineConfig.channels = numChannels; | 
|  | engineConfig.sampleRate = sampleRate; | 
|  |  | 
|  | #if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \ | 
|  | !defined(MA_NO_DEVICE_IO) | 
|  | if (context != nullptr) | 
|  | { | 
|  | engineConfig.pContext = context; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #ifdef EXTERNAL_RIVE_AUDIO_ENGINE | 
|  | engineConfig.noDevice = MA_TRUE; | 
|  | #endif | 
|  |  | 
|  | ma_engine* engine = new ma_engine(); | 
|  |  | 
|  | if (ma_engine_init(&engineConfig, engine) != MA_SUCCESS) | 
|  | { | 
|  | #if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \ | 
|  | !defined(MA_NO_DEVICE_IO) | 
|  | if (context != nullptr) | 
|  | { | 
|  | ma_context_uninit(context); | 
|  | free(context); | 
|  | context = nullptr; | 
|  | } | 
|  | #endif | 
|  | fprintf(stderr, "AudioEngine::Make - failed to init engine\n"); | 
|  | delete engine; | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | return rcp<AudioEngine>(new AudioEngine(engine, context)); | 
|  | } | 
|  |  | 
|  | 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, ma_context* context) : | 
|  | m_device(ma_engine_get_device(engine)), m_engine(engine), m_context(context) | 
|  | {} | 
|  |  | 
|  | rcp<AudioSound> AudioEngine::play(rcp<AudioSource> source, | 
|  | uint64_t startTime, | 
|  | uint64_t endTime, | 
|  | uint64_t soundStartTime, | 
|  | Artboard* artboard) | 
|  | { | 
|  | if (endTime != 0 && startTime >= endTime) | 
|  | { | 
|  | // Requested to stop sound before start. | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | std::unique_lock<std::mutex> lock(m_mutex); | 
|  | // 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(); | 
|  |  | 
|  | rcp<AudioSound> audioSound = rcp<AudioSound>(new AudioSound(this, source, artboard)); | 
|  | if (source->isBuffered()) | 
|  | { | 
|  | rive::Span<float> samples = source->bufferedSamples(); | 
|  | ma_uint64 sizeInFrames = samples.size() / source->channels(); | 
|  | if (endTime != 0) | 
|  | { | 
|  | float durationSeconds = (soundStartTime + endTime - startTime) / (float)sampleRate(); | 
|  | ma_uint64 clippedFrames = (ma_uint64)std::round(durationSeconds * source->sampleRate()); | 
|  | if (clippedFrames < sizeInFrames) | 
|  | { | 
|  | sizeInFrames = clippedFrames; | 
|  | } | 
|  | } | 
|  | ma_audio_buffer_config config = ma_audio_buffer_config_init(ma_format_f32, | 
|  | source->channels(), | 
|  | sizeInFrames, | 
|  | (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 | 
|  | { | 
|  | // We wrapped the miniaudio decoder with a custom data source "Clipped | 
|  | // Decoder" which lets us ensure that the end callback for the sound is | 
|  | // called when we reach the end of the clip. This won't happen when | 
|  | // using ma_sound_set_stop_time_in_pcm_frames(audioSound->sound(), | 
|  | // endTime); as this keeps the sound playing/ready to fade back in. | 
|  | auto clip = audioSound->clippedDecoder(); | 
|  | 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, | 
|  | &clip->decoder) != MA_SUCCESS) | 
|  | { | 
|  | fprintf(stderr, "AudioSource::play - Failed to initialize decoder.\n"); | 
|  | return nullptr; | 
|  | } | 
|  | clip->frameCursor = 0; | 
|  | clip->endFrame = endTime == 0 ? std::numeric_limits<uint64_t>::max() | 
|  | : soundStartTime + endTime - startTime; | 
|  | ma_data_source_config baseConfig = ma_data_source_config_init(); | 
|  | baseConfig.vtable = &g_ma_end_clipped_decoder_vtable; | 
|  | if (ma_data_source_init(&baseConfig, &clip->base) != MA_SUCCESS) | 
|  | { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | if (ma_sound_init_from_data_source(m_engine, | 
|  | audioSound->clippedDecoder(), | 
|  | MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION, | 
|  | nullptr, | 
|  | audioSound->sound()) != MA_SUCCESS) | 
|  | { | 
|  | return nullptr; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (soundStartTime != 0) | 
|  | { | 
|  | audioSound->seek(soundStartTime); | 
|  | } | 
|  |  | 
|  | ma_sound_set_end_callback(audioSound->sound(), SoundCompleted, audioSound.get()); | 
|  |  | 
|  | if (startTime != 0) | 
|  | { | 
|  | ma_sound_set_start_time_in_pcm_frames(audioSound->sound(), startTime); | 
|  | } | 
|  | #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; | 
|  | } | 
|  |  | 
|  | #ifdef TESTING | 
|  | size_t AudioEngine::playingSoundCount() | 
|  | { | 
|  | std::unique_lock<std::mutex> lock(m_mutex); | 
|  | size_t count = 0; | 
|  | auto sound = m_playingSoundsHead; | 
|  | while (sound != nullptr) | 
|  | { | 
|  | count++; | 
|  | sound = sound->m_nextPlaying; | 
|  | } | 
|  |  | 
|  | return count; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | void AudioEngine::stop(Artboard* artboard) | 
|  | { | 
|  | std::unique_lock<std::mutex> lock(m_mutex); | 
|  | auto sound = m_playingSoundsHead; | 
|  | while (sound != nullptr) | 
|  | { | 
|  | auto next = sound->m_nextPlaying; | 
|  | if (sound->m_artboard == artboard) | 
|  | { | 
|  | sound->stop(); | 
|  | m_completedSounds.push_back(sound); | 
|  | unlinkSound(sound); | 
|  | } | 
|  | sound = next; | 
|  | } | 
|  | } | 
|  |  | 
|  | AudioEngine::~AudioEngine() | 
|  | { | 
|  | auto sound = m_playingSoundsHead; | 
|  | while (sound != nullptr) | 
|  | { | 
|  | sound->dispose(); | 
|  |  | 
|  | auto next = sound->m_nextPlaying; | 
|  | sound->m_nextPlaying = nullptr; | 
|  | sound->m_prevPlaying = nullptr; | 
|  | sound = next; | 
|  | } | 
|  |  | 
|  | for (auto sound : m_completedSounds) | 
|  | { | 
|  | sound->dispose(); | 
|  | } | 
|  | m_completedSounds.clear(); | 
|  |  | 
|  | #if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \ | 
|  | !defined(MA_NO_DEVICE_IO) | 
|  | // m_context is only set when Core Audio is available | 
|  | if (m_context != nullptr) | 
|  | { | 
|  | ma_context_uninit(m_context); | 
|  | free(m_context); | 
|  | m_context = nullptr; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | 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() | 
|  | { | 
|  | return (uint64_t)ma_engine_get_time_in_pcm_frames(m_engine); | 
|  | } | 
|  |  | 
|  | static rcp<AudioEngine> m_runtimeAudioEngine; | 
|  | rcp<AudioEngine> AudioEngine::RuntimeEngine(bool makeWhenNecessary) | 
|  | { | 
|  | if (!makeWhenNecessary) | 
|  | { | 
|  | return m_runtimeAudioEngine; | 
|  | } | 
|  | else if (m_runtimeAudioEngine == nullptr) | 
|  | { | 
|  | m_runtimeAudioEngine = AudioEngine::Make(defaultNumChannels, defaultSampleRate); | 
|  | } | 
|  | return m_runtimeAudioEngine; | 
|  | } | 
|  |  | 
|  | #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; | 
|  | } | 
|  | bool AudioEngine::sumAudioFrames(float* frames, uint64_t numFrames) | 
|  | { | 
|  | size_t numChannels = (size_t)channels(); | 
|  | size_t count = (size_t)numFrames * numChannels; | 
|  |  | 
|  | if (m_readFrames.size() < count) | 
|  | { | 
|  | m_readFrames.resize(count); | 
|  | } | 
|  | ma_uint64 framesRead = 0; | 
|  | if (ma_engine_read_pcm_frames(m_engine, | 
|  | (void*)m_readFrames.data(), | 
|  | (ma_uint64)numFrames, | 
|  | &framesRead) != MA_SUCCESS) | 
|  | { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | count = framesRead * numChannels; | 
|  |  | 
|  | const size_t alignedCount = count - count % 4; | 
|  | float* src = m_readFrames.data(); | 
|  | float* dst = frames; | 
|  | float* srcEnd = src + alignedCount; | 
|  |  | 
|  | while (src != srcEnd) | 
|  | { | 
|  | float4 sum = simd::load4f(src) + simd::load4f(dst); | 
|  | simd::store(dst, sum); | 
|  |  | 
|  | src += 4; | 
|  | dst += 4; | 
|  | } | 
|  | for (size_t i = alignedCount; i < count; i++) | 
|  | { | 
|  | frames[i] += m_readFrames[i]; | 
|  | } | 
|  | return true; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #endif |