From 2eca7b17defbbc8bf642a4185db43b5fdc17c38c Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 7 Jan 2023 18:47:41 +0000 Subject: [PATCH] Add pitch bend range, and volume MIDI CC message support, and ignore CC messages sent to popular reserved channels. Fix wobble effect. --- .../java/sh/ball/audio/ShapeAudioPlayer.java | 60 +++++++++++++++- .../sh/ball/audio/effect/WobbleEffect.java | 8 +-- .../java/sh/ball/audio/midi/MidiNote.java | 45 +++++++++++- .../ball/gui/controller/MainController.java | 72 ++++++++++--------- 4 files changed, 142 insertions(+), 43 deletions(-) diff --git a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java index 3af086d..821747e 100644 --- a/src/main/java/sh/ball/audio/ShapeAudioPlayer.java +++ b/src/main/java/sh/ball/audio/ShapeAudioPlayer.java @@ -52,6 +52,12 @@ public class ShapeAudioPlayer implements AudioPlayer> { 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 audioEngineBuilder; private final BlockingQueue> frameQueue = new ArrayBlockingQueue<>(BUFFER_SIZE); @@ -113,6 +119,11 @@ public class ShapeAudioPlayer implements AudioPlayer> { 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> { } 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> { 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> { 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> { // 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; + } + } + } + } + } } } diff --git a/src/main/java/sh/ball/audio/effect/WobbleEffect.java b/src/main/java/sh/ball/audio/effect/WobbleEffect.java index 4b645fd..4a7d499 100644 --- a/src/main/java/sh/ball/audio/effect/WobbleEffect.java +++ b/src/main/java/sh/ball/audio/effect/WobbleEffect.java @@ -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; diff --git a/src/main/java/sh/ball/audio/midi/MidiNote.java b/src/main/java/sh/ball/audio/midi/MidiNote.java index 883e206..7e2c2ac 100644 --- a/src/main/java/sh/ball/audio/midi/MidiNote.java +++ b/src/main/java/sh/ball/audio/midi/MidiNote.java @@ -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 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; diff --git a/src/main/java/sh/ball/gui/controller/MainController.java b/src/main/java/sh/ball/gui/controller/MainController.java index 50933ba..90a41c8 100644 --- a/src/main/java/sh/ball/gui/controller/MainController.java +++ b/src/main/java/sh/ball/gui/controller/MainController.java @@ -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