kopia lustrzana https://github.com/jameshball/osci-render
WIP: Add channels to MIDI playback
rodzic
0afd92009c
commit
df9ec401e4
|
@ -22,6 +22,8 @@ import javax.sound.midi.ShortMessage;
|
|||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
|
||||
import static sh.ball.gui.Gui.audioPlayer;
|
||||
|
||||
public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
||||
|
||||
private static final double MIN_TRACE = 0.001;
|
||||
|
@ -35,13 +37,13 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
// Stereo audio
|
||||
private static final int NUM_OUTPUTS = 2;
|
||||
private static final double MIN_LENGTH_INCREMENT = 0.0000000001;
|
||||
public static final double EPSILON = 0.001;
|
||||
|
||||
// MIDI
|
||||
private final short[] keyTargetVolumes = new short[128];
|
||||
private final short[] keyActualVolumes = new short[128];
|
||||
private final Set<Integer> keysDown = ConcurrentHashMap.newKeySet();
|
||||
private final short[][] keyTargetVolumes = new short[16][128];
|
||||
private final short[][] keyActualVolumes = new short[16][128];
|
||||
private final Set<MidiNote> keysDown = ConcurrentHashMap.newKeySet();
|
||||
private boolean midiStarted = false;
|
||||
private int mainChannel = 0;
|
||||
private int baseKey = 60;
|
||||
private double pitchBend = 1.0;
|
||||
private int lastDecay = 0;
|
||||
|
@ -54,7 +56,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 Map<MidiNote, SineEffect> sineEffects = new ConcurrentHashMap<>();
|
||||
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
private AudioEngine audioEngine;
|
||||
|
@ -89,13 +91,25 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
}
|
||||
|
||||
private void resetMidi() {
|
||||
keysDown.clear();
|
||||
for (int i = 0; i < keyTargetVolumes.length; i++) {
|
||||
Arrays.fill(keyTargetVolumes[i], (short) 0);
|
||||
Arrays.fill(keyActualVolumes[i], (short) 0);
|
||||
}
|
||||
// Middle C is down by default
|
||||
keyTargetVolumes[60] = (short) MidiNote.MAX_VELOCITY;
|
||||
keyActualVolumes[60] = (short) MidiNote.MAX_VELOCITY;
|
||||
keysDown.add(60);
|
||||
keyTargetVolumes[0][60] = (short) MidiNote.MAX_VELOCITY;
|
||||
keyActualVolumes[0][60] = (short) MidiNote.MAX_VELOCITY;
|
||||
keysDown.add(new MidiNote(60));
|
||||
midiStarted = false;
|
||||
}
|
||||
|
||||
public void stopMidiNotes() {
|
||||
keysDown.clear();
|
||||
for (short[] keyTargetVolume : keyTargetVolumes) {
|
||||
Arrays.fill(keyTargetVolume, (short) 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFrequency(DoubleProperty frequency) {
|
||||
this.frequency = frequency;
|
||||
frequency.addListener((o, old, f) -> setBaseFrequency(f.doubleValue()));
|
||||
|
@ -192,12 +206,14 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
}
|
||||
|
||||
private Vector2 applyEffects(int frame, Vector2 vector) {
|
||||
vector = vector.scale(2 * baseFrequencyVolumeScale * keyActualVolumes[baseKey] / MidiNote.MAX_VELOCITY);
|
||||
vector = vector.scale(2 * baseFrequencyVolumeScale * keyActualVolumes[mainChannel][baseKey] / MidiNote.MAX_VELOCITY);
|
||||
if (midiStarted) {
|
||||
if (lastDecay > decayFrames) {
|
||||
for (int i = 0; i < keyActualVolumes.length; i++) {
|
||||
if (keyActualVolumes[i] > keyTargetVolumes[i]) {
|
||||
keyActualVolumes[i]--;
|
||||
for (int j = 0; j < keyActualVolumes[i].length; j++) {
|
||||
if (keyActualVolumes[i][j] > keyTargetVolumes[i][j]) {
|
||||
keyActualVolumes[i][j]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastDecay = 0;
|
||||
|
@ -205,17 +221,18 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
lastDecay++;
|
||||
if (lastAttack > attackFrames) {
|
||||
for (int i = 0; i < keyActualVolumes.length; i++) {
|
||||
if (keyActualVolumes[i] < keyTargetVolumes[i]) {
|
||||
keyActualVolumes[i]++;
|
||||
for (int j = 0; j < keyActualVolumes[i].length; j++) {
|
||||
if (keyActualVolumes[i][j] < keyTargetVolumes[i][j]) {
|
||||
keyActualVolumes[i][j]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastAttack = 0;
|
||||
}
|
||||
lastAttack++;
|
||||
for (int key : keysDown) {
|
||||
double frequency = new MidiNote(key).frequency();
|
||||
if (Math.abs(frequency - baseFrequency) > EPSILON) {
|
||||
vector = sineEffects.get(key).apply(frame, vector);
|
||||
for (MidiNote note : keysDown) {
|
||||
if (note.channel() != mainChannel) {
|
||||
vector = sineEffects.get(note).apply(frame, vector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -331,6 +348,11 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
updateSineEffects();
|
||||
}
|
||||
|
||||
public void setMainMidiChannel(int channel) {
|
||||
this.mainChannel = channel;
|
||||
notesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFrame(List<Shape> frame) {
|
||||
try {
|
||||
|
@ -371,8 +393,11 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
this.device = device;
|
||||
sineEffects.clear();
|
||||
this.sampleRate = device.sampleRate();
|
||||
for (int i = 0; i < keyActualVolumes.length; i++) {
|
||||
sineEffects.add(new SineEffect(sampleRate, new MidiNote(i).frequency(), keyActualVolumes[i] / MidiNote.MAX_VELOCITY));
|
||||
for (int channel = 0; channel < keyActualVolumes.length; channel++) {
|
||||
for (int key = 0; key < keyActualVolumes[channel].length; key++) {
|
||||
MidiNote note = new MidiNote(key, channel);
|
||||
sineEffects.put(note, new SineEffect(sampleRate, note.frequency(), keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY));
|
||||
}
|
||||
}
|
||||
for (Effect effect : effects.values()) {
|
||||
if (effect instanceof PhaseEffect phase) {
|
||||
|
@ -422,35 +447,37 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
|
||||
private void updateSineEffects() {
|
||||
double totalVolume = 0;
|
||||
for (short volume : keyActualVolumes) {
|
||||
totalVolume += volume / MidiNote.MAX_VELOCITY;
|
||||
for (short[] volumes : keyActualVolumes) {
|
||||
for (short volume : volumes) {
|
||||
totalVolume += volume / MidiNote.MAX_VELOCITY;
|
||||
}
|
||||
}
|
||||
if (totalVolume != 0) {
|
||||
double scaledVolume = (1 - baseFrequencyVolumeScale) / totalVolume;
|
||||
for (int i = 0; i < keyActualVolumes.length; i++) {
|
||||
double frequency = new MidiNote(i).frequency();
|
||||
if (keyActualVolumes[i] > 0 && sineEffects.size() != 0) {
|
||||
SineEffect effect = sineEffects.get(i);
|
||||
effect.setVolume(scaledVolume * keyActualVolumes[i] / MidiNote.MAX_VELOCITY);
|
||||
effect.setFrequency(frequency * pitchBend);
|
||||
for (int channel = 0; channel < keyActualVolumes.length; channel++) {
|
||||
for (int key = 0; key < keyActualVolumes[channel].length; key++) {
|
||||
MidiNote note = new MidiNote(key, channel);
|
||||
if (keyActualVolumes[channel][key] > 0 && sineEffects.size() != 0) {
|
||||
SineEffect effect = sineEffects.get(note);
|
||||
effect.setVolume(scaledVolume * keyActualVolumes[channel][key] / MidiNote.MAX_VELOCITY);
|
||||
effect.setFrequency(note.frequency() * pitchBend);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notesChanged() {
|
||||
int loudestKey = 0;
|
||||
int maxVelocity = 0;
|
||||
for (int i = 0; i < keyTargetVolumes.length; i++) {
|
||||
if (keyTargetVolumes[i] > maxVelocity && keysDown.contains(i)) {
|
||||
loudestKey = i;
|
||||
maxVelocity = keyTargetVolumes[i];
|
||||
MidiNote note = null;
|
||||
for (int key = 0; key < keyTargetVolumes[mainChannel].length; key++) {
|
||||
note = new MidiNote(key, mainChannel);
|
||||
if (keysDown.contains(note)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (maxVelocity > 0) {
|
||||
double baseFrequency = new MidiNote(loudestKey).frequency();
|
||||
frequency.set(baseFrequency);
|
||||
baseKey = loudestKey;
|
||||
if (note != null) {
|
||||
frequency.set(note.frequency());
|
||||
baseKey = note.key();
|
||||
}
|
||||
|
||||
updateSineEffects();
|
||||
|
@ -472,17 +499,30 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
|
|||
keysDown.clear();
|
||||
midiStarted = true;
|
||||
}
|
||||
MidiNote note = new MidiNote(message.getData1());
|
||||
MidiNote note = new MidiNote(message.getData1(), message.getChannel());
|
||||
int velocity = message.getData2();
|
||||
|
||||
if (command == ShortMessage.NOTE_OFF) {
|
||||
keyTargetVolumes[note.key()] = 0;
|
||||
keysDown.remove(note.key());
|
||||
keyTargetVolumes[note.channel()][note.key()] = 0;
|
||||
keysDown.remove(note);
|
||||
} else {
|
||||
keyTargetVolumes[note.key()] = (short) velocity;
|
||||
keysDown.add(note.key());
|
||||
keyTargetVolumes[note.channel()][note.key()] = (short) velocity;
|
||||
keysDown.add(note);
|
||||
}
|
||||
notesChanged();
|
||||
} 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() << MidiNote.PITCH_BEND_DATA_LENGTH) | message.getData1();
|
||||
// get pitch bend in range -1 to 1
|
||||
double pitchBendFactor = (double) pitchBend / MidiNote.PITCH_BEND_MAX;
|
||||
pitchBendFactor = 2 * pitchBendFactor - 1;
|
||||
pitchBendFactor *= MidiNote.PITCH_BEND_SEMITONES;
|
||||
// 12 tone equal temperament
|
||||
pitchBendFactor /= 12;
|
||||
pitchBendFactor = Math.pow(2, pitchBendFactor);
|
||||
|
||||
audioPlayer.setPitchBendFactor(pitchBendFactor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@ import java.util.Objects;
|
|||
public class MidiNote {
|
||||
|
||||
public static double MAX_VELOCITY = 127;
|
||||
public static short MAX_CC = 0x78;
|
||||
public static short MAX_CHANNEL = 15;
|
||||
public static short ALL_NOTES_OFF = 0x7B;
|
||||
public static final double MIDDLE_C = 261.6255798;
|
||||
public static final int PITCH_BEND_DATA_LENGTH = 7;
|
||||
public static final int PITCH_BEND_MAX = 16383;
|
||||
|
@ -19,14 +22,20 @@ public class MidiNote {
|
|||
private final String name;
|
||||
private final int key;
|
||||
private final int octave;
|
||||
private final int channel;
|
||||
|
||||
public MidiNote(int key) {
|
||||
public MidiNote(int key, int channel) {
|
||||
this.key = key;
|
||||
this.channel = channel;
|
||||
this.octave = (key / 12)-1;
|
||||
int note = key % 12;
|
||||
this.name = NOTE_NAMES[note];
|
||||
}
|
||||
|
||||
public MidiNote(int key) {
|
||||
this(key, 0);
|
||||
}
|
||||
|
||||
public int key() {
|
||||
return key;
|
||||
}
|
||||
|
@ -36,17 +45,21 @@ public class MidiNote {
|
|||
return (float) (A4 * Math.pow(2, (key - KEY_A4) / 12d));
|
||||
}
|
||||
|
||||
public int channel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
MidiNote midiNote = (MidiNote) o;
|
||||
return key == midiNote.key;
|
||||
return key == midiNote.key && channel == midiNote.channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(key);
|
||||
return Objects.hash(key, channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,6 +5,7 @@ import javafx.scene.control.*;
|
|||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
@ -12,8 +13,11 @@ import sh.ball.audio.*;
|
|||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
|
@ -88,6 +92,12 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
|
|||
private MenuItem saveProjectMenuItem;
|
||||
@FXML
|
||||
private MenuItem saveAsProjectMenuItem;
|
||||
@FXML
|
||||
private MenuItem resetMidiMappingMenuItem;
|
||||
@FXML
|
||||
private MenuItem stopMidiNotesMenuItem;
|
||||
@FXML
|
||||
private Spinner<Integer> midiChannelSpinner;
|
||||
|
||||
public MainController() throws Exception {
|
||||
// Clone DEFAULT_OBJ InputStream using a ByteArrayOutputStream
|
||||
|
@ -107,9 +117,9 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
|
|||
}
|
||||
this.sampleRate = defaultDevice.sampleRate();
|
||||
}
|
||||
|
||||
// initialises midiButtonMap by mapping MIDI logo SVGs to the slider that they
|
||||
// control if they are selected.
|
||||
|
||||
private Map<SVGPath, Slider> initializeMidiButtonMap() {
|
||||
Map<SVGPath, Slider> midiMap = new HashMap<>();
|
||||
subControllers().forEach(controller -> midiMap.putAll(controller.getMidiButtonMap()));
|
||||
|
@ -177,6 +187,29 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
|
|||
}
|
||||
});
|
||||
|
||||
resetMidiMappingMenuItem.setOnAction(e -> resetCCMap());
|
||||
|
||||
stopMidiNotesMenuItem.setOnAction(e -> audioPlayer.stopMidiNotes());
|
||||
|
||||
NumberFormat format = NumberFormat.getIntegerInstance();
|
||||
UnaryOperator<TextFormatter.Change> filter = c -> {
|
||||
if (c.isContentChange()) {
|
||||
ParsePosition parsePosition = new ParsePosition(0);
|
||||
// NumberFormat evaluates the beginning of the text
|
||||
format.parse(c.getControlNewText(), parsePosition);
|
||||
if (parsePosition.getIndex() == 0 ||
|
||||
parsePosition.getIndex() < c.getControlNewText().length()) {
|
||||
// reject parsing the complete text failed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
};
|
||||
|
||||
midiChannelSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, MidiNote.MAX_CHANNEL));
|
||||
midiChannelSpinner.getEditor().setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, filter));
|
||||
midiChannelSpinner.valueProperty().addListener((o, oldValue, newValue) -> audioPlayer.setMainMidiChannel(newValue));
|
||||
|
||||
objController.updateObjectRotateSpeed();
|
||||
|
||||
switchAudioDevice(defaultDevice, false);
|
||||
|
@ -388,7 +421,7 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
|
|||
}
|
||||
// If there is a slider associated with the MIDI channel, update the value
|
||||
// of it
|
||||
if (CCMap.containsKey(cc)) {
|
||||
if (cc <= MidiNote.MAX_CC && CCMap.containsKey(cc)) {
|
||||
Slider slider = midiButtonMap.get(CCMap.get(cc));
|
||||
double sliderValue = midiPressureToPressure(slider, value);
|
||||
|
||||
|
@ -397,23 +430,12 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
|
|||
sliderValue = increment * (Math.round(sliderValue / increment));
|
||||
}
|
||||
slider.setValue(sliderValue);
|
||||
} else if (cc == MidiNote.ALL_NOTES_OFF) {
|
||||
audioPlayer.stopMidiNotes();
|
||||
}
|
||||
} else if (command == ShortMessage.PROGRAM_CHANGE) {
|
||||
// We want to change the file that is currently playing
|
||||
Platform.runLater(() -> changeFrameSource(message.getMessage()[1]));
|
||||
} 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() << MidiNote.PITCH_BEND_DATA_LENGTH) | message.getData1();
|
||||
// get pitch bend in range -1 to 1
|
||||
double pitchBendFactor = (double) pitchBend / MidiNote.PITCH_BEND_MAX;
|
||||
pitchBendFactor = 2 * pitchBendFactor - 1;
|
||||
pitchBendFactor *= MidiNote.PITCH_BEND_SEMITONES;
|
||||
// 12 tone equal temperament
|
||||
pitchBendFactor /= 12;
|
||||
pitchBendFactor = Math.pow(2, pitchBendFactor);
|
||||
|
||||
audioPlayer.setPitchBendFactor(pitchBendFactor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.CustomMenuItem?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.Spinner?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.input.KeyCodeCombination?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
|
||||
<AnchorPane prefHeight="658.0" prefWidth="837.0" stylesheets="@../css/main.css" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sh.ball.gui.controller.MainController">
|
||||
<TitledPane animated="false" collapsible="false" layoutX="424.0"
|
||||
layoutY="38.0" prefHeight="387.0" prefWidth="402.0"
|
||||
text="Audio Effects">
|
||||
<fx:include fx:id="effects" source="effects.fxml"/>
|
||||
<TitledPane animated="false" collapsible="false" layoutX="424.0" layoutY="38.0" prefHeight="387.0" prefWidth="402.0" text="Audio Effects">
|
||||
<fx:include fx:id="effects" source="effects.fxml" />
|
||||
</TitledPane>
|
||||
<TitledPane fx:id="objTitledPane" animated="false" collapsible="false" layoutX="424.0" layoutY="436.0" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="213.0" prefWidth="402.0" text="3D .obj file settings">
|
||||
<fx:include fx:id="obj" source="obj.fxml"/>
|
||||
<fx:include fx:id="obj" source="obj.fxml" />
|
||||
</TitledPane>
|
||||
<fx:include fx:id="general" source="general.fxml"/>
|
||||
<fx:include fx:id="general" source="general.fxml" />
|
||||
<TitledPane collapsible="false" layoutX="10.0" layoutY="363.0" prefHeight="286.0" prefWidth="402.0" text="Image settings">
|
||||
<fx:include fx:id="image" source="image.fxml"/>
|
||||
<fx:include fx:id="image" source="image.fxml" />
|
||||
</TitledPane>
|
||||
<MenuBar prefHeight="27.0" prefWidth="838.0">
|
||||
<menus>
|
||||
|
@ -41,6 +42,25 @@
|
|||
</MenuItem>
|
||||
</items>
|
||||
</Menu>
|
||||
<Menu mnemonicParsing="false" text="MIDI">
|
||||
<items>
|
||||
<MenuItem fx:id="resetMidiMappingMenuItem" mnemonicParsing="false" text="Reset MIDI Mappings">
|
||||
<accelerator>
|
||||
<KeyCodeCombination alt="UP" code="M" control="UP" meta="UP" shift="UP" shortcut="DOWN" />
|
||||
</accelerator></MenuItem>
|
||||
<MenuItem fx:id="stopMidiNotesMenuItem" mnemonicParsing="false" text="Stop MIDI Notes" />
|
||||
<CustomMenuItem hideOnClick="false" mnemonicParsing="false" text="MIDI Channel">
|
||||
<content>
|
||||
<AnchorPane>
|
||||
<children>
|
||||
<Label prefHeight="25.0" text="Main MIDI Channel" textFill="WHITE" />
|
||||
<Spinner fx:id="midiChannelSpinner" editable="true" layoutY="25.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</content>
|
||||
</CustomMenuItem>
|
||||
</items>
|
||||
</Menu>
|
||||
</menus>
|
||||
</MenuBar>
|
||||
</AnchorPane>
|
||||
|
|
Ładowanie…
Reference in New Issue