From d6ac1ee83fec42f9c5ba4a14248879b541f1f58d Mon Sep 17 00:00:00 2001 From: Mark Slee Date: Tue, 24 Sep 2013 17:28:53 -0700 Subject: [PATCH] Refactor MIDI stuff so deck focusing is listenable and controllable --- DanUtil.pde | 2 - MarkSlee.pde | 6 +- _Internals.pde | 237 +--------------------- _MIDI.pde | 459 ++++++++++++++++++++++++++++++++++++++++++ _UIImplementation.pde | 19 +- code/GLucose.jar | Bin 26584 -> 26587 bytes 6 files changed, 486 insertions(+), 237 deletions(-) create mode 100644 _MIDI.pde diff --git a/DanUtil.pde b/DanUtil.pde index d3f9ac0..c27eaed 100644 --- a/DanUtil.pde +++ b/DanUtil.pde @@ -121,7 +121,6 @@ public class DGlobals { void controllerChangeReceived(rwmidi.Controller cc) { if (cc.getCC() == 7 && btwn(cc.getChannel(),0,7)) { Sliders[cc.getChannel()] = 1.*cc.getValue()/127.; } - else if (cc.getCC() == 15 && cc.getChannel() == 0) { lx.engine.getDeck(1).getCrossfader().setValue( 1.*cc.getValue()/127.); } @@ -157,7 +156,6 @@ public class DGlobals { double Tap1 = 0; double getNow() { return millis() + 1000*second() + 60*1000*minute() + 3600*1000*hour(); } - void noteOffReceived(Note note) { if (CurPat == null) return; int row = DG.mapRow(note.getPitch()), col = note.getChannel(); diff --git a/MarkSlee.pde b/MarkSlee.pde index 7738419..3a9b530 100644 --- a/MarkSlee.pde +++ b/MarkSlee.pde @@ -421,13 +421,15 @@ public class PianoKeyPattern extends SCPattern { return base[index % base.length]; } - public void noteOnReceived(Note note) { + public boolean noteOnReceived(Note note) { LinearEnvelope env = getEnvelope(note.getPitch()); env.setEndVal(min(1, env.getValuef() + (note.getVelocity() / 127.)), getAttackTime()).start(); + return true; } - public void noteOffReceived(Note note) { + public boolean noteOffReceived(Note note) { getEnvelope(note.getPitch()).setEndVal(0, getReleaseTime()).start(); + return true; } public void run(double deltaMs) { diff --git a/_Internals.pde b/_Internals.pde index 19b4ed6..60a5f32 100644 --- a/_Internals.pde +++ b/_Internals.pde @@ -48,8 +48,7 @@ HeronLX lx; LXPattern[] patterns; MappingTool mappingTool; PandaDriver[] pandaBoards; -SCMidiInput midiQwertyKeys; -SCMidiInput midiQwertyAPC; +MidiEngine midiEngine; // Display configuration mode boolean mappingMode = false; @@ -124,14 +123,7 @@ void setup() { logTime("Built PandaDriver"); // MIDI devices - List midiControllers = new ArrayList(); - midiControllers.add(midiQwertyKeys = new SCMidiInput(SCMidiInput.KEYS)); - midiControllers.add(midiQwertyAPC = new SCMidiInput(SCMidiInput.APC)); - for (MidiInputDevice device : RWMidi.getInputDevices()) { - boolean enableDevice = device.getName().contains("APC"); - midiControllers.add(new SCMidiInput(device).setEnabled(enableDevice)); - } - SCMidiDevices.initializeStandardDevices(glucose); + midiEngine = new MidiEngine(); logTime("Setup MIDI devices"); // Build overlay UI @@ -144,7 +136,7 @@ void setup() { new UISpeed(4, 624, 140, 50), new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324), - uiMidi = new UIMidi(midiControllers, width-144, 332, 140, 158), + uiMidi = new UIMidi(midiEngine, width-144, 332, 140, 158), new UIOutput(width-144, 494, 140, 106), uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86), @@ -167,6 +159,8 @@ void setup() { eyeY = midY + 70; eyeX = midX + eyeR*sin(eyeA); eyeZ = midZ + eyeR*cos(eyeA); + + // Add mouse scrolling event support addMouseWheelListener(new java.awt.event.MouseWheelListener() { public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) { mouseWheel(mwe.getWheelRotation()); @@ -176,221 +170,6 @@ void setup() { println("Hit the 'p' key to toggle Panda Board output"); } -public interface SCMidiInputListener { - public void onEnabled(SCMidiInput controller, boolean enabled); -} - -public class SCMidiInput extends AbstractScrollItem { - - public static final int MIDI = 0; - public static final int KEYS = 1; - public static final int APC = 2; - - private boolean enabled = false; - private final String name; - private final int mode; - private int octaveShift = 0; - - class NoteMeta { - int channel; - int number; - NoteMeta(int channel, int number) { - this.channel = channel; - this.number = number; - } - } - - final Map keyToNote = new HashMap(); - - final List listeners = new ArrayList(); - - public SCMidiInput addListener(SCMidiInputListener l) { - listeners.add(l); - return this; - } - - public SCMidiInput removeListener(SCMidiInputListener l) { - listeners.remove(l); - return this; - } - - SCMidiInput(MidiInputDevice d) { - mode = MIDI; - d.createInput(this); - name = d.getName().replace("Unknown vendor",""); - } - - SCMidiInput(int mode) { - this.mode = mode; - switch (mode) { - case APC: - name = "QWERTY (APC Mode)"; - mapAPC(); - break; - default: - case KEYS: - name = "QWERTY (Key Mode)"; - mapKeys(); - break; - } - } - - private void mapAPC() { - mapNote('1', 0, 53); - mapNote('2', 1, 53); - mapNote('3', 2, 53); - mapNote('4', 3, 53); - mapNote('5', 4, 53); - mapNote('6', 5, 53); - mapNote('q', 0, 54); - mapNote('w', 1, 54); - mapNote('e', 2, 54); - mapNote('r', 3, 54); - mapNote('t', 4, 54); - mapNote('y', 5, 54); - mapNote('a', 0, 55); - mapNote('s', 1, 55); - mapNote('d', 2, 55); - mapNote('f', 3, 55); - mapNote('g', 4, 55); - mapNote('h', 5, 55); - mapNote('z', 0, 56); - mapNote('x', 1, 56); - mapNote('c', 2, 56); - mapNote('v', 3, 56); - mapNote('b', 4, 56); - mapNote('n', 5, 56); - registerKeyEvent(this); - } - - private void mapKeys() { - int note = 48; - mapNote('a', 1, note++); - mapNote('w', 1, note++); - mapNote('s', 1, note++); - mapNote('e', 1, note++); - mapNote('d', 1, note++); - mapNote('f', 1, note++); - mapNote('t', 1, note++); - mapNote('g', 1, note++); - mapNote('y', 1, note++); - mapNote('h', 1, note++); - mapNote('u', 1, note++); - mapNote('j', 1, note++); - mapNote('k', 1, note++); - mapNote('o', 1, note++); - mapNote('l', 1, note++); - registerKeyEvent(this); - } - - void mapNote(char ch, int channel, int number) { - keyToNote.put(ch, new NoteMeta(channel, number)); - } - - public String getLabel() { - return name; - } - - public void keyEvent(KeyEvent e) { - if (!enabled) { - return; - } - char c = Character.toLowerCase(e.getKeyChar()); - NoteMeta nm = keyToNote.get(c); - if (nm != null) { - switch (e.getID()) { - case KeyEvent.KEY_PRESSED: - noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127)); - break; - case KeyEvent.KEY_RELEASED: - noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0)); - break; - } - } - if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) { - switch (c) { - case 'z': - octaveShift = constrain(octaveShift-1, -4, 4); - break; - case 'x': - octaveShift = constrain(octaveShift+1, -4, 4); - break; - } - } - } - - public boolean isEnabled() { - return enabled; - } - - public boolean isSelected() { - return enabled; - } - - public void onMousePressed() { - setEnabled(!enabled); - } - - public SCMidiInput setEnabled(boolean enabled) { - if (enabled != this.enabled) { - this.enabled = enabled; - for (SCMidiInputListener l : listeners) { - l.onEnabled(this, enabled); - } - } - return this; - } - - private SCPattern getFocusedPattern() { - Engine.Deck focusedDeck = (uiMidi != null) ? uiMidi.getFocusedDeck() : lx.engine.getDefaultDeck(); - return (SCPattern) focusedDeck.getActivePattern(); - } - - private boolean logMidi() { - return (uiMidi != null) && uiMidi.logMidi(); - } - - void programChangeReceived(ProgramChange pc) { - if (!enabled) { - return; - } - if (logMidi()) { - println(getLabel() + " :: Program Change :: " + pc.getNumber()); - } - } - - void controllerChangeReceived(rwmidi.Controller cc) { - if (!enabled) { - return; - } - if (logMidi()) { - println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue()); - } - getFocusedPattern().controllerChangeReceived(cc); - } - - void noteOnReceived(Note note) { - if (!enabled) { - return; - } - if (logMidi()) { - println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity()); - } - getFocusedPattern().noteOnReceived(note); - } - - void noteOffReceived(Note note) { - if (!enabled) { - return; - } - if (logMidi()) { - println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity()); - } - getFocusedPattern().noteOffReceived(note); - } - -} - /** * Core render loop and drawing functionality. */ @@ -633,13 +412,13 @@ void keyPressed() { frameRate(++targetFramerate); break; case 'd': - if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) { + if (!midiEngine.isQwertyEnabled()) { debugMode = !debugMode; println("Debug output: " + (debugMode ? "ON" : "OFF")); } break; case 'm': - if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) { + if (!midiEngine.isQwertyEnabled()) { mappingMode = !mappingMode; uiPatternA.setVisible(!mappingMode); uiMapping.setVisible(mappingMode); @@ -661,7 +440,7 @@ void keyPressed() { } break; case 'u': - if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) { + if (!midiEngine.isQwertyEnabled()) { uiOn = !uiOn; } break; diff --git a/_MIDI.pde b/_MIDI.pde new file mode 100644 index 0000000..4b3f313 --- /dev/null +++ b/_MIDI.pde @@ -0,0 +1,459 @@ +/** + * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND + * + * //\\ //\\ //\\ //\\ + * ///\\\ ///\\\ ///\\\ ///\\\ + * \\\/// \\\/// \\\/// \\\/// + * \\// \\// \\// \\// + * + * EXPERTS ONLY!! EXPERTS ONLY!! + * + * This file defines the MIDI mapping interfaces. This shouldn't + * need editing unless you're adding top level support for a + * specific MIDI device of some sort. Generally, all MIDI devices + * will just work with the default configuration, and you can + * set your SCPattern class to respond to the controllers that you + * care about. + */ + +interface MidiEngineListener { + public void onFocusedDeck(int deckIndex); +} + +class MidiEngine { + + private final List listeners = new ArrayList(); + private final List midiControllers = new ArrayList(); + + public MidiEngine addListener(MidiEngineListener l) { + listeners.add(l); + return this; + } + + public MidiEngine removeListener(MidiEngineListener l) { + listeners.remove(l); + return this; + } + + private SCMidiInput midiQwertyKeys; + private SCMidiInput midiQwertyAPC; + + private int activeDeckIndex = 0; + + public MidiEngine() { + + midiControllers.add(midiQwertyKeys = new SCMidiInput(SCMidiInput.KEYS)); + midiControllers.add(midiQwertyAPC = new SCMidiInput(SCMidiInput.APC)); + for (MidiInputDevice device : RWMidi.getInputDevices()) { + if (device.getName().contains("APC")) { + midiControllers.add(new APC40MidiInput(device).setEnabled(true)); + } else if (device.getName().contains("SLIDER/KNOB KORG")) { + midiControllers.add(new KorgNanoKontrolMidiInput(device).setEnabled(true)); + } else { + midiControllers.add(new SCMidiInput(device)); + } + } + } + + public List getControllers() { + return this.midiControllers; + } + + public MidiEngine setFocusedDeck(int deckIndex) { + if (this.activeDeckIndex != deckIndex) { + this.activeDeckIndex = deckIndex; + for (MidiEngineListener listener : listeners) { + listener.onFocusedDeck(deckIndex); + } + } + return this; + } + + public Engine.Deck getFocusedDeck() { + return lx.engine.getDeck(activeDeckIndex); + } + + public boolean isQwertyEnabled() { + return midiQwertyKeys.isEnabled() || midiQwertyAPC.isEnabled(); + } +} + +public interface SCMidiInputListener { + public void onEnabled(SCMidiInput controller, boolean enabled); +} + +public class SCMidiInput extends AbstractScrollItem { + + public static final int MIDI = 0; + public static final int KEYS = 1; + public static final int APC = 2; + + private boolean enabled = false; + private final String name; + private final int mode; + private int octaveShift = 0; + + class NoteMeta { + int channel; + int number; + NoteMeta(int channel, int number) { + this.channel = channel; + this.number = number; + } + } + + final Map keyToNote = new HashMap(); + + final List listeners = new ArrayList(); + + public SCMidiInput addListener(SCMidiInputListener l) { + listeners.add(l); + return this; + } + + public SCMidiInput removeListener(SCMidiInputListener l) { + listeners.remove(l); + return this; + } + + SCMidiInput(MidiInputDevice d) { + mode = MIDI; + d.createInput(this); + name = d.getName().replace("Unknown vendor",""); + } + + SCMidiInput(int mode) { + this.mode = mode; + switch (mode) { + case APC: + name = "QWERTY (APC Mode)"; + mapAPC(); + break; + default: + case KEYS: + name = "QWERTY (Key Mode)"; + mapKeys(); + break; + } + } + + private void mapAPC() { + mapNote('1', 0, 53); + mapNote('2', 1, 53); + mapNote('3', 2, 53); + mapNote('4', 3, 53); + mapNote('5', 4, 53); + mapNote('6', 5, 53); + mapNote('q', 0, 54); + mapNote('w', 1, 54); + mapNote('e', 2, 54); + mapNote('r', 3, 54); + mapNote('t', 4, 54); + mapNote('y', 5, 54); + mapNote('a', 0, 55); + mapNote('s', 1, 55); + mapNote('d', 2, 55); + mapNote('f', 3, 55); + mapNote('g', 4, 55); + mapNote('h', 5, 55); + mapNote('z', 0, 56); + mapNote('x', 1, 56); + mapNote('c', 2, 56); + mapNote('v', 3, 56); + mapNote('b', 4, 56); + mapNote('n', 5, 56); + registerKeyEvent(this); + } + + private void mapKeys() { + int note = 48; + mapNote('a', 1, note++); + mapNote('w', 1, note++); + mapNote('s', 1, note++); + mapNote('e', 1, note++); + mapNote('d', 1, note++); + mapNote('f', 1, note++); + mapNote('t', 1, note++); + mapNote('g', 1, note++); + mapNote('y', 1, note++); + mapNote('h', 1, note++); + mapNote('u', 1, note++); + mapNote('j', 1, note++); + mapNote('k', 1, note++); + mapNote('o', 1, note++); + mapNote('l', 1, note++); + registerKeyEvent(this); + } + + void mapNote(char ch, int channel, int number) { + keyToNote.put(ch, new NoteMeta(channel, number)); + } + + public String getLabel() { + return name; + } + + public void keyEvent(KeyEvent e) { + if (!enabled) { + return; + } + char c = Character.toLowerCase(e.getKeyChar()); + NoteMeta nm = keyToNote.get(c); + if (nm != null) { + switch (e.getID()) { + case KeyEvent.KEY_PRESSED: + noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127)); + break; + case KeyEvent.KEY_RELEASED: + noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0)); + break; + } + } + if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) { + switch (c) { + case 'z': + octaveShift = constrain(octaveShift-1, -4, 4); + break; + case 'x': + octaveShift = constrain(octaveShift+1, -4, 4); + break; + } + } + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isSelected() { + return enabled; + } + + public void onMousePressed() { + setEnabled(!enabled); + } + + public SCMidiInput setEnabled(boolean enabled) { + if (enabled != this.enabled) { + this.enabled = enabled; + for (SCMidiInputListener l : listeners) { + l.onEnabled(this, enabled); + } + } + return this; + } + + protected SCPattern getFocusedPattern() { + return (SCPattern) midiEngine.getFocusedDeck().getActivePattern(); + } + + private boolean logMidi() { + return (uiMidi != null) && uiMidi.logMidi(); + } + + final void programChangeReceived(ProgramChange pc) { + if (!enabled) { + return; + } + if (logMidi()) { + println(getLabel() + " :: Program Change :: " + pc.getNumber()); + } + handleProgramChange(pc); + } + + final void controllerChangeReceived(rwmidi.Controller cc) { + if (!enabled) { + return; + } + if (logMidi()) { + println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue()); + } + if (!getFocusedPattern().controllerChangeReceived(cc)) { + handleControllerChange(cc); + } + } + + final void noteOnReceived(Note note) { + if (!enabled) { + return; + } + if (logMidi()) { + println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity()); + } + if (!getFocusedPattern().noteOnReceived(note)) { + handleNoteOn(note); + } + } + + final void noteOffReceived(Note note) { + if (!enabled) { + return; + } + if (logMidi()) { + println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity()); + } + if (!getFocusedPattern().noteOffReceived(note)) { + handleNoteOff(note); + } + } + + // Subclasses may implement these to map top-level functionality + protected void handleProgramChange(ProgramChange pc) { + } + protected void handleControllerChange(rwmidi.Controller cc) { + } + protected void handleNoteOn(Note note) { + } + protected void handleNoteOff(Note note) { + } +} + +public class APC40MidiInput extends SCMidiInput { + + private boolean shiftOn = false; + private LXEffect releaseEffect = null; + + APC40MidiInput(MidiInputDevice d) { + super(d); + } + + protected void handleControllerChange(rwmidi.Controller cc) { + int number = cc.getCC(); + switch (number) { + // Crossfader + case 15: + lx.engine.getDeck(1).getCrossfader().setValue(cc.getValue() / 127.); + break; + } + + int parameterIndex = -1; + if (number >= 48 && number <= 55) { + parameterIndex = number - 48; + } else if (number >= 16 && number <= 19) { + parameterIndex = 8 + (number-16); + } + if (parameterIndex >= 0) { + List parameters = getFocusedPattern().getParameters(); + if (parameterIndex < parameters.size()) { + parameters.get(parameterIndex).setValue(cc.getValue() / 127.); + } + } + + if (number >= 20 && number <= 23) { + int effectIndex = number - 20; + List parameters = glucose.getSelectedEffect().getParameters(); + if (effectIndex < parameters.size()) { + parameters.get(effectIndex).setValue(cc.getValue() / 127.); + } + } + } + + protected void handleNoteOn(Note note) { + switch (note.getPitch()) { + case 94: // right bank + midiEngine.setFocusedDeck(1); + break; + case 95: // left bank + midiEngine.setFocusedDeck(0); + break; + case 96: // up bank + if (shiftOn) { + glucose.incrementSelectedEffectBy(1); + } else { + midiEngine.getFocusedDeck().goNext(); + } + break; + case 97: // down bank + if (shiftOn) { + glucose.incrementSelectedEffectBy(-1); + } else { + midiEngine.getFocusedDeck().goPrev(); + } + break; + + case 98: // shift + shiftOn = true; + break; + + case 99: // tap tempo + lx.tempo.tap(); + break; + case 100: // nudge+ + lx.tempo.setBpm(lx.tempo.bpm() + (shiftOn ? 1 : .1)); + break; + case 101: // nudge- + lx.tempo.setBpm(lx.tempo.bpm() - (shiftOn ? 1 : .1)); + break; + + case 91: // play + case 93: // rec + releaseEffect = glucose.getSelectedEffect(); + if (releaseEffect.isMomentary()) { + releaseEffect.enable(); + } else { + releaseEffect.toggle(); + } + break; + + case 92: // stop + glucose.getSelectedEffect().disable(); + break; + } + } + + protected void handleNoteOff(Note note) { + switch (note.getPitch()) { + case 93: // rec + if (releaseEffect != null) { + if (releaseEffect.isMomentary()) { + releaseEffect.disable(); + } + } + break; + + case 98: // shift + shiftOn = false; + break; + } + } +} + +class KorgNanoKontrolMidiInput extends SCMidiInput { + + KorgNanoKontrolMidiInput(MidiInputDevice d) { + super(d); + } + + protected void handleControllerChange(rwmidi.Controller cc) { + int number = cc.getCC(); + if (number >= 16 && number <= 23) { + int parameterIndex = number - 16; + List parameters = getFocusedPattern().getParameters(); + if (parameterIndex < parameters.size()) { + parameters.get(parameterIndex).setValue(cc.getValue() / 127.); + } + } + + if (cc.getValue() == 127) { + switch (number) { + // Left track + case 58: + midiEngine.setFocusedDeck(0); + break; + // Right track + case 59: + midiEngine.setFocusedDeck(1); + break; + // Left chevron + case 43: + midiEngine.getFocusedDeck().goPrev(); + break; + // Right chevron + case 44: + midiEngine.getFocusedDeck().goNext(); + break; + } + } + } +} + diff --git a/_UIImplementation.pde b/_UIImplementation.pde index a12c189..df3a7e4 100644 --- a/_UIImplementation.pde +++ b/_UIImplementation.pde @@ -483,16 +483,21 @@ class UIMidi extends UIWindow { private final UIToggleSet deckMode; private final UIButton logMode; - UIMidi(List midiControllers, float x, float y, float w, float h) { + UIMidi(final MidiEngine midiEngine, float x, float y, float w, float h) { super("MIDI", x, y, w, h); + // Processing compiler doesn't seem to get that list of class objects also conform to interface List scrollItems = new ArrayList(); - for (SCMidiInput mc : midiControllers) { + for (SCMidiInput mc : midiEngine.getControllers()) { scrollItems.add(mc); } final UIScrollList scrollList; (scrollList = new UIScrollList(1, titleHeight, w-2, 100)).setItems(scrollItems).addToContainer(this); - (deckMode = new UIToggleSet(4, 130, 90, 20)).setOptions(new String[] { "A", "B" }).addToContainer(this); + (deckMode = new UIToggleSet(4, 130, 90, 20) { + protected void onToggle(String value) { + midiEngine.setFocusedDeck(value == "A" ? 0 : 1); + } + }).setOptions(new String[] { "A", "B" }).addToContainer(this); (logMode = new UIButton(98, 130, w-103, 20)).setLabel("LOG").addToContainer(this); SCMidiInputListener listener = new SCMidiInputListener() { @@ -500,9 +505,15 @@ class UIMidi extends UIWindow { scrollList.redraw(); } }; - for (SCMidiInput mc : midiControllers) { + for (SCMidiInput mc : midiEngine.getControllers()) { mc.addListener(listener); } + + midiEngine.addListener(new MidiEngineListener() { + public void onFocusedDeck(int deckIndex) { + deckMode.setValue(deckIndex == 0 ? "A" : "B"); + } + }); } diff --git a/code/GLucose.jar b/code/GLucose.jar index 26b508fbce7d98f7413eefe22704297b0d74e543..7b558bbc2bc53d89106ef46af7770bff5e8d4854 100755 GIT binary patch delta 2586 zcmZWr3piBk8eTK*3^ryoW88(wEoJYBVPfPmgj{nS%%BvNaHWvh8h%G!+4y-y0gC>M140j=P zx$qKVE0+imkmKA~C(0Km%)MWiCRWEM!b!*itM`7)087afKKsI9&?i~Jh{s@9eOFS$ za*bJI!-@3S;Z*lwX92kgtGcjgn{LwX-kKi$(PL4!^Y0P2PI%_Ojd5(38$G+Xea|8N z)2KFAt*WXV!eshs@5Vh8tq8sQ^%r_J+5JUZdD;`9pAGWRe9-H6RR`uoeFy~ZVL)f9$R>6?o5c9R}o z^)hWMQ@0mmD)X+94MHzTtdY+qdL#_{X@)VYkrv9%yV|q27R|1&Al{d7A59U{Ds2iS zO{AdD~oKAZ3TAw<_ZY_PS(i>2-D(Ad?vPH7>j7aT@ z$=xKHSo};t9FrmIiW(R)5+6q(yHEyOcx~#Z9N>gGD zChxBkd6d0+&VIOfPt+F1b-GB;JpbF$a}ocnJ4abMh?^a)T<32q#G#mrETzB`YPORO ze=HYYX{*+5dax~`Q8?;N=Y#*aP8j02gIw(~Y1UA8MCL03HcK$(8!z$Ogh0Zs3B=hV z;G{|}R|rlhs$_n+I7@?sgp;R)6#+Qu*R$ZY%jie&YSMmH6ok8O7#H(+9Ec177#TEI zK}c}@c%GenY?cp#l!btbH5$yD3jn7e!8%3qH$IqE*7_B!1uWaP!=25P1b#RnI+pT@ zbGOlJ5SuFl@Ck`v5Bwx5D#$X*CNw7E|x`Pt)_vw!>+BTDo**OJqPn2gZ>1pn<*WxnY87zh{ zZD+i-oA*giWtKgxfAu8yqQ`VP3o~}G?dta1a&fPX>e1h$-tr$Bs(bkwvTO2txM}S2 z*yPfVtp<@n;_GYWzKpwfVlC3|8KMeQLXO`t|Ik1*TrlmLXl)H|I@pW|`ml1MfUG6@ z>1IsQQA`V=^1T&hx8ze%4Oe1E=}BuX#Wx}sZ%l59#0)It7hUgcakCHb>O@gQgE7t5 zen@y?41^o!L+yS{CU@!A_a_Yewsx&@r^)e?H9nf9JNm+0*rJCR*4jgvxU{>GinEHR zrskZ&9!cH7Nnv|C7gD@cCsSOWh?m>mNOP`S9J6{8&#DsKe2LgZ{u=n#)=S;f8Pj4e z7iz=Z->pd0sCf<Q;Z7p;K3e5-f3M{#p50R&+<#+f_YjzAt(sZ7EC!Lll>4P_Lo9X64iN zoQjn=F68|_D*xoCd$LX4E`th{GfUawmcLjAtJo7`bW*=IsyL~scc*tt)J`)3JMn(Vj< zChHI2iTWIXN4ns^QJfOc4Mf30h=F)yp&V$DQ_yy@Lm<(7+;0J4?zaFrMQ5%>lpliX z1R+Rq1FZ=t3e<%a4{_R)HNXS`4UjY(3@Os^NEU$u$!%cd3hfNAV+nDD-sN1%8yFve zB-&PZGxUC94T-brB_RmHZm8kc2gx)vctewhCqsQMEcHPEf*Mc|gxgRU3(#}|ygL#d zkH`bwbb>N>L&M9;2W&YV2_g`L-_SuK0$-v6cu40BWq^)HwwZ%*b2T6t#Df%r@W{6o z90JU{ydtYwrzW}eI5$FH_ delta 2622 zcmZWr2Ut_t623{O0wP5TgccR)ZBeAR&_Y1UQbk%I6jv+=DC!b}5GfjkD@}?71OY2W z1hG&A1f(rUkq&VcM2Wi6g}u3XzW31kedn8#f99W=GxyG!40j<0yAT2n1Qf>>0N?=t z;EGnVfCB0}O)FW4hGP9{m4(IJE+lXT6hf%5gfT*y1%janMK&q{mLr5=s0E?Qf))_! zEGM7>a-0QMY(bsnfj9Ad6)0W^!QJoDY4`w|3&x>d>5hW01dGYliHUxH+cj3%=JI85N&{9<;ZKnMuOlCfW4^Z?i12r~l-YnZQwlvFRO! z{+3IgF=!nw!a|7V65{C&Z%~N3M0BuRmTooj?5@?8IxB?vIKF zdP)n?s>m#_dqqGOKcF5OTH@jfh<7gdOOj57U+WAuzLP>0n$aoEXVSvl@H2^y_Nm&Z z;&T!|E#$2RW#~n>H2q$t+EJ`^Nx!J-?o5fK9HGoEsjY(Wpj=I%`08xwcMhSbaJ}3m zL2jMNq;N2QT_o&zF%WENH>w)ynAvn=%$_lFFS@_{HJ71Yg&oB{l6gH-No25SSLt7} zZD30VzF3jG9`1j>`+9r;6DOQp>mzlcZ_XvPTB0W1#N&@4Mr-kK!^8mXH~okVLAPcL z%c@jY$I*v5V_`{Wk|x)xzYf%#Gb7mCO>1@72-Zt?Pk6`hmq|VM^*vs7H|-+-=~KIZ z3#@I+jS(oZdN|s^A2Fox;vJ_*PFyqkdrn?v&m3V4MP404nDd;wSNolls$u)}@+vwd zNXF}%ie=Ziq{5;1g_ZlKMlZ^LOj>FVnZ&Gc6;w*@Xupq@+cma2aW+FX>$yLlPE}pH zbk*Wg5VnaadAXMl-%I#;F&56C$@(?6<9D)-$b**RVzVQy3#N-@L=M z(2*94`z^PM)gOJbB=)1{pr(Vm;ep#!#SxKj2)IWAw*O~yNDP655npjUTXjbwlN&~? zcj7qU4Q-Dx+ooG#nG;4{shP2jg*3w0Hq^b+T(IhvUOro8Yd~fjcbZ}mTUi-g9;Tf9 zfC2znZZLVTD7X>8S+N7Rih^|{!{tY%NC4Qw$pj(`l*V|cNZWbTD8lXe|AY+jKd7E7 z(&qc398KwRHT78XU7qt!a%7rZ-BmY`t=fK@mnvOw z^sts?wV3!D?Wc*y(JK<}r}H}6Q<)3+;fEI#so_Q@^Mh*Z5&dt^P~4CE@DT=w z^BcNz_N`sXifrm9x5dVW$m2R^XN5xQzKql@N?I+gJf69iuuGvgegGrsGG9e(hc9P&P<^{vKUh;(kvkEkfZ6++?>a;w-0ks zm|mfHAA<*I>OkClO_5CB-k@8i8flo=K59+qVNc4(U^zWaGHNJ|k?={=JX@-?L#Iev zS<1f#{p>iw{F!%YP5oX)7Z01uxx+e-#~=KGNQjU!rbm@w5=iB`1-gD)ralDTRf&w3 z>iB!Cr?`d=8Zq?y2XY%3#UZA68jp&S1$!Ox+aojqHMbl$SCubUQbit&8>h z%pQH^4_B}3{-}o!U$I*Ne;e&S%Pm?(J3j~}MK{D6EX1W}(tOX>#? zk~b+X;5MQPOf@EA5OmO!s3gk@7N;`zbPERnRB!=+^d^SkPS9jSXF;(51-JmD_q|+4p%Cs+hT@wT z*`RlT0?bGVz#vTxKu-fHm7fpCS$yE*TZpy`a)I4MZ!`rFxa}YZNrHVs5(fFs41zGA z2Z>$#|6Q3shvo%iN$k+~Nf^W|*iK@XX$^96-v)d|k^r>=(XihGff%H^Jp@7THIQAo z|6Z(|KNq~;31m89e>p@9PQaEEIzTwIHh%