Improve MIDI, EffectAnimator, and SmoothEffect performance

pull/53/head v1.19.1
James Ball 2022-03-13 21:03:40 +00:00
rodzic 18c5565d61
commit ae7c974ec0
6 zmienionych plików z 69 dodań i 55 usunięć

Wyświetl plik

@ -6,7 +6,7 @@
<groupId>sh.ball</groupId>
<artifactId>osci-render</artifactId>
<version>1.19.0</version>
<version>1.19.1</version>
<name>osci-render</name>

Wyświetl plik

@ -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<List<Shape>> {
// MIDI
private final short[][] keyTargetVolumes = new short[MidiNote.NUM_CHANNELS][128];
private final short[][] keyActualVolumes = new short[MidiNote.NUM_CHANNELS][128];
private final Set<MidiNote> 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<List<Shape>> {
private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new ConcurrentHashMap<>();
private final Map<MidiNote, SineEffect> sineEffects = new ConcurrentHashMap<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private AudioEngine audioEngine;
@ -91,27 +93,28 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
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<List<Shape>> {
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<List<Shape>> {
@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<List<Shape>> {
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<List<Shape>> {
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) {

Wyświetl plik

@ -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;

Wyświetl plik

@ -7,56 +7,47 @@ import java.util.List;
public class SmoothEffect implements SettableEffect {
private List<Vector2> 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<Vector2> 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);
}
}

Wyświetl plik

@ -60,7 +60,7 @@ public class MidiNote {
@Override
public int hashCode() {
return Objects.hash(key, channel);
return (key << 16) + channel;
}
@Override

Wyświetl plik

@ -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)
);
}