From ae7c974ec08f9ab0166578bb1a5f4fc93738059b Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 13 Mar 2022 21:03:40 +0000 Subject: [PATCH] Improve MIDI, EffectAnimator, and SmoothEffect performance --- pom.xml | 2 +- .../java/sh/ball/audio/ShapeAudioPlayer.java | 53 +++++++++++-------- .../sh/ball/audio/effect/EffectAnimator.java | 13 ++++- .../sh/ball/audio/effect/SmoothEffect.java | 51 ++++++++---------- .../java/sh/ball/audio/midi/MidiNote.java | 2 +- .../sh/ball/gui/controller/ObjController.java | 3 +- 6 files changed, 69 insertions(+), 55 deletions(-) diff --git a/pom.xml b/pom.xml index 9065021a..d625df9a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ sh.ball osci-render - 1.19.0 + 1.19.1 osci-render diff --git a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java index 9739551f..66d461bb 100644 --- a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java +++ b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java @@ -7,6 +7,7 @@ import sh.ball.audio.effect.Effect; import java.io.*; import java.util.*; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import sh.ball.audio.effect.PhaseEffect; import sh.ball.audio.effect.SineEffect; @@ -39,11 +40,13 @@ public class ShapeAudioPlayer implements AudioPlayer> { // MIDI private final short[][] keyTargetVolumes = new short[MidiNote.NUM_CHANNELS][128]; private final short[][] keyActualVolumes = new short[MidiNote.NUM_CHANNELS][128]; - private final Set keysDown = ConcurrentHashMap.newKeySet(); + private final AtomicInteger numKeysDown = new AtomicInteger(1); + private final MidiNote[][] keysDown = new MidiNote[MidiNote.NUM_CHANNELS][128]; + private final SineEffect[][] sineEffects = new SineEffect[MidiNote.NUM_CHANNELS][128]; private boolean midiStarted = false; private int mainChannel = 0; private MidiNote baseNote = new MidiNote(60, mainChannel); - private double[] pitchBends = new double[MidiNote.NUM_CHANNELS]; + private final double[] pitchBends = new double[MidiNote.NUM_CHANNELS]; private int lastDecay = 0; private double decaySeconds = 0.2; private int decayFrames; @@ -54,7 +57,6 @@ public class ShapeAudioPlayer implements AudioPlayer> { private final Callable audioEngineBuilder; private final BlockingQueue> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); private final Map effects = new ConcurrentHashMap<>(); - private final Map sineEffects = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private AudioEngine audioEngine; @@ -91,27 +93,28 @@ public class ShapeAudioPlayer implements AudioPlayer> { } public boolean midiPlaying() { - return keysDown.size() > 0; + return numKeysDown.get() > 0; } public void resetMidi() { - keysDown.clear(); - for (int i = 0; i < keyTargetVolumes.length; i++) { + for (int i = 0; i < MidiNote.NUM_CHANNELS; i++) { + Arrays.fill(keysDown[i], null); Arrays.fill(keyTargetVolumes[i], (short) 0); Arrays.fill(keyActualVolumes[i], (short) 0); } // Middle C is down by default keyTargetVolumes[0][60] = (short) MidiNote.MAX_VELOCITY; keyActualVolumes[0][60] = (short) MidiNote.MAX_VELOCITY; - keysDown.add(new MidiNote(60)); + MidiNote note = new MidiNote(60); + keysDown[note.channel()][note.key()] = note; midiStarted = false; notesChanged(); } public void stopMidiNotes() { - keysDown.clear(); - for (short[] keyTargetVolume : keyTargetVolumes) { - Arrays.fill(keyTargetVolume, (short) 0); + for (int i = 0; i < MidiNote.NUM_CHANNELS; i++) { + Arrays.fill(keysDown[i], null); + Arrays.fill(keyTargetVolumes[i], (short) 0); } } @@ -245,9 +248,11 @@ public class ShapeAudioPlayer implements AudioPlayer> { lastAttack = 0; } lastAttack++; - for (MidiNote note : keysDown) { - if (!note.equals(baseNote)) { - vector = sineEffects.get(note).apply(frame, vector); + for (int channel = 0; channel < keysDown.length; channel++) { + for (int key = 0; key < keysDown[0].length; key++) { + if (keysDown[channel][key] != null && !keysDown[channel][key].equals(baseNote)) { + vector = sineEffects[channel][key].apply(frame, vector); + } } } } @@ -405,12 +410,14 @@ public class ShapeAudioPlayer implements AudioPlayer> { @Override public void setDevice(AudioDevice device) { this.device = device; - sineEffects.clear(); + for (int i = 0; i < MidiNote.NUM_CHANNELS; i++) { + Arrays.fill(sineEffects[i], null); + } this.sampleRate = device.sampleRate(); for (int channel = 0; channel < keyActualVolumes.length; channel++) { for (int key = 0; key < keyActualVolumes[channel].length; key++) { MidiNote note = new MidiNote(key, channel); - sineEffects.put(note, new SineEffect(sampleRate, note.frequency(), keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY)); + sineEffects[channel][key] = new SineEffect(sampleRate, note.frequency(), keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY); } } for (Effect effect : effects.values()) { @@ -471,10 +478,12 @@ public class ShapeAudioPlayer implements AudioPlayer> { for (int channel = 0; channel < keyActualVolumes.length; channel++) { for (int key = 0; key < keyActualVolumes[channel].length; key++) { MidiNote note = new MidiNote(key, channel); - if (keyActualVolumes[channel][key] > 0 && sineEffects.size() != 0) { - SineEffect effect = sineEffects.get(note); - effect.setVolume(scaledVolume * keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY); - effect.setFrequency(note.frequency() * pitchBends[channel]); + if (keyActualVolumes[channel][key] > 0) { + SineEffect effect = sineEffects[channel][key]; + if (effect != null) { + effect.setVolume(scaledVolume * keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY); + effect.setFrequency(note.frequency() * pitchBends[channel]); + } } } } @@ -520,10 +529,12 @@ public class ShapeAudioPlayer implements AudioPlayer> { if (command == ShortMessage.NOTE_OFF || velocity == 0) { keyTargetVolumes[note.channel()][note.key()] = 0; - keysDown.remove(note); + keysDown[note.channel()][note.key()] = null; + numKeysDown.getAndDecrement(); } else { keyTargetVolumes[note.channel()][note.key()] = (short) velocity; - keysDown.add(note); + keysDown[note.channel()][note.key()] = note; + numKeysDown.getAndIncrement(); } notesChanged(); } else if (command == ShortMessage.PITCH_BEND) { diff --git a/src/main/java/sh/ball/audio/effect/EffectAnimator.java b/src/main/java/sh/ball/audio/effect/EffectAnimator.java index 48c6a567..bd645fff 100644 --- a/src/main/java/sh/ball/audio/effect/EffectAnimator.java +++ b/src/main/java/sh/ball/audio/effect/EffectAnimator.java @@ -9,6 +9,7 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { private final SettableEffect effect; private AnimationType type = AnimationType.STATIC; + private boolean justSetToStatic = true; private double targetValue = 0.5; private double actualValue = 0.5; private boolean linearDirection = true; @@ -29,6 +30,9 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { public void setAnimation(AnimationType type) { this.type = type; this.linearDirection = true; + if (type == AnimationType.STATIC) { + justSetToStatic = true; + } } public void setMin(double min) { @@ -50,7 +54,14 @@ public class EffectAnimator extends PhaseEffect implements SettableEffect { double normalisedTargetValue = (targetValue - minValue) / range; double normalisedActualValue = (actualValue - minValue) / range; switch (type) { - case STATIC -> actualValue = targetValue; + case STATIC -> { + if (justSetToStatic) { + actualValue = targetValue; + justSetToStatic = false; + effect.setValue(actualValue); + } + return effect.apply(count, vector); + } case SEESAW -> { double scalar = 10 * Math.max(Math.min(normalisedActualValue, 1 - normalisedActualValue), 0.01); double change = range * scalar * SPEED_SCALE * normalisedTargetValue / sampleRate; diff --git a/src/main/java/sh/ball/audio/effect/SmoothEffect.java b/src/main/java/sh/ball/audio/effect/SmoothEffect.java index c6e70214..88246f16 100644 --- a/src/main/java/sh/ball/audio/effect/SmoothEffect.java +++ b/src/main/java/sh/ball/audio/effect/SmoothEffect.java @@ -7,56 +7,47 @@ import java.util.List; public class SmoothEffect implements SettableEffect { - private List window; + private static final int MAX_WINDOW_SIZE = 2048; + + private final Vector2[] window; private int windowSize; private int head = 0; public SmoothEffect(int windowSize) { this.windowSize = windowSize <= 0 ? 1 : windowSize; - this.window = new ArrayList<>(); - for (int i = 0; i < windowSize; i++) { - window.add(null); - } + this.window = new Vector2[MAX_WINDOW_SIZE]; } @Override public synchronized void setValue(double value) { int windowSize = (int) (256 * value); - int oldWindowSize = this.windowSize; - this.windowSize = windowSize <= 0 ? 1 : windowSize; - List newWindow = new ArrayList<>(); - for (int i = 0; i < this.windowSize; i++) { - newWindow.add(null); - } - for (int i = 0; i < Math.min(this.windowSize, oldWindowSize); i++) { - newWindow.set(i, window.get(head++)); - if (head >= window.size()) { - head = 0; - } - } - head = 0; - window = newWindow; + this.windowSize = Math.max(1, Math.min(MAX_WINDOW_SIZE, windowSize)); } + // could be made much more efficient by just subbing prev vector and adding + // new vector to the aggregate previous average @Override public synchronized Vector2 apply(int count, Vector2 vector) { - window.set(head, vector); - head++; - if (head >= windowSize) { + window[head++] = vector; + if (head >= MAX_WINDOW_SIZE) { head = 0; } double totalX = 0; double totalY = 0; - int size = 0; - - for (Vector2 v : window) { - if (v != null) { - totalX += v.getX(); - totalY += v.getY(); - size++; + int newHead = head - 1; + for (int i = 0; i < windowSize; i++) { + if (newHead < 0) { + newHead = MAX_WINDOW_SIZE - 1; } + + if (window[newHead] != null) { + totalX += window[newHead].getX(); + totalY += window[newHead].getY(); + } + + newHead--; } - return new Vector2(totalX / size, totalY / size); + return new Vector2(totalX / windowSize, totalY / windowSize); } } diff --git a/src/main/java/sh/ball/audio/midi/MidiNote.java b/src/main/java/sh/ball/audio/midi/MidiNote.java index 4bb84d65..2d667752 100644 --- a/src/main/java/sh/ball/audio/midi/MidiNote.java +++ b/src/main/java/sh/ball/audio/midi/MidiNote.java @@ -60,7 +60,7 @@ public class MidiNote { @Override public int hashCode() { - return Objects.hash(key, channel); + return (key << 16) + channel; } @Override diff --git a/src/main/java/sh/ball/gui/controller/ObjController.java b/src/main/java/sh/ball/gui/controller/ObjController.java index e988fe13..a63b3e4f 100644 --- a/src/main/java/sh/ball/gui/controller/ObjController.java +++ b/src/main/java/sh/ball/gui/controller/ObjController.java @@ -60,8 +60,9 @@ public class ObjController implements Initializable, SubController { // changes the rotateSpeed of the FrameProducer public void setObjectRotateSpeed(double rotateSpeed) { + double actualSpeed = (Math.exp(3 * Math.min(10, Math.abs(rotateSpeed))) - 1) / 50; producer.setFrameSettings( - ObjSettingsFactory.rotateSpeed((Math.exp(3 * rotateSpeed) - 1) / 50) + ObjSettingsFactory.rotateSpeed(rotateSpeed > 0 ? actualSpeed : -actualSpeed) ); }