diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java index 9aa3eb699..629e22303 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java @@ -619,7 +619,7 @@ public class AnimationStateTests { } }); - setup("setAnimation twice with mixing", // 22 + setup("setAnimation twice with multiple mixing", // 22 expect(0, "start", 0, 0), // expect(0, "interrupt", 0, 0), // expect(0, "end", 0, 0), // @@ -651,85 +651,20 @@ public class AnimationStateTests { expect(0, "start", 0, 0.4f), // expect(0, "event 0", 0.1f, 0.5f), // - - expect(2, "end", 0.3f, 0.6f), // - expect(2, "dispose", 0.3f, 0.6f), // - - expect(0, "event 14", 0.5f, 0.9f), // - - expect(1, "complete", 1, 1), // - expect(1, "end", 1, 1.1f), // - expect(1, "dispose", 1, 1.1f), // - - expect(0, "event 30", 1, 1.4f), // - expect(0, "complete", 1, 1.4f), // - expect(0, "end", 1, 1.5f), // - expect(0, "dispose", 1, 1.5f) // - ); - stateData.setDefaultMix(0.6f); - state.setAnimation(0, "events0", false); // First should be ignored. - state.setAnimation(0, "events1", false); - run(0.1f, 1000, new TestListener() { - public void frame (float time) { - if (MathUtils.isEqual(time, 0.2f)) { - state.setAnimation(0, "events0", false); // First should be ignored. - state.setAnimation(0, "events2", false); - } - if (MathUtils.isEqual(time, 0.4f)) { - state.setAnimation(0, "events1", false); // First should be ignored. - state.setAnimation(0, "events0", false).setTrackEnd(1); - } - } - }); - - setup("setAnimation twice with multiple mixing", // 23 - expect(0, "start", 0, 0), // - expect(0, "interrupt", 0, 0), // - expect(0, "end", 0, 0), // - expect(0, "dispose", 0, 0), // - - expect(1, "start", 0, 0), // - expect(1, "event 0", 0, 0), // - - note("First 2 setAnimation calls are done."), - - expect(1, "interrupt", 0.2f, 0.2f), // - - expect(0, "start", 0, 0.2f), // - expect(0, "interrupt", 0, 0.2f), // - expect(0, "end", 0, 0.2f), // - expect(0, "dispose", 0, 0.2f), // - - expect(2, "start", 0, 0.2f), // - expect(2, "event 0", 0.1f, 0.3f), // - - note("Second 2 setAnimation calls are done."), - - expect(2, "interrupt", 0.2f, 0.4f), // - - expect(1, "start", 0, 0.4f), // - expect(1, "interrupt", 0, 0.4f), // - expect(1, "end", 0, 0.4f), // - expect(1, "dispose", 0, 0.4f), // - - expect(0, "start", 0, 0.4f), // - expect(0, "event 0", 0.1f, 0.5f), // - - expect(1, "end", 0.8f, 0.9f), // - expect(1, "dispose", 0.8f, 0.9f), // - expect(0, "event 14", 0.5f, 0.9f), // expect(2, "end", 0.8f, 1.1f), // expect(2, "dispose", 0.8f, 1.1f), // + expect(1, "end", 0.8f, 1.1f), // + expect(1, "dispose", 0.8f, 1.1f), // + expect(0, "event 30", 1, 1.4f), // expect(0, "complete", 1, 1.4f), // expect(0, "end", 1, 1.5f), // expect(0, "dispose", 1, 1.5f) // ); stateData.setDefaultMix(0.6f); - state.setMultipleMixing(true); state.setAnimation(0, "events0", false); // First should be ignored. state.setAnimation(0, "events1", false); run(0.1f, 1000, new TestListener() { @@ -744,9 +679,8 @@ public class AnimationStateTests { } } }); - state.setMultipleMixing(false); - setup("addAnimation with delay on empty track", // 24 + setup("addAnimation with delay on empty track", // 23 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 5), // expect(0, "event 14", 0.5f, 5.5f), // @@ -758,7 +692,7 @@ public class AnimationStateTests { state.addAnimation(0, "events0", false, 5).setTrackEnd(1); run(0.1f, 10, null); - setup("setAnimation during AnimationStateListener"); // 25 + setup("setAnimation during AnimationStateListener"); // 24 state.addListener(new AnimationStateListener() { public void start (TrackEntry entry) { if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false); @@ -789,7 +723,7 @@ public class AnimationStateTests { state.setAnimation(1, "events1", false).setTrackEnd(1); run(0.1f, 10, null); - setup("clearTrack", // 26 + setup("clearTrack", // 25 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // @@ -803,7 +737,7 @@ public class AnimationStateTests { } }); - setup("setEmptyAnimation", // 27 + setup("setEmptyAnimation", // 26 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // @@ -825,7 +759,7 @@ public class AnimationStateTests { } }); - setup("TrackEntry listener"); // 28 + setup("TrackEntry listener"); // 27 final AtomicInteger counter = new AtomicInteger(); state.addAnimation(0, "events0", false, 0).setListener(new AnimationStateListener() { public void start (TrackEntry entry) { diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index 1e96856f4..256d0a014 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -33,8 +33,8 @@ package com.esotericsoftware.spine; import static com.esotericsoftware.spine.Animation.RotateTimeline.*; import com.badlogic.gdx.utils.Array; -import com.badlogic.gdx.utils.BooleanArray; import com.badlogic.gdx.utils.FloatArray; +import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.IntSet; import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pool.Poolable; @@ -49,6 +49,7 @@ import com.esotericsoftware.spine.Animation.Timeline; * See Applying Animations in the Spine Runtimes Guide. */ public class AnimationState { static private final Animation emptyAnimation = new Animation("", new Array(0), 0); + static private final int SUBSEQUENT = 0, FIRST = 1, DIP = 2; private AnimationStateData data; final Array tracks = new Array(); @@ -56,7 +57,7 @@ public class AnimationState { final Array listeners = new Array(); private final EventQueue queue = new EventQueue(); private final IntSet propertyIDs = new IntSet(); - boolean animationsChanged, multipleMixing; + boolean animationsChanged; private float timeScale = 1; Pool trackEntryPool = new Pool() { @@ -93,7 +94,7 @@ public class AnimationState { current.delay = 0; } - TrackEntry next = current.next; + TrackEntry from = current.mixingFrom, next = current.next; if (next != null) { // When the next entry's delay is passed, change to the next entry, preserving leftover time. float nextTime = current.trackLast - next.delay; @@ -108,14 +109,21 @@ public class AnimationState { } continue; } - } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + } else if (current.trackLast >= current.trackEnd && from == null) { // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. tracks.set(i, null); queue.end(current); disposeNext(current); continue; } - updateMixingFrom(current, delta); + if (from != null && updateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + do { + queue.end(from); + from = from.mixingFrom; + } while (from != null); + current.mixingFrom = null; + } current.trackTime += currentDelta; } @@ -123,22 +131,19 @@ public class AnimationState { queue.drain(); } - private void updateMixingFrom (TrackEntry entry, float delta) { + /** Returns true when all mixing from entries are complete. */ + private boolean updateMixingFrom (TrackEntry entry, float delta) { TrackEntry from = entry.mixingFrom; - if (from == null) return; + if (from == null) return true; - updateMixingFrom(from, delta); - - if (entry.mixTime >= entry.mixDuration && from.mixingFrom == null && entry.mixTime > 0) { - entry.mixingFrom = null; - queue.end(from); - return; - } + boolean finished = updateMixingFrom(from, delta); + if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) return finished; from.animationLast = from.nextAnimationLast; from.trackLast = from.nextTrackLast; from.trackTime += delta * from.timeScale; entry.mixTime += delta * entry.timeScale; + return false; } /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the @@ -156,7 +161,7 @@ public class AnimationState { // Apply mixing from entries first. float mix = current.alpha; if (current.mixingFrom != null) - mix *= applyMixingFrom(current, skeleton); + mix *= applyMixingFrom(current, skeleton, 0); else if (current.trackTime >= current.trackEnd && current.next == null) // mix = 0; // Set to setup pose the last time the entry will be applied. @@ -172,14 +177,14 @@ public class AnimationState { if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1); float[] timelinesRotation = current.timelinesRotation.items; - boolean[] timelinesFirst = current.timelinesFirst.items; + int[] timelineData = current.timelineData.items; for (int ii = 0; ii < timelineCount; ii++) { Timeline timeline = (Timeline)timelines[ii]; if (timeline instanceof RotateTimeline) { - applyRotateTimeline(timeline, skeleton, animationTime, mix, timelinesFirst[ii], timelinesRotation, ii << 1, + applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineData[ii] > 0, timelinesRotation, ii << 1, firstFrame); } else - timeline.apply(skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false); + timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineData[ii] > 0, false); } } queueEvents(current, animationTime); @@ -191,25 +196,25 @@ public class AnimationState { queue.drain(); } - private float applyMixingFrom (TrackEntry entry, Skeleton skeleton) { - TrackEntry from = entry.mixingFrom; - if (from.mixingFrom != null) applyMixingFrom(from, skeleton); - + private float applyMixingFrom (TrackEntry to, Skeleton skeleton, float parentMix) { float mix; - if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes. + if (to.mixDuration == 0) // Single frame mix to undo mixingFrom changes. mix = 1; else { - mix = entry.mixTime / entry.mixDuration; + mix = to.mixTime / to.mixDuration; if (mix > 1) mix = 1; } + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) applyMixingFrom(from, skeleton, mix); + Array events = mix < from.eventThreshold ? this.events : null; boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; float animationLast = from.animationLast, animationTime = from.getAnimationTime(); int timelineCount = from.animation.timelines.size; Object[] timelines = from.animation.timelines.items; - boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = multipleMixing ? null : from.timelinesLast.items; - float alphaBase = from.alpha * entry.mixAlpha, alphaMix = alphaBase * (1 - mix); + int[] timelineData = from.timelineData.items; + float alphaMix = from.alpha * to.mixAlpha * (1 - mix), alphaDip = from.alpha * to.mixAlpha * (1 - parentMix); boolean firstFrame = from.timelinesRotation.size == 0; if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); @@ -217,20 +222,21 @@ public class AnimationState { for (int i = 0; i < timelineCount; i++) { Timeline timeline = (Timeline)timelines[i]; - boolean setupPose = timelinesFirst[i]; - float alpha = timelinesLast != null && setupPose && !timelinesLast[i] ? alphaBase : alphaMix; + int data = timelineData[i]; + boolean first = data > 0; + float alpha = data == DIP ? alphaDip : alphaMix; if (timeline instanceof RotateTimeline) - applyRotateTimeline(timeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame); + applyRotateTimeline(timeline, skeleton, animationTime, alpha, first, timelinesRotation, i << 1, firstFrame); else { - if (!setupPose) { + if (!first) { if (!attachments && timeline instanceof AttachmentTimeline) continue; if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; } - timeline.apply(skeleton, animationLast, animationTime, events, alpha, setupPose, true); + timeline.apply(skeleton, animationLast, animationTime, events, alpha, first, true); } } - if (entry.mixDuration > 0) queueEvents(from, animationTime); + if (to.mixDuration > 0) queueEvents(from, animationTime); this.events.clear(); from.nextAnimationLast = animationTime; from.nextTrackLast = from.trackTime; @@ -382,29 +388,7 @@ public class AnimationState { if (interrupt) queue.interrupt(from); current.mixingFrom = from; current.mixTime = 0; - - TrackEntry mixingFrom = from.mixingFrom; - if (mixingFrom != null && from.mixDuration > 0) { - if (multipleMixing) { - // The interrupted mix will mix out from its current percentage to zero. - current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1); - } else { - // A mix was interrupted, mix from the closest animation. - if (from.mixTime / from.mixDuration < 0.5f && mixingFrom.animation != emptyAnimation) { - current.mixingFrom = mixingFrom; - mixingFrom.mixingFrom = from; - mixingFrom.mixTime = from.mixDuration - from.mixTime; - mixingFrom.mixDuration = from.mixDuration; - from.mixingFrom = null; - from = mixingFrom; - } - - // End the other animation after it is applied one last time. - from.mixAlpha = 0; - from.mixTime = 0; - from.mixDuration = 0; - } - } + current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1); // Store interrupted mix percentage. from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in. } @@ -583,83 +567,19 @@ public class AnimationState { private void animationsChanged () { animationsChanged = false; + // Set timelinesData for all entries, from lowest track to highest. IntSet propertyIDs = this.propertyIDs; - - // Set timelinesFirst for all entries, from lowest track to highest. - int i = 0, n = tracks.size; propertyIDs.clear(); - for (; i < n; i++) { // Find first non-null entry. + TrackEntry lastEntry = null; + for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry entry = tracks.get(i); - if (entry == null) continue; - setTimelinesFirst(entry); - i++; - break; - } - for (; i < n; i++) { // Rest of entries. - TrackEntry entry = tracks.get(i); - if (entry != null) checkTimelinesFirst(entry); - } - - if (multipleMixing) return; - - // Set timelinesLast for mixingFrom entries, from highest track to lowest that has mixingFrom. - propertyIDs.clear(); - int lowestMixingFrom = n; - for (i = 0; i < n; i++) { // Find lowest track with a mixingFrom entry. - TrackEntry entry = tracks.get(i); - if (entry == null || entry.mixingFrom == null) continue; - lowestMixingFrom = i; - break; - } - for (i = n - 1; i >= lowestMixingFrom; i--) { // Find first non-null entry. - TrackEntry entry = tracks.get(i); - if (entry == null) continue; - - // Store properties for non-mixingFrom entry but don't set timelinesLast, which is only used for mixingFrom entries. - Object[] timelines = entry.animation.timelines.items; - for (int ii = 0, nn = entry.animation.timelines.size; ii < nn; ii++) - propertyIDs.add(((Timeline)timelines[ii]).getPropertyId()); - - entry = entry.mixingFrom; - while (entry != null) { - checkTimelinesUsage(entry, entry.timelinesLast); - entry = entry.mixingFrom; + if (entry != null) { + entry.setTimelineData(lastEntry, propertyIDs); + lastEntry = entry; } } } - /** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */ - private void setTimelinesFirst (TrackEntry entry) { - if (entry.mixingFrom != null) { - setTimelinesFirst(entry.mixingFrom); - checkTimelinesUsage(entry, entry.timelinesFirst); - return; - } - IntSet propertyIDs = this.propertyIDs; - int n = entry.animation.timelines.size; - Object[] timelines = entry.animation.timelines.items; - boolean[] usage = entry.timelinesFirst.setSize(n); - for (int i = 0; i < n; i++) { - propertyIDs.add(((Timeline)timelines[i]).getPropertyId()); - usage[i] = true; - } - } - - /** From last to first mixingFrom entries, calls checkTimelineUsage. */ - private void checkTimelinesFirst (TrackEntry entry) { - if (entry.mixingFrom != null) checkTimelinesFirst(entry.mixingFrom); - checkTimelinesUsage(entry, entry.timelinesFirst); - } - - private void checkTimelinesUsage (TrackEntry entry, BooleanArray usageArray) { - IntSet propertyIDs = this.propertyIDs; - int n = entry.animation.timelines.size; - Object[] timelines = entry.animation.timelines.items; - boolean[] usage = usageArray.setSize(n); - for (int i = 0; i < n; i++) - usage[i] = propertyIDs.add(((Timeline)timelines[i]).getPropertyId()); - } - /** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */ public TrackEntry getCurrent (int trackIndex) { if (trackIndex >= tracks.size) return null; @@ -701,25 +621,6 @@ public class AnimationState { this.timeScale = timeScale; } - /** When false, only two animations can be mixed at once. Interrupting a mix by setting a new animation will discard one of the - * two old animations, keeping the one closest to being fully mixed in. Discarding an animation in this way may cause keyed - * values to jump. - *

- * When true, any number of animations can be mixed at once without causing keyed values to jump. Mixing is done by mixing out - * one or more old animations while mixing in the newest one. When animations key the same value, this may cause "dipping", - * where the value moves toward the setup pose as the old animations mix out, then back to the keyed value as the new animation - * mixes in. - *

- * Defaults to false. */ - public boolean getMultipleMixing () { - return multipleMixing; - } - - public void setMultipleMixing (boolean multipleMixing) { - this.multipleMixing = multipleMixing; - animationsChanged = true; - } - /** The AnimationStateData to look up mix durations. */ public AnimationStateData getData () { return data; @@ -760,7 +661,7 @@ public class AnimationState { float animationStart, animationEnd, animationLast, nextAnimationLast; float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale; float alpha, mixTime, mixDuration, mixAlpha; - final BooleanArray timelinesFirst = new BooleanArray(), timelinesLast = new BooleanArray(); + final IntArray timelineData = new IntArray(); final FloatArray timelinesRotation = new FloatArray(); public void reset () { @@ -768,11 +669,33 @@ public class AnimationState { mixingFrom = null; animation = null; listener = null; - timelinesFirst.clear(); - timelinesLast.clear(); + timelineData.clear(); timelinesRotation.clear(); } + TrackEntry setTimelineData (TrackEntry parent, IntSet propertyIDs) { + TrackEntry lastEntry = mixingFrom != null ? mixingFrom.setTimelineData(this, propertyIDs) : this; + int n = animation.timelines.size; + Object[] timelines = animation.timelines.items; + int[] timelineData = this.timelineData.setSize(n << 1); + for (int i = 0; i < n; i++) { + int id = ((Timeline)timelines[i]).getPropertyId(); + boolean first = propertyIDs.add(id); + if (first && parent != null && parent.hasTimeline(id)) + timelineData[i] = DIP; + else + timelineData[i] = first ? FIRST : SUBSEQUENT; + } + return lastEntry; + } + + private boolean hasTimeline (int id) { + Object[] timelines = animation.timelines.items; + for (int i = 0, n = animation.timelines.size; i < n; i++) + if (((Timeline)timelines[i]).getPropertyId() == id) return true; + return false; + } + /** The index of the track where this track entry is either current or queued. *

* See {@link AnimationState#getCurrent(int)}. */ @@ -994,7 +917,7 @@ public class AnimationState { } /** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no - * mixing is currently occuring. If mixing from multiple animations, mixingFrom makes up a linked list. */ + * mixing is currently occuring. When mixing from multiple animations, mixingFrom makes up a linked list. */ public TrackEntry getMixingFrom () { return mixingFrom; } diff --git a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java index 30621c5aa..49e24bca6 100644 --- a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java +++ b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java @@ -187,7 +187,6 @@ public class SkeletonViewer extends ApplicationAdapter { skeleton.updateWorldTransform(); state = new AnimationState(new AnimationStateData(skeletonData)); - state.setMultipleMixing(ui.multipleMixingCheckbox.isChecked()); state.addListener(new AnimationStateAdapter() { public void event (TrackEntry entry, Event event) { ui.toast(event.getData().getName()); @@ -384,7 +383,6 @@ public class SkeletonViewer extends ApplicationAdapter { List skinList = new List(skin); ScrollPane skinScroll = new ScrollPane(skinList, skin, "bg"); CheckBox loopCheckbox = new CheckBox("Loop", skin); - CheckBox multipleMixingCheckbox = new CheckBox("Multiple mixing", skin); CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin); Slider mixSlider = new Slider(0, 4, 0.01f, false, skin); Label mixLabel = new Label("0.3", skin); @@ -539,8 +537,6 @@ public class SkeletonViewer extends ApplicationAdapter { table.add(mixSlider).fillX().expandX(); root.add(table).fill().row(); } - root.add(); - root.add(multipleMixingCheckbox).row(); window.add(root).expand().fill(); window.pack(); @@ -706,12 +702,6 @@ public class SkeletonViewer extends ApplicationAdapter { } }); - multipleMixingCheckbox.addListener(new ChangeListener() { - public void changed (ChangeEvent event, Actor actor) { - if (state != null) state.setMultipleMixing(multipleMixingCheckbox.isChecked()); - } - }); - skinList.addListener(new ChangeListener() { public void changed (ChangeEvent event, Actor actor) { if (skeleton != null) { @@ -792,7 +782,6 @@ public class SkeletonViewer extends ApplicationAdapter { debugClippingCheckbox.addListener(savePrefsListener); premultipliedCheckbox.addListener(savePrefsListener); loopCheckbox.addListener(savePrefsListener); - multipleMixingCheckbox.addListener(savePrefsListener); speedSlider.addListener(savePrefsListener); speedResetButton.addListener(savePrefsListener); mixSlider.addListener(savePrefsListener); @@ -857,7 +846,6 @@ public class SkeletonViewer extends ApplicationAdapter { prefs.putBoolean("debugClipping", debugClippingCheckbox.isChecked()); prefs.putBoolean("premultiplied", premultipliedCheckbox.isChecked()); prefs.putBoolean("loop", loopCheckbox.isChecked()); - prefs.putBoolean("multipleMixing", multipleMixingCheckbox.isChecked()); prefs.putFloat("speed", speedSlider.getValue()); prefs.putFloat("mix", mixSlider.getValue()); prefs.putFloat("scale", scaleSlider.getValue()); @@ -886,7 +874,6 @@ public class SkeletonViewer extends ApplicationAdapter { debugClippingCheckbox.setChecked(prefs.getBoolean("debugClipping", true)); premultipliedCheckbox.setChecked(prefs.getBoolean("premultiplied", true)); loopCheckbox.setChecked(prefs.getBoolean("loop", false)); - multipleMixingCheckbox.setChecked(prefs.getBoolean("multipleMixing", false)); speedSlider.setValue(prefs.getFloat("speed", 0.3f)); mixSlider.setValue(prefs.getFloat("mix", 0.3f));