Add MIDI keyboard support for playing notes

pull/35/head
James Ball 2021-07-15 21:18:59 +01:00
rodzic a9f688aa41
commit 5415a91bf3
5 zmienionych plików z 96 dodań i 26 usunięć

Wyświetl plik

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

Wyświetl plik

@ -25,7 +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 static final double MIN_LENGTH_INCREMENT = 0.0000000001;
private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
@ -74,7 +74,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
if (currentShape >= frame.size()) {
currentShape = 0;
frame = frameQueue.take();
updateTotalAudioFrames();
updateLengthIncrement();
}
return channels;
@ -131,6 +131,12 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
@Override
public void setFrequency(double frequency) {
this.frequency = frequency;
updateLengthIncrement();
}
@Override
public double getFrequency() {
return frequency;
}
private Shape getCurrentShape() {
@ -141,7 +147,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
return frame.get(currentShape);
}
private void updateTotalAudioFrames() {
private void updateLengthIncrement() {
double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate();
lengthIncrement = Math.max(totalLength / (sampleRate / frequency), MIN_LENGTH_INCREMENT);
@ -151,7 +157,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
public void run() {
try {
frame = frameQueue.take();
updateTotalAudioFrames();
updateLengthIncrement();
} catch (InterruptedException e) {
throw new RuntimeException("Initial frame not found. Cannot continue.");
}

Wyświetl plik

@ -21,6 +21,15 @@ public class MidiNote {
return key;
}
public double frequency() {
// Concert A Pitch is A4 and has the key number 69
final int KEY_A4 = 69;
// 440Hz
final int A4 = 440;
// Returns the frequency of the given key (equal temperament)
return (float) (440 * Math.pow(2, (key - KEY_A4) / 12d));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

Wyświetl plik

@ -1,8 +1,8 @@
package sh.ball.gui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.value.WritableValue;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.paint.Color;
@ -49,10 +49,10 @@ import sh.ball.parser.ParserFactory;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class Controller implements Initializable, FrequencyListener, MidiListener, Listener {
public class Controller implements Initializable, FrequencyListener, MidiListener, Listener, WritableValue<Double> {
private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj");
private static final double MAX_FREQUENCY = 20000;
private static final double MAX_FREQUENCY = 12000;
private final FileChooser fileChooser = new FileChooser();
private final DirectoryChooser folderChooser = new DirectoryChooser();
@ -71,6 +71,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private final AudioDevice defaultDevice;
private boolean recording = false;
private Timeline recordingTimeline;
private Timeline volumeTimeline;
private Paint armedMidiPaint;
private SVGPath armedMidi;
@ -172,6 +173,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
@FXML
private ComboBox<AudioDevice> deviceComboBox;
public Controller(AudioPlayer<List<Shape>> audioPlayer) throws IOException {
this.audioPlayer = audioPlayer;
FrameSet<List<Shape>> frames = new ObjParser(DEFAULT_OBJ).parse();
@ -617,26 +619,63 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
return (double) tmp / factor;
}
private double midiPressureToPressure(Slider slider, int midiPressure) {
double max = slider.getMax();
double min = slider.getMin();
double range = max - min;
return min + (midiPressure / 127.0) * range;
}
@Override
public void sendMidiMessage(int status, MidiNote note, int pressure) {
if (armedMidi != null) {
if (midiMap.containsValue(armedMidi)) {
midiMap.values().remove(armedMidi);
public void sendMidiMessage(int status, MidiNote note, int midiPressure) {
double frequency = note.frequency();
if (frequency > 32 && frequency < 8000) {
double scale = midiPressureToPressure(scaleSlider, midiPressure);
scale /= 10;
if (midiPressure == 0) {
KeyValue kv = new KeyValue(scaleSlider.valueProperty(), 0, Interpolator.EASE_OUT);
KeyFrame kf = new KeyFrame(Duration.millis(500), kv);
volumeTimeline = new Timeline(kf);
Platform.runLater(volumeTimeline::play);
} else {
if (volumeTimeline != null) {
volumeTimeline.stop();
}
frequencySlider.setValue(Math.log(frequency) / Math.log(MAX_FREQUENCY));
audioPlayer.setFrequency(frequency);
scaleSlider.setValue(scale);
}
} else {
if (armedMidi != null) {
if (midiMap.containsValue(armedMidi)) {
midiMap.values().remove(armedMidi);
}
if (midiMap.containsKey(note)) {
midiMap.get(note).setFill(Color.color(1, 1, 1));
}
midiMap.put(note, armedMidi);
armedMidi.setFill(Color.color(0, 1, 0));
armedMidiPaint = null;
armedMidi = null;
}
if (midiMap.containsKey(note)) {
midiMap.get(note).setFill(Color.color(1, 1, 1));
Slider slider = midiButtonMap.get(midiMap.get(note));
slider.setValue(midiPressureToPressure(slider, midiPressure));
}
midiMap.put(note, armedMidi);
armedMidi.setFill(Color.color(0, 1, 0));
armedMidiPaint = null;
armedMidi = null;
}
if (midiMap.containsKey(note)) {
Slider slider = midiButtonMap.get(midiMap.get(note));
double max = slider.getMax();
double min = slider.getMin();
double range = max - min;
slider.setValue(min + (pressure / 127.0) * range);
}
}
// gets the volume/scale
@Override
public Double getValue() {
return scaleSlider.getValue();
}
@Override
public void setValue(Double scale) {
scaleSlider.setValue(scale);
}
}

Wyświetl plik

@ -100,11 +100,25 @@
<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="frequencySlider" blockIncrement="0.001" layoutX="116.0" layoutY="45.0" majorTickUnit="0.1" max="1.0" prefHeight="42.0" prefWidth="253.0" showTickMarks="true" value="0.56212" />
<Slider fx:id="frequencySlider" blockIncrement="0.001" layoutX="116.0" layoutY="45.0" majorTickUnit="0.07389" max="1.0" minorTickCount="2" prefHeight="42.0" prefWidth="253.0" showTickMarks="true" value="0.59269" />
<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" />
<Label layoutX="257.0" layoutY="71.0" text="C4" />
<Label layoutX="239.0" layoutY="71.0" text="C3" />
<Label layoutX="222.0" layoutY="71.0" text="C2" />
<Label layoutX="193.0" layoutY="71.0" text="20" />
<Label layoutX="274.0" layoutY="71.0" text="C5" />
<Label layoutX="292.0" layoutY="71.0" text="C6" />
<Label layoutX="310.0" layoutY="71.0" text="C7" />
<Label layoutX="344.0" layoutY="71.0" text="12000">
<font>
<Font size="11.0" />
</font>
</Label>
<Label layoutX="162.0" layoutY="71.0" text="5" />
<Label layoutX="122.0" layoutY="71.0" text="0" />
</children>
</AnchorPane>
</content>