kopia lustrzana https://github.com/jameshball/osci-render
Implement ability to play chords
rodzic
1f60dfd17e
commit
410ddd59ae
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue