Implement ability to play chords

pull/35/head
James Ball 2021-07-20 21:41:43 +01:00
rodzic 1f60dfd17e
commit 410ddd59ae
8 zmienionych plików z 101 dodań i 74 usunięć

Wyświetl plik

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

Wyświetl plik

@ -14,13 +14,13 @@ public interface AudioPlayer<S> extends Runnable {
boolean isPlaying(); boolean isPlaying();
void setBaseFrequency(double frequency); void setBaseFrequencies(List<Double> frequency);
void setPitchBendFactor(double pitchBend); void setPitchBendFactor(double pitchBend);
double getFrequency(); List<Double> getFrequencies();
double getBaseFrequency(); List<Double> getBaseFrequencies();
void addFrame(S frame); void addFrame(S frame);

Wyświetl plik

@ -5,7 +5,9 @@ import sh.ball.audio.effect.Effect;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.stream.Collectors;
import sh.ball.audio.effect.SineEffect;
import sh.ball.audio.engine.AudioDevice; import sh.ball.audio.engine.AudioDevice;
import sh.ball.audio.engine.AudioEngine; import sh.ball.audio.engine.AudioEngine;
import sh.ball.shapes.Shape; import sh.ball.shapes.Shape;
@ -13,7 +15,6 @@ import sh.ball.shapes.Vector2;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioInputStream;
import javax.swing.*;
public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> { public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
@ -32,6 +33,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private final Callable<AudioEngine> audioEngineBuilder; private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new ConcurrentHashMap<>(); private final Map<Object, Effect> effects = new ConcurrentHashMap<>();
private final List<SineEffect> sineEffects = new CopyOnWriteArrayList<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private AudioEngine audioEngine; private AudioEngine audioEngine;
@ -43,7 +45,8 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private double lengthIncrement = MIN_LENGTH_INCREMENT; private double lengthIncrement = MIN_LENGTH_INCREMENT;
private double lengthDrawn = 0; private double lengthDrawn = 0;
private int count = 0; private int count = 0;
private double frequency = MIDDLE_C; private List<Double> frequencies = List.of(MIDDLE_C);
private double mainFrequency = MIDDLE_C;
private double pitchBend = 1.0; private double pitchBend = 1.0;
private double trace = 0.5; private double trace = 0.5;
@ -127,6 +130,12 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
} }
private Vector2 applyEffects(int frame, Vector2 vector) { private Vector2 applyEffects(int frame, Vector2 vector) {
int numNotes = sineEffects.size() + 1;
vector.scale(1.0 / numNotes);
for (SineEffect effect : sineEffects) {
effect.setVolume(1.0 / numNotes);
vector = effect.apply(frame, vector);
}
for (Effect effect : effects.values()) { for (Effect effect : effects.values()) {
vector = effect.apply(frame, vector); vector = effect.apply(frame, vector);
} }
@ -134,9 +143,18 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
} }
@Override @Override
public void setBaseFrequency(double frequency) { public void setBaseFrequencies(List<Double> frequencies) {
this.frequency = frequency; this.frequencies = frequencies;
double maxFrequency = frequencies.stream().max(Double::compareTo).get();
this.mainFrequency = maxFrequency;
updateLengthIncrement(); updateLengthIncrement();
sineEffects.clear();
for (Double frequency : frequencies) {
if (frequency != maxFrequency) {
sineEffects.add(new SineEffect(device.sampleRate(), frequency));
}
}
} }
@Override @Override
@ -146,13 +164,13 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
} }
@Override @Override
public double getBaseFrequency() { public List<Double> getBaseFrequencies() {
return frequency; return frequencies;
} }
@Override @Override
public double getFrequency() { public List<Double> getFrequencies() {
return frequency * pitchBend; return frequencies.stream().map(d -> d * pitchBend).collect(Collectors.toList());
} }
private Shape getCurrentShape() { private Shape getCurrentShape() {
@ -166,7 +184,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private void updateLengthIncrement() { private void updateLengthIncrement() {
double totalLength = Shape.totalLength(frame); double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate(); int sampleRate = device.sampleRate();
double actualFrequency = frequency * pitchBend; double actualFrequency = mainFrequency * pitchBend;
lengthIncrement = Math.max(totalLength / (sampleRate / actualFrequency), MIN_LENGTH_INCREMENT); lengthIncrement = Math.max(totalLength / (sampleRate / actualFrequency), MIN_LENGTH_INCREMENT);
} }

Wyświetl plik

@ -3,41 +3,36 @@ package sh.ball.audio.effect;
import sh.ball.audio.FrequencyListener; import sh.ball.audio.FrequencyListener;
import sh.ball.shapes.Vector2; import sh.ball.shapes.Vector2;
public class SineEffect extends PhaseEffect implements FrequencyListener { public class SineEffect extends PhaseEffect {
private static final double DEFAULT_VOLUME = 0.2; private static final double DEFAULT_VOLUME = 1;
private double frequency; private double frequency;
private double lastFrequency;
private double volume; private double volume;
public SineEffect(int sampleRate, double volume) { public SineEffect(int sampleRate, double frequency, double volume) {
super(sampleRate, 2); super(sampleRate, 2);
this.frequency = frequency;
this.volume = Math.max(Math.min(volume, 1), 0); this.volume = Math.max(Math.min(volume, 1), 0);
} }
public SineEffect(int sampleRate) { public SineEffect(int sampleRate, double frequency) {
this(sampleRate, DEFAULT_VOLUME); this(sampleRate, frequency, DEFAULT_VOLUME);
}
public void update() {
frequency = lastFrequency;
} }
public void setVolume(double volume) { public void setVolume(double volume) {
this.volume = volume; this.volume = volume;
} }
@Override public void setFrequency(double frequency) {
public void updateFrequency(double leftFrequency, double rightFrequency) { this.frequency = frequency;
lastFrequency = leftFrequency;
} }
@Override @Override
public Vector2 apply(int count, Vector2 vector) { public Vector2 apply(int count, Vector2 vector) {
double theta = nextTheta(); double theta = nextTheta();
double x = vector.getX() + volume * Math.sin(frequency * theta); double x = vector.getX() + volume * Math.sin(frequency * theta);
double y = vector.getY() + volume * Math.sin(frequency * theta); double y = vector.getY() + volume * Math.cos(frequency * theta);
return new Vector2(x, y); return new Vector2(x, y);
} }

Wyświetl plik

@ -0,0 +1,44 @@
package sh.ball.audio.effect;
import sh.ball.audio.FrequencyListener;
import sh.ball.shapes.Vector2;
public class WobbleEffect extends PhaseEffect implements FrequencyListener {
private static final double DEFAULT_VOLUME = 0.2;
private double frequency;
private double lastFrequency;
private double volume;
public WobbleEffect(int sampleRate, double volume) {
super(sampleRate, 2);
this.volume = Math.max(Math.min(volume, 1), 0);
}
public WobbleEffect(int sampleRate) {
this(sampleRate, DEFAULT_VOLUME);
}
public void update() {
frequency = lastFrequency;
}
public void setVolume(double volume) {
this.volume = volume;
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
lastFrequency = leftFrequency;
}
@Override
public Vector2 apply(int count, Vector2 vector) {
double theta = nextTheta();
double x = vector.getX() + volume * Math.sin(frequency * theta);
double y = vector.getY() + volume * Math.sin(frequency * theta);
return new Vector2(x, y);
}
}

Wyświetl plik

@ -23,6 +23,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -69,7 +70,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private final RotateEffect rotateEffect; private final RotateEffect rotateEffect;
private final TranslateEffect translateEffect; private final TranslateEffect translateEffect;
private final SineEffect wobbleEffect; private final WobbleEffect wobbleEffect;
private final ScaleEffect scaleEffect; private final ScaleEffect scaleEffect;
private int sampleRate; private int sampleRate;
@ -195,7 +196,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
this.sampleRate = defaultDevice.sampleRate(); this.sampleRate = defaultDevice.sampleRate();
this.rotateEffect = new RotateEffect(sampleRate); this.rotateEffect = new RotateEffect(sampleRate);
this.translateEffect = new TranslateEffect(sampleRate); this.translateEffect = new TranslateEffect(sampleRate);
this.wobbleEffect = new SineEffect(sampleRate); this.wobbleEffect = new WobbleEffect(sampleRate);
this.scaleEffect = new ScaleEffect(); this.scaleEffect = new ScaleEffect();
} }
@ -217,7 +218,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private Map<Slider, Consumer<Double>> initializeSliderMap() { private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of( return Map.of(
frequencySlider, f -> audioPlayer.setBaseFrequency(Math.pow(MAX_FREQUENCY, f)), frequencySlider, f -> audioPlayer.setBaseFrequencies(List.of(Math.pow(MAX_FREQUENCY, f))),
rotateSpeedSlider, rotateEffect::setSpeed, rotateSpeedSlider, rotateEffect::setSpeed,
translationSpeedSlider, translateEffect::setSpeed, translationSpeedSlider, translateEffect::setSpeed,
scaleSlider, scaleEffect::setScale, scaleSlider, scaleEffect::setScale,
@ -633,9 +634,11 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
return min + (midiPressure / 127.0) * range; return min + (midiPressure / 127.0) * range;
} }
private void playNote(double frequency, double volume) { private void playNotes(double volume) {
frequencySlider.setValue(Math.log(frequency) / Math.log(MAX_FREQUENCY)); List<Double> frequencies = downKeys.stream().map(MidiNote::frequency).collect(Collectors.toList());
audioPlayer.setBaseFrequency(frequency); double mainFrequency = frequencies.get(frequencies.size() - 1);
frequencySlider.setValue(Math.log(mainFrequency) / Math.log(MAX_FREQUENCY));
audioPlayer.setBaseFrequencies(frequencies);
scaleSlider.setValue(volume); scaleSlider.setValue(volume);
} }
@ -668,7 +671,6 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
MidiNote note = new MidiNote(message.getData1()); MidiNote note = new MidiNote(message.getData1());
int velocity = message.getData2(); int velocity = message.getData2();
double frequency = note.frequency();
double oldVolume = scaleSlider.getValue(); double oldVolume = scaleSlider.getValue();
double volume = midiPressureToPressure(scaleSlider, velocity); double volume = midiPressureToPressure(scaleSlider, velocity);
volume /= 10; volume /= 10;
@ -681,8 +683,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
volumeTimeline = new Timeline(kf); volumeTimeline = new Timeline(kf);
volumeTimeline.play(); volumeTimeline.play();
} else { } else {
frequency = downKeys.get(downKeys.size() - 1).frequency(); playNotes(oldVolume);
playNote(frequency, oldVolume);
} }
} else { } else {
downKeys.add(note); downKeys.add(note);
@ -690,7 +691,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
volumeTimeline.stop(); volumeTimeline.stop();
volumeTimeline = null; volumeTimeline = null;
} }
playNote(frequency, volume); playNotes(volume);
KeyValue kv = new KeyValue(scaleSlider.valueProperty(), scaleSlider.valueProperty().get() * 0.75, Interpolator.EASE_OUT); KeyValue kv = new KeyValue(scaleSlider.valueProperty(), scaleSlider.valueProperty().get() * 0.75, Interpolator.EASE_OUT);
KeyFrame kf = new KeyFrame(Duration.millis(250), kv); KeyFrame kf = new KeyFrame(Duration.millis(250), kv);
volumeTimeline = new Timeline(kf); volumeTimeline = new Timeline(kf);

