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>
<artifactId>osci-render</artifactId>
<version>1.11.5</version>
<version>1.11.6</version>
<name>osci-render</name>

Wyświetl plik

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

Wyświetl plik

@ -5,7 +5,9 @@ import sh.ball.audio.effect.Effect;
import java.io.*;
import java.util.*;
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.AudioEngine;
import sh.ball.shapes.Shape;
@ -13,7 +15,6 @@ import sh.ball.shapes.Vector2;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.swing.*;
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 BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new ConcurrentHashMap<>();
private final List<SineEffect> sineEffects = new CopyOnWriteArrayList<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private AudioEngine audioEngine;
@ -43,7 +45,8 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private double lengthIncrement = MIN_LENGTH_INCREMENT;
private double lengthDrawn = 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 trace = 0.5;
@ -127,6 +130,12 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
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()) {
vector = effect.apply(frame, vector);
}
@ -134,9 +143,18 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
@Override
public void setBaseFrequency(double frequency) {
this.frequency = frequency;
public void setBaseFrequencies(List<Double> frequencies) {
this.frequencies = frequencies;
double maxFrequency = frequencies.stream().max(Double::compareTo).get();
this.mainFrequency = maxFrequency;
updateLengthIncrement();
sineEffects.clear();
for (Double frequency : frequencies) {
if (frequency != maxFrequency) {
sineEffects.add(new SineEffect(device.sampleRate(), frequency));
}
}
}
@Override
@ -146,13 +164,13 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
@Override
public double getBaseFrequency() {
return frequency;
public List<Double> getBaseFrequencies() {
return frequencies;
}
@Override
public double getFrequency() {
return frequency * pitchBend;
public List<Double> getFrequencies() {
return frequencies.stream().map(d -> d * pitchBend).collect(Collectors.toList());
}
private Shape getCurrentShape() {
@ -166,7 +184,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private void updateLengthIncrement() {
double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate();
double actualFrequency = frequency * pitchBend;
double actualFrequency = mainFrequency * pitchBend;
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.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 lastFrequency;
private double volume;
public SineEffect(int sampleRate, double volume) {
public SineEffect(int sampleRate, double frequency, double volume) {
super(sampleRate, 2);
this.frequency = frequency;
this.volume = Math.max(Math.min(volume, 1), 0);
}
public SineEffect(int sampleRate) {
this(sampleRate, DEFAULT_VOLUME);
}
public void update() {
frequency = lastFrequency;
public SineEffect(int sampleRate, double frequency) {
this(sampleRate, frequency, DEFAULT_VOLUME);
}
public void setVolume(double volume) {
this.volume = volume;
}
@Override
public void updateFrequency(double leftFrequency, double rightFrequency) {
lastFrequency = leftFrequency;
public void setFrequency(double frequency) {
this.frequency = frequency;
}
@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);
double y = vector.getY() + volume * Math.cos(frequency * theta);
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.Executors;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
@ -69,7 +70,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private final RotateEffect rotateEffect;
private final TranslateEffect translateEffect;
private final SineEffect wobbleEffect;
private final WobbleEffect wobbleEffect;
private final ScaleEffect scaleEffect;
private int sampleRate;
@ -195,7 +196,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
this.sampleRate = defaultDevice.sampleRate();
this.rotateEffect = new RotateEffect(sampleRate);
this.translateEffect = new TranslateEffect(sampleRate);
this.wobbleEffect = new SineEffect(sampleRate);
this.wobbleEffect = new WobbleEffect(sampleRate);
this.scaleEffect = new ScaleEffect();
}
@ -217,7 +218,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private Map<Slider, Consumer<Double>> initializeSliderMap() {
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,
translationSpeedSlider, translateEffect::setSpeed,
scaleSlider, scaleEffect::setScale,
@ -633,9 +634,11 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
return min + (midiPressure / 127.0) * range;
}
private void playNote(double frequency, double volume) {
frequencySlider.setValue(Math.log(frequency) / Math.log(MAX_FREQUENCY));
audioPlayer.setBaseFrequency(frequency);
private void playNotes(double volume) {
List<Double> frequencies = downKeys.stream().map(MidiNote::frequency).collect(Collectors.toList());
double mainFrequency = frequencies.get(frequencies.size() - 1);
frequencySlider.setValue(Math.log(mainFrequency) / Math.log(MAX_FREQUENCY));
audioPlayer.setBaseFrequencies(frequencies);
scaleSlider.setValue(volume);
}
@ -668,7 +671,6 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
MidiNote note = new MidiNote(message.getData1());
int velocity = message.getData2();
double frequency = note.frequency();
double oldVolume = scaleSlider.getValue();
double volume = midiPressureToPressure(scaleSlider, velocity);
volume /= 10;
@ -681,8 +683,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
volumeTimeline = new Timeline(kf);
volumeTimeline.play();
} else {
frequency = downKeys.get(downKeys.size() - 1).frequency();
playNote(frequency, oldVolume);
playNotes(oldVolume);
}
} else {
downKeys.add(note);
@ -690,7 +691,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
volumeTimeline.stop();
volumeTimeline = null;
}
playNote(frequency, volume);
playNotes(volume);
KeyValue kv = new KeyValue(scaleSlider.valueProperty(), scaleSlider.valueProperty().get() * 0.75, Interpolator.EASE_OUT);
KeyFrame kf = new KeyFrame(Duration.millis(250), kv);
volumeTimeline = new Timeline(kf);

Wyświetl plik

@ -19,7 +19,10 @@ public final class Line extends Shape {
}
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

Wyświetl plik

@ -1,42 +1,8 @@
package sh.ball.shapes;
public class QuadraticBezierCurve extends Shape {
private final Vector2 p0;
private final Vector2 p1;
private final Vector2 p2;
public class QuadraticBezierCurve extends CubicBezierCurve {
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this.p0 = p0;
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));
super(p0, p0.add(p1.sub(p0).scale(2.0/3.0)), p2.add(p1.sub(p2).scale(2.0/3.0)), p2);
}
}