Refactor MIDI code out of Controller and into ShapeAudioPlayer

pull/35/head
James Ball 2021-08-30 23:36:12 +01:00
rodzic ce0dca7179
commit cdc5139141
4 zmienionych plików z 170 dodań i 127 usunięć

Wyświetl plik

@ -2,11 +2,12 @@ package sh.ball.audio;
import sh.ball.audio.effect.Effect;
import sh.ball.audio.engine.AudioDevice;
import sh.ball.audio.midi.MidiListener;
import javax.sound.sampled.AudioInputStream;
import java.util.List;
public interface AudioPlayer<S> extends Runnable {
public interface AudioPlayer<S> extends Runnable, MidiListener {
void reset() throws Exception;
@ -18,8 +19,6 @@ public interface AudioPlayer<S> extends Runnable {
void setMainFrequencyScale(double scale);
void setBaseFrequencies(List<Double> frequency);
void setPitchBendFactor(double pitchBend);
List<Double> getFrequencies();

Wyświetl plik

@ -1,5 +1,13 @@
package sh.ball.audio;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.util.Duration;
import sh.ball.audio.effect.Effect;
import java.io.*;
@ -10,9 +18,12 @@ 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.audio.midi.MidiCommunicator;
import sh.ball.audio.midi.MidiNote;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
import javax.sound.midi.ShortMessage;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
@ -30,6 +41,13 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private static final double MIN_LENGTH_INCREMENT = 0.0000000001;
private static final double MIDDLE_C = 261.63;
// MIDI
private static final int PITCH_BEND_DATA_LENGTH = 7;
private static final int PITCH_BEND_MAX = 16383;
private static final int PITCH_BEND_SEMITONES = 2;
private final List<MidiNote> downKeys = new CopyOnWriteArrayList<>();
private Timeline volumeTimeline;
private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
private final Map<Object, Effect> effects = new ConcurrentHashMap<>();
@ -45,19 +63,35 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private double lengthIncrement = MIN_LENGTH_INCREMENT;
private double lengthDrawn = 0;
private int count = 0;
private DoubleProperty volume;
private List<Double> frequencies = List.of(MIDDLE_C);
private double mainFrequency = MIDDLE_C;
private double octaveFrequency;
private DoubleProperty frequency;
private double mainFrequencyScale = 0.5;
private double maxFrequency = MIDDLE_C;
private double maxFrequency;
private double pitchBend = 1.0;
private double trace = 0.5;
private int octave = 0;
private AudioDevice device;
public ShapeAudioPlayer(Callable<AudioEngine> audioEngineBuilder) throws Exception {
public ShapeAudioPlayer(Callable<AudioEngine> audioEngineBuilder, MidiCommunicator communicator) throws Exception {
this.audioEngineBuilder = audioEngineBuilder;
this.audioEngine = audioEngineBuilder.call();
setFrequency(new SimpleDoubleProperty(0));
this.maxFrequency = frequency.get();
setFrequency(new SimpleDoubleProperty(0));
communicator.addListener(this);
}
public void setFrequency(DoubleProperty frequency) {
this.frequency = frequency;
frequency.addListener((o, old, f) -> setBaseFrequencies(List.of(f.doubleValue())));
}
public void setVolume(DoubleProperty volume) {
this.volume = volume;
}
private Vector2 generateChannels() throws InterruptedException {
@ -143,14 +177,14 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
for (Effect effect : effects.values()) {
vector = effect.apply(frame, vector);
}
return vector;
return vector.scale(volume.get());
}
@Override
public void setBaseFrequencies(List<Double> frequencies) {
private void setBaseFrequencies(List<Double> frequencies) {
this.frequencies = frequencies;
this.maxFrequency = frequencies.stream().max(Double::compareTo).get();
this.mainFrequency = maxFrequency * Math.pow(2, octave - 1);
octaveFrequency = maxFrequency * Math.pow(2, octave - 1);
updateLengthIncrement();
sineEffects.clear();
@ -186,10 +220,12 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
private void updateLengthIncrement() {
double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate();
double actualFrequency = mainFrequency * pitchBend;
lengthIncrement = Math.max(totalLength / (sampleRate / actualFrequency), MIN_LENGTH_INCREMENT);
if (frame != null) {
double totalLength = Shape.totalLength(frame);
int sampleRate = device.sampleRate();
double actualFrequency = octaveFrequency * pitchBend;
lengthIncrement = Math.max(totalLength / (sampleRate / actualFrequency), MIN_LENGTH_INCREMENT);
}
}
@Override
@ -234,7 +270,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
@Override
public void setOctave(int octave) {
this.octave = octave;
this.mainFrequency = maxFrequency * Math.pow(2, octave - 1);
octaveFrequency = maxFrequency * Math.pow(2, octave - 1);
}
@Override
@ -311,6 +347,63 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
return new AudioInputStream(new ByteArrayInputStream(input), audioFormat, framesRecorded);
}
private void playNotes(double noteVolume) {
List<Double> frequencies = downKeys.stream().map(MidiNote::frequency).collect(Collectors.toList());
double mainFrequency = frequencies.get(frequencies.size() - 1);
frequency.set(mainFrequency);
setBaseFrequencies(frequencies);
volume.set(noteVolume);
}
@Override
public void sendMidiMessage(ShortMessage message) {
int command = message.getCommand();
if (command == ShortMessage.NOTE_ON || command == ShortMessage.NOTE_OFF) {
MidiNote note = new MidiNote(message.getData1());
int velocity = message.getData2();
double oldVolume = volume.get();
double newVolume = velocity / 127.0;
if (command == ShortMessage.NOTE_OFF) {
downKeys.remove(note);
if (downKeys.isEmpty()) {
KeyValue kv = new KeyValue(volume, 0, Interpolator.EASE_OUT);
KeyFrame kf = new KeyFrame(Duration.millis(500), kv);
volumeTimeline = new Timeline(kf);
Platform.runLater(volumeTimeline::play);
} else {
playNotes(oldVolume);
}
} else {
downKeys.add(note);
if (volumeTimeline != null) {
volumeTimeline.stop();
volumeTimeline = null;
}
playNotes(newVolume);
KeyValue kv = new KeyValue(volume, volume.get() * 0.75, Interpolator.EASE_OUT);
KeyFrame kf = new KeyFrame(Duration.millis(250), kv);
volumeTimeline = new Timeline(kf);
Platform.runLater(volumeTimeline::play);
}
} else if (command == ShortMessage.PITCH_BEND) {
// using these instructions https://sites.uci.edu/camp2014/2014/04/30/managing-midi-pitchbend-messages/
int pitchBend = (message.getData2() << PITCH_BEND_DATA_LENGTH) | message.getData1();
// get pitch bend in range -1 to 1
double pitchBendFactor = (double) pitchBend / PITCH_BEND_MAX;
pitchBendFactor = 2 * pitchBendFactor - 1;
pitchBendFactor *= PITCH_BEND_SEMITONES;
// 12 tone equal temperament
pitchBendFactor /= 12;
pitchBendFactor = Math.pow(2, pitchBendFactor);
setPitchBendFactor(pitchBendFactor);
}
}
private static class Listener {
private final byte[] buffer;
private final Semaphore sema;

Wyświetl plik

@ -2,6 +2,8 @@ package sh.ball.gui;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.WritableValue;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
@ -41,6 +43,7 @@ import org.xml.sax.SAXException;
import sh.ball.audio.effect.Effect;
import sh.ball.audio.effect.EffectType;
import sh.ball.audio.engine.AudioDevice;
import sh.ball.audio.engine.ConglomerateAudioEngine;
import sh.ball.audio.midi.MidiCommunicator;
import sh.ball.audio.midi.MidiListener;
import sh.ball.audio.midi.MidiNote;
@ -52,33 +55,30 @@ import sh.ball.parser.ParserFactory;
import sh.ball.shapes.Shape;
import sh.ball.shapes.Vector2;
public class Controller implements Initializable, FrequencyListener, MidiListener, Listener, WritableValue<Double> {
public class Controller implements Initializable, FrequencyListener, Listener, WritableValue<Double>, MidiListener {
private static final InputStream DEFAULT_OBJ = Controller.class.getResourceAsStream("/models/cube.obj");
private static final double MAX_FREQUENCY = 12000;
private static final int PITCH_BEND_DATA_LENGTH = 7;
private static final int PITCH_BEND_MAX = 16383;
private static final int PITCH_BEND_SEMITONES = 2;
private static final double MIDDLE_C = 261.63;
private final FileChooser fileChooser = new FileChooser();
private final DirectoryChooser folderChooser = new DirectoryChooser();
private final AudioPlayer<List<Shape>> audioPlayer;
private final ShapeAudioPlayer audioPlayer;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Map<Integer, SVGPath> CCMap = new HashMap<>();
private final List<MidiNote> downKeys = new CopyOnWriteArrayList<>();
private Map<SVGPath, Slider> midiButtonMap;
private final RotateEffect rotateEffect;
private final TranslateEffect translateEffect;
private final WobbleEffect wobbleEffect;
private final ScaleEffect scaleEffect;
private final DoubleProperty frequency;
private int sampleRate;
private FrequencyAnalyser<List<Shape>> analyser;
private final AudioDevice defaultDevice;
private boolean recording = false;
private Timeline recordingTimeline;
private Timeline volumeTimeline;
private Paint armedMidiPaint;
private SVGPath armedMidi;
@ -188,8 +188,12 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
@FXML
private ComboBox<AudioDevice> deviceComboBox;
public Controller(AudioPlayer<List<Shape>> audioPlayer) throws IOException {
this.audioPlayer = audioPlayer;
public Controller() throws Exception {
MidiCommunicator midiCommunicator = new MidiCommunicator();
midiCommunicator.addListener(this);
new Thread(midiCommunicator).start();
this.audioPlayer = new ShapeAudioPlayer(ConglomerateAudioEngine::new, midiCommunicator);
FrameSet<List<Shape>> frames = new ObjParser(DEFAULT_OBJ).parse();
frameSets.add(frames);
frameSetPaths.add("cube.obj");
@ -204,7 +208,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
this.rotateEffect = new RotateEffect(sampleRate);
this.translateEffect = new TranslateEffect(sampleRate);
this.wobbleEffect = new WobbleEffect(sampleRate);
this.scaleEffect = new ScaleEffect();
this.frequency = new SimpleDoubleProperty(0);
}
private Map<SVGPath, Slider> initializeMidiButtonMap() {
@ -227,10 +231,8 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
private Map<Slider, Consumer<Double>> initializeSliderMap() {
return Map.of(
frequencySlider, f -> audioPlayer.setBaseFrequencies(List.of(Math.pow(MAX_FREQUENCY, f))),
rotateSpeedSlider, rotateEffect::setSpeed,
translationSpeedSlider, translateEffect::setSpeed,
scaleSlider, scaleEffect::setScale,
focalLengthSlider, d -> updateFocalLength(),
objectRotateSpeedSlider, d -> updateObjectRotateSpeed(),
visibilitySlider, audioPlayer::setMainFrequencyScale
@ -256,6 +258,12 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
frequencySlider.valueProperty().addListener((o, old, f) -> frequency.set(Math.pow(MAX_FREQUENCY, f.doubleValue())));
frequency.addListener((o, old, f) -> frequencySlider.setValue(Math.log(f.doubleValue()) / Math.log(MAX_FREQUENCY)));
audioPlayer.setFrequency(frequency);
frequency.set(MIDDLE_C);
audioPlayer.setVolume(scaleSlider.valueProperty());
this.midiButtonMap = initializeMidiButtonMap();
midiButtonMap.keySet().forEach(midi -> midi.setOnMouseClicked(e -> {
@ -271,7 +279,7 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
}
armedMidiPaint = midi.getFill();
armedMidi = midi;
midi.setFill(Color.color(1, 0, 0));
midi.setFill(Color.RED);
}
}));
@ -360,7 +368,6 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
updateObjectRotateSpeed();
audioPlayer.addEffect(EffectType.SCALE, scaleEffect);
audioPlayer.addEffect(EffectType.ROTATE, rotateEffect);
audioPlayer.addEffect(EffectType.TRANSLATE, translateEffect);
@ -373,9 +380,6 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
analyser = new FrequencyAnalyser<>(audioPlayer, 2, sampleRate);
startFrequencyAnalyser(analyser);
startAudioPlayerThread();
MidiCommunicator midiCommunicator = new MidiCommunicator();
midiCommunicator.addListener(this);
new Thread(midiCommunicator).start();
deviceComboBox.valueProperty().addListener((options, oldDevice, newDevice) -> {
if (newDevice != null) {
@ -639,99 +643,6 @@ 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;
}
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);
}
@Override
public void sendMidiMessage(ShortMessage message) {
Platform.runLater(() -> {
int command = message.getCommand();
if (command == ShortMessage.CONTROL_CHANGE) {
int id = message.getData1();
int value = message.getData2();
if (armedMidi != null) {
if (CCMap.containsValue(armedMidi)) {
CCMap.values().remove(armedMidi);
}
if (CCMap.containsKey(id)) {
CCMap.get(id).setFill(Color.color(1, 1, 1));
}
CCMap.put(id, armedMidi);
armedMidi.setFill(Color.color(0, 1, 0));
armedMidiPaint = null;
armedMidi = null;
}
if (CCMap.containsKey(id)) {
Slider slider = midiButtonMap.get(CCMap.get(id));
double sliderValue = midiPressureToPressure(slider, value);
if (slider.isSnapToTicks()) {
double increment = slider.getMajorTickUnit() / (slider.getMinorTickCount() + 1);
sliderValue = increment * (Math.round(sliderValue / increment));
}
slider.setValue(sliderValue);
}
} else if (command == ShortMessage.NOTE_ON || command == ShortMessage.NOTE_OFF) {
MidiNote note = new MidiNote(message.getData1());
int velocity = message.getData2();
double oldVolume = scaleSlider.getValue();
double volume = midiPressureToPressure(scaleSlider, velocity);
volume /= 10;
if (command == ShortMessage.NOTE_OFF) {
downKeys.remove(note);
if (downKeys.isEmpty()) {
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 {
playNotes(oldVolume);
}
} else {
downKeys.add(note);
if (volumeTimeline != null) {
volumeTimeline.stop();
volumeTimeline = null;
}
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);
Platform.runLater(volumeTimeline::play);
}
} else if (command == ShortMessage.PITCH_BEND) {
// using these instructions https://sites.uci.edu/camp2014/2014/04/30/managing-midi-pitchbend-messages/
int pitchBend = (message.getData2() << PITCH_BEND_DATA_LENGTH) | message.getData1();
// get pitch bend in range -1 to 1
double pitchBendFactor = (double) pitchBend / PITCH_BEND_MAX;
pitchBendFactor = 2 * pitchBendFactor - 1;
pitchBendFactor *= PITCH_BEND_SEMITONES;
// 12 tone equal temperament
pitchBendFactor /= 12;
pitchBendFactor = Math.pow(2, pitchBendFactor);
audioPlayer.setPitchBendFactor(pitchBendFactor);
}
});
}
// gets the volume/scale
@Override
public Double getValue() {
@ -742,4 +653,44 @@ public class Controller implements Initializable, FrequencyListener, MidiListene
public void setValue(Double scale) {
scaleSlider.setValue(scale);
}
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(ShortMessage message) {
int command = message.getCommand();
if (command == ShortMessage.CONTROL_CHANGE) {
int id = message.getData1();
int value = message.getData2();
if (armedMidi != null) {
if (CCMap.containsValue(armedMidi)) {
CCMap.values().remove(armedMidi);
}
if (CCMap.containsKey(id)) {
CCMap.get(id).setFill(Color.WHITE);
}
CCMap.put(id, armedMidi);
armedMidi.setFill(Color.LIME);
armedMidiPaint = null;
armedMidi = null;
}
if (CCMap.containsKey(id)) {
Slider slider = midiButtonMap.get(CCMap.get(id));
double sliderValue = midiPressureToPressure(slider, value);
if (slider.isSnapToTicks()) {
double increment = slider.getMajorTickUnit() / (slider.getMinorTickCount() + 1);
sliderValue = increment * (Math.round(sliderValue / increment));
}
slider.setValue(sliderValue);
}
}
}
}

Wyświetl plik

@ -24,7 +24,7 @@ public class Gui extends Application {
System.setProperty("prism.lcdtext", "false");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/osci-render.fxml"));
Controller controller = new Controller(new ShapeAudioPlayer(ConglomerateAudioEngine::new));
Controller controller = new Controller();
loader.setController(controller);
Parent root = loader.load();