Convert weight-based shape drawing to frequency-based drawing for consistent output frequency

pull/35/head
James Ball 2021-07-15 19:38:53 +01:00
rodzic 03e1e679e6
commit d785538f94
14 zmienionych plików z 81 dodań i 126 usunięć

Wyświetl plik

@ -14,7 +14,7 @@ public interface AudioPlayer<S> extends Runnable {
boolean isPlaying();
void setQuality(double quality);
void setFrequency(double quality);
void addFrame(S frame);

Wyświetl plik

@ -13,8 +13,6 @@ import sh.ball.shapes.Vector2;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.LineUnavailableException;
import java.util.concurrent.locks.ReentrantLock;
public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
@ -27,6 +25,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private static final boolean BIG_ENDIAN = false;
// Stereo audio
private static final int NUM_OUTPUTS = 2;
private static final double MIN_LENGTH_INCREMENT = 0.0001;
private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
@ -39,10 +38,11 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private int framesRecorded = 0;
private List<Shape> frame;
private int currentShape = 0;
private int audioFramesDrawn = 0;
private double lengthIncrement = MIN_LENGTH_INCREMENT;
private double lengthDrawn = 0;
private int count = 0;
private double frequency = 261.63;
private double weight = Shape.DEFAULT_WEIGHT;
private AudioDevice device;
public ShapeAudioPlayer(Callable<AudioEngine> audioEngineBuilder) throws Exception {
@ -51,29 +51,30 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
private Vector2 generateChannels() throws InterruptedException {
Shape shape = getCurrentShape().setWeight(weight);
Shape shape = getCurrentShape();
double totalAudioFrames = shape.getWeight() * shape.getLength();
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
double length = shape.getLength();
double drawingProgress = length == 0 ? 1 : lengthDrawn / length;
Vector2 nextVector = applyEffects(count, shape.nextVector(drawingProgress));
Vector2 channels = cutoff(nextVector);
writeChannels((float) channels.getX(), (float) channels.getY());
audioFramesDrawn++;
lengthDrawn += lengthIncrement;
if (++count > MAX_COUNT) {
count = 0;
}
if (audioFramesDrawn > totalAudioFrames) {
audioFramesDrawn = 0;
if (lengthDrawn > length) {
lengthDrawn = lengthDrawn - length;
currentShape++;
}
if (currentShape >= frame.size()) {
currentShape = 0;
frame = frameQueue.take();
updateTotalAudioFrames();
}
return channels;
@ -128,8 +129,8 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
@Override
public void setQuality(double quality) {
this.weight = quality;
public void setFrequency(double frequency) {
this.frequency = frequency;
}
private Shape getCurrentShape() {
@ -140,10 +141,17 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
return frame.get(currentShape);
}
private void updateTotalAudioFrames() {
double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate();
lengthIncrement = Math.max(totalLength / (sampleRate / frequency), MIN_LENGTH_INCREMENT);
}
@Override
public void run() {
try {
frame = frameQueue.take();
updateTotalAudioFrames();
} catch (InterruptedException e) {
throw new RuntimeException("Initial frame not found. Cannot continue.");
}

Wyświetl plik

@ -15,4 +15,6 @@ public interface AudioEngine {
List<AudioDevice> devices();
AudioDevice getDefaultDevice();
AudioDevice currentDevice();
}

Wyświetl plik

@ -20,6 +20,7 @@ public class ConglomerateAudioEngine implements AudioEngine {
private static AudioDevice javaDevice;
private boolean playing = false;
private AudioDevice device;
@Override
public boolean isPlaying() {
@ -29,12 +30,14 @@ public class ConglomerateAudioEngine implements AudioEngine {
@Override
public void play(Callable<Vector2> channelGenerator, AudioDevice device) throws Exception {
playing = true;
this.device = device;
if (xtDevices.contains(device)) {
xtEngine.play(channelGenerator, device);
} else {
javaEngine.play(channelGenerator, javaDevice);
}
playing = false;
this.device = null;
}
@Override
@ -66,4 +69,9 @@ public class ConglomerateAudioEngine implements AudioEngine {
public AudioDevice getDefaultDevice() {
return javaEngine.getDefaultDevice();
}
@Override
public AudioDevice currentDevice() {
return device;
}
}

Wyświetl plik

@ -22,6 +22,7 @@ public class JavaAudioEngine implements AudioEngine {
private volatile boolean stopped = false;
private SourceDataLine source;
private AudioDevice device;
@Override
public boolean isPlaying() {
@ -31,6 +32,7 @@ public class JavaAudioEngine implements AudioEngine {
@Override
public void play(Callable<Vector2> channelGenerator, AudioDevice device) throws Exception {
this.stopped = false;
this.device = device;
AudioFormat format = new AudioFormat((float) device.sampleRate(), BIT_DEPTH, NUM_CHANNELS, SIGNED_SAMPLE, BIG_ENDIAN);
@ -71,6 +73,7 @@ public class JavaAudioEngine implements AudioEngine {
}
}
source.stop();
this.device = null;
}
@Override
@ -87,4 +90,9 @@ public class JavaAudioEngine implements AudioEngine {
public AudioDevice getDefaultDevice() {
return new DefaultAudioDevice("default", "default", DEFAULT_SAMPLE_RATE, AudioSample.INT16);
}
@Override
public AudioDevice currentDevice() {
return device;
}
}

Wyświetl plik

@ -120,6 +120,7 @@ public class XtAudioEngine implements AudioEngine {
}
}
playing = false;
this.device = null;
}
@Override
@ -196,6 +197,11 @@ public class XtAudioEngine implements AudioEngine {
}
}
@Override
public AudioDevice currentDevice() {
return device;
}
private XtService getService(XtPlatform platform) {
XtService service = platform.getService(platform.setupToSystem(Enums.XtSetup.SYSTEM_AUDIO));

Wyświetl plik

@ -105,9 +105,9 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
@FXML
private TextField translationYTextField;
@FXML
private Slider weightSlider;
private Slider frequencySlider;
@FXML
private SVGPath weightMidi;
private SVGPath frequencyMidi;
@FXML
private Slider rotateSpeedSlider;
@FXML
@ -192,7 +192,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private Map<SVGPath, Slider> initializeMidiButtonMap() {
Map<SVGPath, Slider> midiMap = new HashMap<>();
midiMap.put(weightMidi, weightSlider);
midiMap.put(frequencyMidi, frequencySlider);
midiMap.put(rotateSpeedMidi, rotateSpeedSlider);
midiMap.put(translationSpeedMidi, translationSpeedSlider);
midiMap.put(scaleMidi, scaleSlider);
@ -208,7 +208,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of(
weightSlider, audioPlayer::setQuality,
frequencySlider, audioPlayer::setFrequency,
rotateSpeedSlider, rotateEffect::setSpeed,
translationSpeedSlider, translateEffect::setSpeed,
scaleSlider, scaleEffect::setScale,

Wyświetl plik

@ -7,19 +7,14 @@ public class CubicBezierCurve extends Shape {
private final Vector2 p2;
private final Vector2 p3;
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) {
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
this.weight = weight;
this.length = new Line(p0, p3).length;
}
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
this(p0, p1, p2, p3, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p0.scale(Math.pow(1 - t, 3))
@ -31,7 +26,7 @@ public class CubicBezierCurve extends Shape {
@Override
public CubicBezierCurve rotate(double theta) {
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
p3.rotate(theta), weight);
p3.rotate(theta));
}
@Override
@ -42,17 +37,12 @@ public class CubicBezierCurve extends Shape {
@Override
public CubicBezierCurve scale(Vector2 vector) {
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
p3.scale(vector), weight);
p3.scale(vector));
}
@Override
public CubicBezierCurve translate(Vector2 vector) {
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
p3.translate(vector), weight);
}
@Override
public CubicBezierCurve setWeight(double weight) {
return new CubicBezierCurve(p0, p1, p2, p3, weight);
p3.translate(vector));
}
}

Wyświetl plik

@ -7,10 +7,9 @@ public final class Ellipse extends Shape {
private final double rotation;
private final Vector2 position;
public Ellipse(double a, double b, double weight, double rotation, Vector2 position) {
public Ellipse(double a, double b, double rotation, Vector2 position) {
this.a = a;
this.b = b;
this.weight = weight;
this.rotation = rotation;
this.position = position;
// Approximation of length.
@ -18,11 +17,11 @@ public final class Ellipse extends Shape {
}
public Ellipse(double a, double b, Vector2 position) {
this(a, b, Shape.DEFAULT_WEIGHT, 0, position);
this(a, b, 0, position);
}
public Ellipse(double a, double b) {
this(a, b, Shape.DEFAULT_WEIGHT, 0, new Vector2());
this(a, b, 0, new Vector2());
}
@Override
@ -40,7 +39,7 @@ public final class Ellipse extends Shape {
theta -= 2 * Math.PI;
}
return new Ellipse(a, b, weight, theta, position);
return new Ellipse(a, b, theta, position);
}
@Override
@ -50,17 +49,12 @@ public final class Ellipse extends Shape {
@Override
public Ellipse scale(Vector2 vector) {
return new Ellipse(a * vector.getX(), b * vector.getY(), weight, rotation,
return new Ellipse(a * vector.getX(), b * vector.getY(), rotation,
position.scale(vector));
}
@Override
public Ellipse translate(Vector2 vector) {
return new Ellipse(a, b, weight, rotation, position.translate(vector));
}
@Override
public Ellipse setWeight(double weight) {
return new Ellipse(a, b, weight, rotation, position);
return new Ellipse(a, b, rotation, position.translate(vector));
}
}

Wyświetl plik

@ -8,21 +8,12 @@ public final class Line extends Shape {
private final Vector2 a;
private final Vector2 b;
public Line(Vector2 a, Vector2 b, double weight) {
public Line(Vector2 a, Vector2 b) {
this.a = a;
this.b = b;
this.weight = weight;
this.length = calculateLength();
}
public Line(Vector2 a, Vector2 b) {
this(a, b, Shape.DEFAULT_WEIGHT);
}
public Line(double x1, double y1, double x2, double y2, double weight) {
this(new Vector2(x1, y1), new Vector2(x2, y2), weight);
}
public Line(double x1, double y1, double x2, double y2) {
this(new Vector2(x1, y1), new Vector2(x2, y2));
}
@ -33,26 +24,26 @@ public final class Line extends Shape {
@Override
public Line rotate(double theta) {
return new Line(a.rotate(theta), b.rotate(theta), weight);
return new Line(a.rotate(theta), b.rotate(theta));
}
@Override
public Line translate(Vector2 vector) {
return new Line(a.translate(vector), b.translate(vector), weight);
return new Line(a.translate(vector), b.translate(vector));
}
@Override
public Line scale(double factor) {
return new Line(a.scale(factor), b.scale(factor), weight);
return new Line(a.scale(factor), b.scale(factor));
}
@Override
public Line scale(Vector2 vector) {
return new Line(a.scale(vector), b.scale(vector), weight);
return new Line(a.scale(vector), b.scale(vector));
}
public Line copy() {
return new Line(a.copy(), b.copy(), weight);
return new Line(a.copy(), b.copy());
}
@Override
@ -114,11 +105,6 @@ public final class Line extends Shape {
return lines;
}
@Override
public Line setWeight(double weight) {
return new Line(getX1(), getY1(), getX2(), getY2(), weight);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {

Wyświetl plik

@ -6,18 +6,13 @@ public class QuadraticBezierCurve extends Shape {
private final Vector2 p1;
private final Vector2 p2;
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) {
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.weight = weight;
this.length = new Line(p0, p2).length;
}
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
this(p0, p1, p2, DEFAULT_WEIGHT);
}
@Override
public Vector2 nextVector(double t) {
return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2)))
@ -26,27 +21,22 @@ public class QuadraticBezierCurve extends Shape {
@Override
public QuadraticBezierCurve rotate(double theta) {
return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight);
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), weight);
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), weight);
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), weight);
}
@Override
public QuadraticBezierCurve setWeight(double weight) {
return new QuadraticBezierCurve(p0, p1, p2, weight);
p2.translate(vector));
}
}

Wyświetl plik

@ -7,9 +7,6 @@ import java.util.List;
public abstract class Shape {
public static final int DEFAULT_WEIGHT = 80;
protected double weight = DEFAULT_WEIGHT;
protected double length;
public abstract Vector2 nextVector(double drawingProgress);
@ -22,12 +19,6 @@ public abstract class Shape {
public abstract Shape translate(Vector2 vector);
public abstract Shape setWeight(double weight);
public double getWeight() {
return weight;
}
public double getLength() {
return length;
}
@ -186,16 +177,15 @@ public abstract class Shape {
return translatedShapes;
}
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start,
double weight) {
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start) {
List<Shape> polygon = new ArrayList<>();
double theta = angleJump * 2 * Math.PI / sides;
Vector2 rotated = start.rotate(theta);
polygon.add(new Line(start, rotated, weight));
polygon.add(new Line(start, rotated));
while (!rotated.equals(start)) {
polygon.add(new Line(rotated.copy(), rotated.rotate(theta), weight));
polygon.add(new Line(rotated.copy(), rotated.rotate(theta)));
rotated = rotated.rotate(theta);
}
@ -203,31 +193,14 @@ public abstract class Shape {
return polygon;
}
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start) {
return generatePolygram(sides, angleJump, start, Line.DEFAULT_WEIGHT);
}
public static List<Shape> generatePolygram(int sides, int angleJump, double scale,
double weight) {
return generatePolygram(sides, angleJump, new Vector2(scale, scale), weight);
}
public static List<Shape> generatePolygram(int sides, int angleJump, double scale) {
return generatePolygram(sides, angleJump, new Vector2(scale, scale));
}
public static List<Shape> generatePolygon(int sides, Vector2 start, double weight) {
return generatePolygram(sides, 1, start, weight);
}
public static List<Shape> generatePolygon(int sides, Vector2 start) {
return generatePolygram(sides, 1, start);
}
public static List<Shape> generatePolygon(int sides, double scale, double weight) {
return generatePolygon(sides, new Vector2(scale, scale), weight);
}
public static List<Shape> generatePolygon(int sides, double scale) {
return generatePolygon(sides, new Vector2(scale, scale));
}

Wyświetl plik

@ -7,18 +7,13 @@ public final class Vector2 extends Shape {
private final double x;
private final double y;
public Vector2(double x, double y, double weight) {
public Vector2(double x, double y) {
this.x = x;
this.y = y;
this.weight = weight;
}
public Vector2(double x, double y) {
this(x, y, Shape.DEFAULT_WEIGHT);
}
public Vector2(double xy) {
this(xy, xy, Shape.DEFAULT_WEIGHT);
this(xy, xy);
}
public Vector2() {
@ -85,11 +80,6 @@ public final class Vector2 extends Shape {
return new Vector2(getX() + vector.getX(), getY() + vector.getY());
}
@Override
public Vector2 setWeight(double weight) {
return new Vector2(x, y, weight);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {

Wyświetl plik

@ -73,24 +73,24 @@
<AnchorPane minHeight="0.0" minWidth="0.0">
<children>
<Slider fx:id="rotateSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="90.0" majorTickUnit="1.0" max="10.0" prefHeight="42.0" prefWidth="254.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="33.0" layoutY="88.0" text="Rotate speed">
<Label layoutX="33.0" layoutY="89.0" text="Rotate speed">
<font>
<Font size="13.0" />
</font>
</Label>
<Slider fx:id="translationSpeedSlider" blockIncrement="0.05" layoutX="116.0" layoutY="128.0" majorTickUnit="1.0" max="10.0" prefHeight="42.0" prefWidth="254.0" showTickLabels="true" showTickMarks="true" />
<Label layoutX="7.0" layoutY="126.0" text="Translation speed">
<Label layoutX="7.0" layoutY="127.0" text="Translation speed">
<font>
<Font size="13.0" />
</font>
</Label>
<Slider fx:id="scaleSlider" blockIncrement="0.05" layoutX="116.0" layoutY="167.0" majorTickUnit="1.0" max="10.0" prefHeight="42.0" prefWidth="253.0" showTickLabels="true" showTickMarks="true" value="1.0" />
<Label layoutX="79.0" layoutY="164.0" text="Scale">
<Label layoutX="79.0" layoutY="166.0" text="Scale">
<font>
<Font size="13.0" />
</font>
</Label>
<Label layoutX="43.0" layoutY="49.0" text="Line weight">
<Label layoutX="11.0" layoutY="50.0" text="Target frequency">
<font>
<Font size="13.0" />
</font>
@ -100,8 +100,8 @@
<Label layoutX="120.0" layoutY="18.0" text="x :" />
<Label layoutX="219.0" layoutY="18.0" text="y :" />
<TextField fx:id="translationYTextField" layoutX="238.0" layoutY="15.0" prefHeight="26.0" prefWidth="70.0" text="0" />
<Slider fx:id="weightSlider" blockIncrement="1.0" layoutX="116.0" layoutY="52.0" majorTickUnit="100.0" max="1000.0" prefHeight="42.0" prefWidth="253.0" showTickLabels="true" showTickMarks="true" value="100.0" />
<SVGPath fx:id="weightMidi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" layoutX="371.0" layoutY="49.0" pickOnBounds="true" />
<Slider fx:id="frequencySlider" blockIncrement="1.0" layoutX="116.0" layoutY="52.0" majorTickUnit="200.0" max="1000.0" prefHeight="42.0" prefWidth="253.0" showTickLabels="true" showTickMarks="true" value="261.63" />
<SVGPath fx:id="frequencyMidi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" layoutX="371.0" layoutY="49.0" pickOnBounds="true" />
<SVGPath fx:id="rotateSpeedMidi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" layoutX="371.0" layoutY="87.0" pickOnBounds="true" />
<SVGPath fx:id="translationSpeedMidi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" layoutX="371.0" layoutY="125.0" pickOnBounds="true" />
<SVGPath fx:id="scaleMidi" content="M20.15 8.26H22V15.74H20.15M13 8.26H18.43C19 8.26 19.3 8.74 19.3 9.3V14.81C19.3 15.5 19 15.74 18.38 15.74H13V11H14.87V13.91H17.5V9.95H13M10.32 8.26H12.14V15.74H10.32M2 8.26H8.55C9.1 8.26 9.41 8.74 9.41 9.3V15.74H7.59V10.15H6.5V15.74H4.87V10.15H3.83V15.74H2Z" fill="WHITE" layoutX="371.0" layoutY="164.0" pickOnBounds="true" />