Skip to content

Commit a38bd4f

Browse files
authored
APC40: Shift+Knob to browse UserPresets (#758)
* APC40: Shift+Knob to browse UserPresets * Store knob value while shift is pressed to avoid jumpy pattern browsing
1 parent 83b27b5 commit a38bd4f

File tree

2 files changed

+64
-12
lines changed

2 files changed

+64
-12
lines changed

te-app/src/main/java/titanicsend/lx/APC40Mk2.java

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
import heronarts.lx.midi.MidiNote;
88
import heronarts.lx.midi.MidiNoteOn;
99
import heronarts.lx.midi.surface.LXMidiSurface;
10-
import heronarts.lx.mixer.LXBus;
10+
import heronarts.lx.mixer.LXAbstractChannel;
1111
import heronarts.lx.mixer.LXChannel;
1212
import heronarts.lx.parameter.BooleanParameter;
1313
import heronarts.lx.parameter.LXParameterListener;
14+
import heronarts.lx.pattern.LXPattern;
1415
import java.util.ArrayList;
16+
import java.util.Arrays;
1517
import java.util.HashMap;
1618
import java.util.List;
1719
import java.util.Map;
1820
import java.util.Map.Entry;
21+
import titanicsend.pattern.TEPerformancePattern;
1922

2023
@LXMidiSurface.Name("Akai APC40 mkII TE")
2124
@LXMidiSurface.DeviceName("APC40 mkII")
@@ -58,9 +61,16 @@ public static void setUserButton(UserButton button, BooleanParameter parameter)
5861

5962
private static final List<APC40Mk2> surfaces = new ArrayList<APC40Mk2>();
6063

64+
private boolean shiftOn = false;
65+
66+
private int[] channelKnobValues = new int[CHANNEL_KNOB_NUM];
67+
6168
public APC40Mk2(LX lx, LXMidiInput input, LXMidiOutput output) {
6269
super(lx, input, output);
6370
surfaces.add(this);
71+
72+
// Initialize channel knob values
73+
Arrays.fill(this.channelKnobValues, 0);
6474
}
6575

6676
@Override
@@ -162,6 +172,20 @@ private void sendUserButton(UserButton button, boolean on) {
162172
private boolean noteReceived(MidiNote note, boolean on) {
163173
int pitch = note.getPitch();
164174

175+
// Track shift state (temporary until available from upstream)
176+
if (pitch == SHIFT) {
177+
this.shiftOn = on;
178+
if (on) {
179+
// TODO: Send normalized value for selected preset? So far it seems okay...
180+
} else {
181+
// When shift is release, send (restore) channel knob values for pattern browsing
182+
for (int i = 0; i < CHANNEL_KNOB_NUM; i++) {
183+
sendControlChange(0, CHANNEL_KNOB + i, this.channelKnobValues[i]);
184+
}
185+
}
186+
return false;
187+
}
188+
165189
if (on) {
166190
for (UserButton userButton : UserButton.values()) {
167191
if (pitch == userButton.note) {
@@ -186,23 +210,43 @@ public void noteOnReceived(MidiNoteOn note) {
186210
super.noteOnReceived(note);
187211
}
188212

213+
@Override
214+
public void noteOffReceived(MidiNote note) {
215+
if (noteReceived(note, false)) {
216+
return;
217+
}
218+
super.noteOffReceived(note);
219+
}
220+
189221
@Override
190222
public void controlChangeReceived(MidiControlChange cc) {
191223
int number = cc.getCC();
192224

193225
// TE: Channel knobs set focused pattern, if fader is down.
194226
if (number >= CHANNEL_KNOB && number <= CHANNEL_KNOB_MAX) {
195-
int channel = number - CHANNEL_KNOB;
196-
if (channel < this.lx.engine.mixer.channels.size()) {
197-
LXBus bus = this.lx.engine.mixer.channels.get(channel);
198-
if (bus instanceof LXChannel && bus.fader.getValue() == 0) {
199-
int numPatterns = ((LXChannel) bus).patterns.size();
200-
if (numPatterns > 0) {
201-
double normalized = cc.getNormalized();
202-
// Set active pattern
203-
((LXChannel) bus).goPatternIndex((int) (normalized * (numPatterns - 1)));
204-
// Alternative for focused pattern
205-
// ((LXChannel)bus).focusedPattern.setNormalized(normalized);
227+
int channelIndex = number - CHANNEL_KNOB;
228+
if (channelIndex < this.lx.engine.mixer.channels.size()) {
229+
LXAbstractChannel abstractChannel = this.lx.engine.mixer.channels.get(channelIndex);
230+
if (abstractChannel instanceof LXChannel channel && isNotLive(abstractChannel)) {
231+
// Knob corresponds to channel, and channel is not live.
232+
if (this.shiftOn) {
233+
// Change user preset on selected pattern
234+
LXPattern pattern = channel.getActivePattern();
235+
if (pattern instanceof TEPerformancePattern tePattern) {
236+
tePattern.getControls().getPresetSelectorOffair().setNormalized(cc.getNormalized());
237+
}
238+
} else {
239+
// Remember knob position in case shift is pressed
240+
this.channelKnobValues[channelIndex] = cc.getValue();
241+
// Change active pattern on the channel
242+
int numPatterns = channel.patterns.size();
243+
if (numPatterns > 0) {
244+
double normalized = cc.getNormalized();
245+
// Set active pattern
246+
channel.goPatternIndex((int) (normalized * (numPatterns - 1)));
247+
// Alternative for focused pattern
248+
// ((LXChannel)bus).focusedPattern.setNormalized(normalized);
249+
}
206250
}
207251
}
208252
}
@@ -212,6 +256,10 @@ public void controlChangeReceived(MidiControlChange cc) {
212256
super.controlChangeReceived(cc);
213257
}
214258

259+
private boolean isNotLive(LXAbstractChannel abstractChannel) {
260+
return abstractChannel.fader.getValue() == 0 || !abstractChannel.enabled.isOn();
261+
}
262+
215263
@Override
216264
public void dispose() {
217265
if (this.isRegistered) {

te-app/src/main/java/titanicsend/pattern/TECommonControls.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ public TEControl getControl(TEControlTag tag) {
165165
return controlList.get(tag);
166166
}
167167

168+
public OffairDiscreteParameter<UserPresetCollection.Selector> getPresetSelectorOffair() {
169+
return this.presetSelectorOffair;
170+
}
171+
168172
/**
169173
* Retrieve backing LX control object for given tag
170174
*

0 commit comments

Comments
 (0)