diff --git a/Android/Pulsar/src/main/java/com/swmansion/pulsar/composers/PatternComposer.kt b/Android/Pulsar/src/main/java/com/swmansion/pulsar/composers/PatternComposer.kt index c4d9be87..371cd1df 100644 --- a/Android/Pulsar/src/main/java/com/swmansion/pulsar/composers/PatternComposer.kt +++ b/Android/Pulsar/src/main/java/com/swmansion/pulsar/composers/PatternComposer.kt @@ -26,12 +26,14 @@ class PatternComposer( private var vibrationEffect: VibrationEffect? = null private var audioBuffer: ByteArray? = null - private var soundPlayer: AudioHapticPlayer? = null + internal var soundPlayer: AudioHapticPlayer? = null + private set private var useCoupledHaptics = false /** [fromMs] starts the pattern that far into its own timeline. */ @JvmOverloads fun parsePattern(hapticsData: PatternData, fromMs: Long = 0L) { + releaseSound() val seekedPattern = PatternSeek.patternFrom(hapticsData, fromMs) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vibrationEffect = try { @@ -59,7 +61,6 @@ class PatternComposer( parsePattern(hapticsData, fromMs) val seekedSound = PatternSeek.soundFrom(sound, fromMs) - soundPlayer?.release() useCoupledHaptics = seekedSound.hapticChannels && isOggUri(seekedSound.uri) && engine.supportsAudioCoupledHaptics() @@ -100,8 +101,13 @@ class PatternComposer( } fun release() { + releaseSound() + } + + private fun releaseSound() { soundPlayer?.release() soundPlayer = null + useCoupledHaptics = false } private fun summarizePattern(hapticsData: PatternData): String { diff --git a/Android/Pulsar/src/test/java/com/swmansion/pulsar/composers/RobolectricPatternComposerSoundTest.kt b/Android/Pulsar/src/test/java/com/swmansion/pulsar/composers/RobolectricPatternComposerSoundTest.kt new file mode 100644 index 00000000..aa4f2ddb --- /dev/null +++ b/Android/Pulsar/src/test/java/com/swmansion/pulsar/composers/RobolectricPatternComposerSoundTest.kt @@ -0,0 +1,69 @@ +package com.swmansion.pulsar.composers + +import com.swmansion.pulsar.audio.AudioSimulator +import com.swmansion.pulsar.haptics.HapticEngineWrapper +import com.swmansion.pulsar.types.CompatibilityMode +import com.swmansion.pulsar.types.ContinuousPattern +import com.swmansion.pulsar.types.ConfigPoint +import com.swmansion.pulsar.types.PatternData +import com.swmansion.pulsar.types.SoundData +import com.swmansion.pulsar.types.ValuePoint +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class RobolectricPatternComposerSoundTest { + + private fun composer(): PatternComposer { + val app = RuntimeEnvironment.getApplication() + return PatternComposer(HapticEngineWrapper(app), AudioSimulator(CompatibilityMode.STANDARD_SUPPORT)) + } + + private fun pattern() = PatternData( + continuousPattern = ContinuousPattern( + amplitude = listOf(ValuePoint(0L, 1f), ValuePoint(100L, 0f)), + frequency = listOf(ValuePoint(0L, 0.5f)), + ), + discretePattern = listOf(ConfigPoint(0L, 1f, 0.3f)), + ) + + @Test + fun reparsingWithoutSoundDropsThePreviousSound() { + val composer = composer() + + composer.parsePatternWithSound(pattern(), SoundData(uri = "/pulsar-test-sound.wav")) + assertNotNull(composer.soundPlayer) + + composer.parsePattern(pattern()) + assertNull(composer.soundPlayer) + } + + @Test + fun reparsingWithSoundReplacesThePreviousSound() { + val composer = composer() + + composer.parsePatternWithSound(pattern(), SoundData(uri = "/pulsar-test-sound.wav")) + val first = composer.soundPlayer + + composer.parsePatternWithSound(pattern(), SoundData(uri = "/pulsar-other-sound.wav")) + + assertNotNull(composer.soundPlayer) + assert(composer.soundPlayer !== first) + } + + @Test + fun releaseDropsTheSound() { + val composer = composer() + + composer.parsePatternWithSound(pattern(), SoundData(uri = "/pulsar-test-sound.wav")) + composer.release() + + assertNull(composer.soundPlayer) + } +} diff --git a/flutter/pulsar/android/src/main/kotlin/com/swmansion/pulsar/flutter/PulsarPlugin.kt b/flutter/pulsar/android/src/main/kotlin/com/swmansion/pulsar/flutter/PulsarPlugin.kt index 938ded5a..71cc8410 100644 --- a/flutter/pulsar/android/src/main/kotlin/com/swmansion/pulsar/flutter/PulsarPlugin.kt +++ b/flutter/pulsar/android/src/main/kotlin/com/swmansion/pulsar/flutter/PulsarPlugin.kt @@ -46,7 +46,7 @@ class PulsarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { override fun onDetachedFromActivityForConfigChanges() { activity = null pulsar = null - patternComposers.clear() + releaseComposers() } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { @@ -57,6 +57,14 @@ class PulsarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { override fun onDetachedFromActivity() { activity = null pulsar = null + releaseComposers() + } + + private fun releaseComposers() { + patternComposers.values.forEach { + it.stop() + it.release() + } patternComposers.clear() } @@ -380,7 +388,10 @@ class PulsarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "PatternComposer_release" -> { val composerId = call.argument("composerId") ?: return result.error("INVALID_ARGS", "composerId required", null) - patternComposers.remove(composerId)?.stop() + patternComposers.remove(composerId)?.let { + it.stop() + it.release() + } result.success(null) } diff --git a/kmp/Pulsar/library/src/androidMain/kotlin/com/swmansion/pulsar/AndroidPulsarFactory.kt b/kmp/Pulsar/library/src/androidMain/kotlin/com/swmansion/pulsar/AndroidPulsarFactory.kt index 3446181e..cb0cd743 100644 --- a/kmp/Pulsar/library/src/androidMain/kotlin/com/swmansion/pulsar/AndroidPulsarFactory.kt +++ b/kmp/Pulsar/library/src/androidMain/kotlin/com/swmansion/pulsar/AndroidPulsarFactory.kt @@ -27,11 +27,11 @@ private class AndroidPulsarHandle( private val nativePulsar: AndroidPulsar, ) : PulsarPlatformHandle { private val presetsHandle by lazy { AndroidPresetsHandle(nativePulsar.getPresets()) } - private val patternComposerHandle by lazy { AndroidPatternComposerHandle(nativePulsar.getPatternComposer()) } override fun presets(): PulsarPresetsHandle = presetsHandle - override fun patternComposer(): PatternComposerHandle = patternComposerHandle + override fun patternComposer(): PatternComposerHandle = + AndroidPatternComposerHandle(nativePulsar.getPatternComposer()) override fun realtimeComposer(): RealtimeComposerHandle = AndroidRealtimeComposerHandle(nativePulsar.getRealtimeComposer()) @@ -362,6 +362,8 @@ private class AndroidPatternComposerHandle( override fun playAudioOnly() = composer.playAudioOnly() override fun stop() = composer.stop() + + override fun dispose() = composer.release() } private class AndroidRealtimeComposerHandle( diff --git a/kmp/Pulsar/library/src/iosMain/kotlin/com/swmansion/pulsar/iosimpl/composers/PatternComposer.kt b/kmp/Pulsar/library/src/iosMain/kotlin/com/swmansion/pulsar/iosimpl/composers/PatternComposer.kt index ff54f594..e02c4df6 100644 --- a/kmp/Pulsar/library/src/iosMain/kotlin/com/swmansion/pulsar/iosimpl/composers/PatternComposer.kt +++ b/kmp/Pulsar/library/src/iosMain/kotlin/com/swmansion/pulsar/iosimpl/composers/PatternComposer.kt @@ -53,12 +53,12 @@ internal class IOSPatternComposerHandle( private var audioResourceId: CHHapticAudioResourceID? = null override fun parsePattern(pattern: PatternData, fromMs: Long) { - releaseAudio() + releaseCurrentPattern() parse(PatternSeek.patternFrom(pattern, fromMs), audioEvent = null) } override fun parsePatternWithSound(pattern: PatternData, sound: SoundData, fromMs: Long) { - releaseAudio() + releaseCurrentPattern() parse( PatternSeek.patternFrom(pattern, fromMs), audioEvent = makeAudioEvent(PatternSeek.soundFrom(sound, fromMs)), @@ -182,7 +182,14 @@ internal class IOSPatternComposerHandle( }.onFailure { log("could not slice audio window: ${it.message}") }.getOrNull() } - private fun releaseAudio() { + private fun releaseCurrentPattern() { + continuousPlayerId?.let(engine::removePlayer) + discretePlayerId?.let(engine::removePlayer) + continuousPlayerId = null + discretePlayerId = null + continuousPattern = null + discretePattern = null + audioResourceId?.let { engine.unregisterAudioResource(it) } audioResourceId = null tempAudioURL?.path?.let { path -> @@ -223,14 +230,8 @@ internal class IOSPatternComposerHandle( override fun dispose() { stop() - continuousPlayerId?.let(engine::removePlayer) - discretePlayerId?.let(engine::removePlayer) - continuousPlayerId = null - discretePlayerId = null - continuousPattern = null - discretePattern = null + releaseCurrentPattern() audioBuffer = null hasSound = false - releaseAudio() } }