Add pitch bend range, and volume MIDI CC message support, and ignore CC messages sent to popular reserved channels. Fix wobble effect.

pull/149/head
James Ball 2023-01-07 18:47:41 +00:00 zatwierdzone przez James H Ball
rodzic 5d789ffd28
commit 2eca7b17de
4 zmienionych plików z 142 dodań i 43 usunięć

Wyświetl plik

@ -52,6 +52,12 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
private int lastAttack = 0;
private double attackSeconds = 0.1;
private int attackFrames;
private final int[] volumes = new int[MidiNote.NUM_CHANNELS];
private final int[] pitchBendRangeSemis = new int[MidiNote.NUM_CHANNELS];
private final int[] pitchBendRangeCents = new int[MidiNote.NUM_CHANNELS];
private final int[] registeredParameterLSBs = new int[MidiNote.NUM_CHANNELS];
private final int[] registeredParameterMSBs = new int[MidiNote.NUM_CHANNELS];
private final Callable<AudioEngine> audioEngineBuilder;
private final BlockingQueue<List<Shape>> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE);
@ -113,6 +119,11 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
public void resetMidi() {
Arrays.fill(keyTargetVolumes, (short) 0);
Arrays.fill(keyActualVolumes, (short) 0);
Arrays.fill(pitchBendRangeSemis, MidiNote.PITCH_BEND_SEMITONES);
Arrays.fill(pitchBendRangeCents, 0);
Arrays.fill(registeredParameterLSBs, -1);
Arrays.fill(registeredParameterMSBs, -1);
Arrays.fill(volumes, 127);
keyOn.clear();
// Middle C is down by default
keyTargetVolumes[60] = (short) MidiNote.MAX_VELOCITY;
@ -307,7 +318,10 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
}
private Vector2 applyEffects(int frame, Vector2 vector) {
vector = vector.scale((double) keyActualVolumes[mainChannel * MidiNote.NUM_KEYS + baseNote.key()] / MidiNote.MAX_VELOCITY);
vector = vector.scale(
((double) keyActualVolumes[mainChannel * MidiNote.NUM_KEYS + baseNote.key()] / MidiNote.MAX_VELOCITY)
* ((double) volumes[mainChannel] / MidiNote.MAX_VELOCITY)
);
if (midiStarted) {
Vector2 sineVector = new Vector2();
@ -348,7 +362,7 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
phase += 1;
double theta = 2 * Math.PI * phase / sampleRate;
double frequency = MidiNote.KEY_TO_FREQUENCY[key];
double frequency = MidiNote.KEY_TO_FREQUENCY[key] * pitchBends[channel];
double x = Math.sin(frequency * theta);
double y = Math.cos(frequency * theta);
@ -682,6 +696,8 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
midiStarted = true;
}
int channel = message.getChannel();
if (command == ShortMessage.NOTE_ON || command == ShortMessage.NOTE_OFF) {
MidiNote note = new MidiNote(message.getData1(), message.getChannel());
int velocity = message.getData2();
@ -708,12 +724,50 @@ public class ShapeAudioPlayer implements AudioPlayer<List<Shape>> {
// 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;
pitchBendFactor *= pitchBendRangeSemis[channel] + pitchBendRangeCents[channel] / 100.0;
// 12 tone equal temperament
pitchBendFactor /= 12;
pitchBendFactor = Math.pow(2, pitchBendFactor);
setPitchBendFactor(message.getChannel(), pitchBendFactor);
} else if (command == ShortMessage.CONTROL_CHANGE) {
int cc = message.getData1();
int value = message.getData2();
if (MidiNote.RESERVED_CC.contains(cc)) {
if (cc == MidiNote.VOLUME_MSB) {
volumes[channel] = value;
} else if (cc == MidiNote.REGISTERED_PARAMETER_LSB) {
if (value == 127) {
registeredParameterLSBs[channel] = -1;
registeredParameterMSBs[channel] = -1;
} else {
registeredParameterLSBs[channel] = value;
}
} else if (cc == MidiNote.REGISTERED_PARAMETER_MSB) {
if (value == 127) {
registeredParameterLSBs[channel] = -1;
registeredParameterMSBs[channel] = -1;
} else {
registeredParameterMSBs[channel] = value;
}
} else if (cc == MidiNote.DATA_ENTRY_MSB || cc == MidiNote.DATA_ENTRY_LSB) {
int lsb = registeredParameterLSBs[channel];
int msb = registeredParameterMSBs[channel];
if (lsb != -1 && msb != -1) {
if (lsb == MidiNote.PITCH_BEND_RANGE_RPM_LSB && msb == MidiNote.PITCH_BEND_RANGE_RPM_MSB) {
// pitch bend range
if (cc == MidiNote.DATA_ENTRY_MSB) {
pitchBendRangeSemis[channel] = value;
} else {
pitchBendRangeCents[channel] = value;
registeredParameterLSBs[channel] = -1;
registeredParameterMSBs[channel] = -1;
}
}
}
}
}
}
}

Wyświetl plik

@ -9,13 +9,11 @@ import sh.ball.shapes.Vector2;
public class WobbleEffect extends PhaseEffect implements FrequencyListener, SettableEffect {
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);
super(sampleRate, 1);
this.volume = Math.max(Math.min(volume, 1), 0);
}
@ -24,7 +22,7 @@ public class WobbleEffect extends PhaseEffect implements FrequencyListener, Sett
}
public void update() {
frequency = lastFrequency;
setSpeed(lastFrequency);
}
public void setVolume(double volume) {
@ -39,7 +37,7 @@ public class WobbleEffect extends PhaseEffect implements FrequencyListener, Sett
@Override
public Vector2 apply(int count, Vector2 vector) {
double theta = nextTheta();
double delta = volume * Math.sin(frequency * theta);
double delta = volume * Math.sin(theta);
double x = vector.x + delta;
double y = vector.y + delta;

Wyświetl plik

@ -1,5 +1,7 @@
package sh.ball.audio.midi;
import java.util.List;
public class MidiNote {
public static final int MAX_VELOCITY = 127;
@ -7,11 +9,52 @@ public class MidiNote {
public static final short MAX_CHANNEL = 15;
public static final short NUM_CHANNELS = (short) (MAX_CHANNEL + 1);
public static final short NUM_KEYS = 128;
public static final short ALL_NOTES_OFF = 0x7B;
public static final short ALL_SOUND_OFF = 120;
public static final short ALL_NOTES_OFF = 123;
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;
public static final int PITCH_BEND_SEMITONES = 2;
public static final int BANK_SELECT_MSB = 0;
public static final int MODULATION_WHEEL_MSB = 1;
public static final int FOOT_PEDAL_MSB = 4;
public static final int DATA_ENTRY_MSB = 6;
public static final int VOLUME_MSB = 7;
public static final int PAN_MSB = 10;
public static final int EXPRESSION_MSB = 11;
public static final int BANK_SELECT_LSB = 32;
public static final int MODULATION_WHEEL_LSB = 33;
public static final int FOOT_PEDAL_LSB = 36;
public static final int DATA_ENTRY_LSB = 38;
public static final int VOLUME_LSB = 39;
public static final int PAN_LSB = 42;
public static final int EXPRESSION_LSB = 43;
public static final int NON_REGISTERED_PARAMETER_LSB = 98;
public static final int NON_REGISTERED_PARAMETER_MSB = 99;
public static final int REGISTERED_PARAMETER_LSB = 100;
public static final int REGISTERED_PARAMETER_MSB = 101;
public static final int PITCH_BEND_RANGE_RPM_LSB = 0;
public static final int PITCH_BEND_RANGE_RPM_MSB = 0;
public static final List<Integer> RESERVED_CC = List.of(
BANK_SELECT_MSB,
MODULATION_WHEEL_MSB,
FOOT_PEDAL_MSB,
DATA_ENTRY_MSB,
VOLUME_MSB,
PAN_MSB,
EXPRESSION_MSB,
BANK_SELECT_LSB,
MODULATION_WHEEL_LSB,
FOOT_PEDAL_LSB,
DATA_ENTRY_LSB,
VOLUME_LSB,
PAN_LSB,
EXPRESSION_LSB,
NON_REGISTERED_PARAMETER_LSB,
NON_REGISTERED_PARAMETER_MSB,
REGISTERED_PARAMETER_LSB,
REGISTERED_PARAMETER_MSB
);
// Concert A Pitch is A4 and has the key number 69
final static int KEY_A4 = 69;

Wyświetl plik

@ -1007,41 +1007,45 @@ public class MainController implements Initializable, FrequencyListener, MidiLis
int cc = message.getData1();
int value = message.getData2();
// if a user has selected a MIDI logo next to a slider, create a mapping
// between the MIDI channel and the SVG MIDI logo
if (armedMidi != null) {
mapMidiCC(cc, armedMidi);
armedMidiPaint = null;
armedMidi = null;
}
// If there is a slider associated with the MIDI channel, update the value
// of it
if (cc <= MidiNote.MAX_CC && CCMap.containsKey(cc)) {
Platform.runLater(() -> {
Slider slider = midiButtonMap.get(CCMap.get(cc));
short closestToZero = channelClosestToZero.get(slider);
double sliderValue = getValueInSliderRange(slider, value / (float) MidiNote.MAX_VELOCITY);
// deadzone
if (value >= closestToZero - midiDeadzone && value <= closestToZero + midiDeadzone && sliderValue < 1) {
slider.setValue(0);
} else {
int leftDeadzone = Math.min(closestToZero, midiDeadzone);
int rightDeadzone = Math.min(MidiNote.MAX_VELOCITY - closestToZero, midiDeadzone);
int actualChannels = MidiNote.MAX_VELOCITY - (leftDeadzone + 1 + rightDeadzone);
int correctedValue;
if (value > closestToZero) {
correctedValue = value - midiDeadzone;
} else {
correctedValue = value + midiDeadzone;
}
double scale = MidiNote.MAX_VELOCITY / (double) actualChannels;
double zeroPoint = closestToZero / (double) MidiNote.MAX_VELOCITY;
slider.setValue(getValueInSliderRange(slider, scale * ((correctedValue / (double) MidiNote.MAX_VELOCITY) - zeroPoint) + zeroPoint));
}
});
} else if (cc == MidiNote.ALL_NOTES_OFF) {
if (MidiNote.RESERVED_CC.contains(cc)) {
// don't allow reserved CCs to be mapped to sliders
} else if (cc == MidiNote.ALL_NOTES_OFF || cc == MidiNote.ALL_SOUND_OFF) {
audioPlayer.stopMidiNotes();
} else if (cc < MidiNote.ALL_SOUND_OFF) {
// if a user has selected a MIDI logo next to a slider, create a mapping
// between the MIDI channel and the SVG MIDI logo
if (armedMidi != null) {
mapMidiCC(cc, armedMidi);
armedMidiPaint = null;
armedMidi = null;
}
// If there is a slider associated with the MIDI channel, update the value
// of it
if (CCMap.containsKey(cc)) {
Platform.runLater(() -> {
Slider slider = midiButtonMap.get(CCMap.get(cc));
short closestToZero = channelClosestToZero.get(slider);
double sliderValue = getValueInSliderRange(slider, value / (float) MidiNote.MAX_VELOCITY);
// deadzone
if (value >= closestToZero - midiDeadzone && value <= closestToZero + midiDeadzone && sliderValue < 1) {
slider.setValue(0);
} else {
int leftDeadzone = Math.min(closestToZero, midiDeadzone);
int rightDeadzone = Math.min(MidiNote.MAX_VELOCITY - closestToZero, midiDeadzone);
int actualChannels = MidiNote.MAX_VELOCITY - (leftDeadzone + 1 + rightDeadzone);
int correctedValue;
if (value > closestToZero) {
correctedValue = value - midiDeadzone;
} else {
correctedValue = value + midiDeadzone;
}
double scale = MidiNote.MAX_VELOCITY / (double) actualChannels;
double zeroPoint = closestToZero / (double) MidiNote.MAX_VELOCITY;
slider.setValue(getValueInSliderRange(slider, scale * ((correctedValue / (double) MidiNote.MAX_VELOCITY) - zeroPoint) + zeroPoint));
}
});
}
}
} else if (command == ShortMessage.PROGRAM_CHANGE) {
// We want to change the file that is currently playing