Wyświetl plik

@ -19,7 +19,10 @@ public final class Line extends Shape {
} }
private double calculateLength() { private double calculateLength() {
return Math.sqrt(Math.pow(getX1() - getX2(), 2) + Math.pow(getY1() - getY2(), 2)); double ac = Math.abs(getY2() - getY1());
double cb = Math.abs(getX2() - getX1());
return Math.hypot(ac, cb);
} }
@Override @Override

Wyświetl plik

@ -1,42 +1,8 @@
package sh.ball.shapes; package sh.ball.shapes;
public class QuadraticBezierCurve extends Shape { public class QuadraticBezierCurve extends CubicBezierCurve {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) { public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this.p0 = p0; super(p0, p0.add(p1.sub(p0).scale(2.0/3.0)), p2.add(p1.sub(p2).scale(2.0/3.0)), p2);
this.p1 = p1;
this.p2 = p2;
this.length = new Line(p0, p2).length;
}
@Override
public Vector2 nextVector(double t) {
return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2)))
.add(p2.sub(p1).scale(Math.pow(t, 2)));
}
@Override
public QuadraticBezierCurve rotate(double theta) {
return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta));
}
@Override
public QuadraticBezierCurve scale(double factor) {
return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor));
}
@Override
public QuadraticBezierCurve scale(Vector2 vector) {
return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector));
}
@Override
public QuadraticBezierCurve translate(Vector2 vector) {
return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector),
p2.translate(vector));
} }
} }