kopia lustrzana https://github.com/jameshball/osci-render
Add pitch bend range, and volume MIDI CC message support, and ignore CC messages sent to popular reserved channels. Fix wobble effect.
rodzic
5d789ffd28
commit
2eca7b17de
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue