Skip to content

Instantly share code, notes, and snippets.

@g-l-i-t-c-h-o-r-s-e
Last active November 23, 2025 09:38
Show Gist options
  • Select an option

  • Save g-l-i-t-c-h-o-r-s-e/285addafadd417706580d8e1824ea55c to your computer and use it in GitHub Desktop.

Select an option

Save g-l-i-t-c-h-o-r-s-e/285addafadd417706580d8e1824ea55c to your computer and use it in GitHub Desktop.
Launchpad MIDI Input (with Light Control) in Quartz Composer
#!/usr/bin/env bash
set -euo pipefail
# --- config (override via env) ---
NAME="${NAME:-LaunchPadMIDIControl}"
CLASS="${CLASS:-LaunchPadMIDIControlPlugIn}"
# Allow overriding SRC, but default to both plugin + view controller .m files
# Example override:
# SRC="LaunchPadMIDIControlPlugIn.m SomeOtherViewController.m" ./build.sh
SRC="${SRC:-LaunchPadMIDIControlPlugIn.m LaunchPadMIDIControlPlugInViewController.m}"
PLUG="$NAME.plugin"
OUT="$(pwd)/build-launchpad"
INST="$HOME/Library/Graphics/Quartz Composer Plug-Ins"
XCODE_APP="${XCODE_APP:-/Applications/Xcode_9.4.1.app}"
DEV="$XCODE_APP/Contents/Developer"
SDKDIR="$DEV/Platforms/MacOSX.platform/Developer/SDKs"
DO_I386="1"
# Clean output directory first (set CLEAN=0 to keep)
CLEAN="${CLEAN:-1}"
# Prefer 10.14, fall back to 10.13, else xcrun
SDK="${SDK:-}"
if [[ -z "${SDK}" ]]; then
if [[ -d "$SDKDIR/MacOSX10.14.sdk" ]]; then SDK="$SDKDIR/MacOSX10.14.sdk"
elif [[ -d "$SDKDIR/MacOSX10.13.sdk" ]]; then SDK="$SDKDIR/MacOSX10.13.sdk"
else SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
fi
fi
[[ -d "$DEV" ]] || { echo "Xcode not found: $XCODE_APP"; exit 1; }
# Check every source file in SRC
for f in $SRC; do
[[ -f "$f" ]] || { echo "Source not found: $f"; exit 1; }
done
[[ -n "${SDK:-}" && -d "$SDK" ]] || { echo "macOS SDK not found."; exit 1; }
export DEVELOPER_DIR="$DEV"
echo "Using SDK: $SDK"
# Clean build output so there are no stale slices
if [[ "$CLEAN" == "1" ]]; then
echo "Cleaning: rm -rf '$OUT'"
rm -rf "$OUT"
fi
mkdir -p "$OUT/i386" "$OUT/x86_64" "$OUT/universal/$PLUG/Contents/MacOS"
# Arch toggles
# Default: skip i386 on 10.14 SDK; allow override with DO_I386=1
if [[ -z "${DO_I386:-}" ]]; then
if [[ "$SDK" == *"10.14.sdk"* ]]; then DO_I386=0; else DO_I386=1; fi
fi
DO_X64="${DO_X64:-1}"
# Minimum OS (override with DEPLOY=10.10, etc.)
DEPLOY="${DEPLOY:-10.14}"
COMMON_CFLAGS=(
-bundle -fobjc-arc -fobjc-link-runtime
-isysroot "$SDK"
-mmacosx-version-min="$DEPLOY"
-I .
)
COMMON_LIBS=(
-framework Foundation
-framework Quartz
-framework OpenGL
-framework AppKit
-framework QuartzCore
-framework CoreMIDI
)
# ---- compile (track what we actually built) ----
BUILT_I386=0
BUILT_X64=0
if [[ "$DO_I386" == "1" ]]; then
echo "Compiling i386…"
# NOTE: $SRC deliberately NOT quoted so it splits into multiple .m files
clang -arch i386 "${COMMON_CFLAGS[@]}" $SRC "${COMMON_LIBS[@]}" -o "$OUT/i386/$NAME" || {
echo "i386 build failed (likely unsupported by this SDK/clang). Set DO_I386=0 to skip."; exit 1; }
BUILT_I386=1
else
echo "Skipping i386 (set DO_I386=1 to attempt, e.g. with 10.13 SDK)."
fi
if [[ "$DO_X64" == "1" ]]; then
echo "Compiling x86_64…"
# NOTE: $SRC deliberately NOT quoted so it splits into multiple .m files
clang -arch x86_64 "${COMMON_CFLAGS[@]}" $SRC "${COMMON_LIBS[@]}" -o "$OUT/x86_64/$NAME"
BUILT_X64=1
fi
# ---- gather only slices from this run ----
BINARIES=()
[[ "$BUILT_I386" == "1" && -f "$OUT/i386/$NAME" ]] && BINARIES+=("$OUT/i386/$NAME")
[[ "$BUILT_X64" == "1" && -f "$OUT/x86_64/$NAME" ]] && BINARIES+=("$OUT/x86_64/$NAME")
if [[ "${#BINARIES[@]}" -eq 0 ]]; then
echo "No binaries were built. Aborting."
exit 1
fi
echo "Creating bundle…"
mkdir -p "$OUT/universal/$PLUG/Contents/MacOS"
lipo -create "${BINARIES[@]}" -output "$OUT/universal/$PLUG/Contents/MacOS/$NAME"
# ---- Info.plist ----
cat >"$OUT/universal/$PLUG/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key> <string>English</string>
<key>CFBundleExecutable</key> <string>${NAME}</string>
<key>CFBundleIdentifier</key> <string>com.yourdomain.${NAME}</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>CFBundleName</key> <string>${NAME}</string>
<key>CFBundlePackageType</key> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <string>1.0</string>
<key>CFBundleSupportedPlatforms</key> <array><string>MacOSX</string></array>
<key>CFBundleVersion</key> <string>1</string>
<key>QCPlugInClasses</key>
<array>
<string>${CLASS}</string>
</array>
<key>NSPrincipalClass</key> <string>QCPlugIn</string>
<key>NSRequiresAquaSystemAppearance</key> <false/>
</dict></plist>
PLIST
echo "Final binary slices:"
lipo -info "$OUT/universal/$PLUG/Contents/MacOS/$NAME" || true
echo "Signing…"
codesign --force -s - "$OUT/universal/$PLUG" >/dev/null || true
echo "Installing to: $INST"
mkdir -p "$INST"
rsync -a "$OUT/universal/$PLUG" "$INST/"
echo "Installed: $INST/$PLUG"
echo "Restart Quartz Composer to load the new plug-in."
echo
echo "Notes:"
echo " • CLEAN=$CLEAN (set CLEAN=0 to keep previous builds)."
echo " • i386 default depends on SDK (10.14 → OFF). Override with DO_I386=0/1."
echo " • DEPLOY=${DEPLOY} (override with DEPLOY=10.10, etc.)."
echo " • SRC='$SRC' (space-separated list of .m files being compiled)."
// LaunchPadMIDIControlPlugIn.h
#import <Quartz/Quartz.h>
#import <CoreMIDI/CoreMIDI.h>
#import <Foundation/Foundation.h>
#import <stdint.h>
@interface LaunchPadMIDIControlPlugIn : QCPlugIn
{
MIDIClientRef _client;
MIDIPortRef _outPort;
MIDIPortRef _inPort;
MIDIEndpointRef _sourceEndpoint; // where we read from
MIDIEndpointRef _destEndpoint; // where we send to
BOOL _gateState; // cached Gate value (for MIDI thread)
BOOL _exclusiveState; // cached Exclusive value (for MIDI thread)
BOOL _lastGate; // for detecting Gate transitions
BOOL _lastReset; // for detecting Reset rising edge
// Per-note state for toggle
BOOL _latched[16][128]; // [channel][note] currently toggled ON?
UInt8 _latchedVelocity[16][128]; // velocity/color used when turning ON
// Per-note current ON state across all modes (momentary + latched).
BOOL _noteActive[16][128];
// Cached velocity values from sliders (0–127)
UInt8 _onVelValue; // velocity used for "LED ON"
UInt8 _offVelValue; // velocity used for "LED OFF"
// Row selector (0–8). Launchpad-style:
// row 0 → notes 0–7, row 1 → 16–23, row 2 → 32–39, ..., row 7 → 112–119.
// Row 8 effectively selects no row.
UInt8 _rowIndex;
// Last seen value of the shared-exclusive bus (-1 or 0–127)
SInt16 _lastSharedActiveNote;
NSString *_cachedDeviceName; // last applied device name filter
// -------- Formula-group ivars --------
BOOL _useFormulaGroup; // Settings tab checkbox
NSString *_groupFormula; // raw string, e.g. "AB13"
BOOL _groupMask[128]; // true for notes in the group
NSArray *_groupNoteList; // NSArray<NSNumber*> of MIDI notes
NSArray *_groupOutputKeys; // NSArray<NSString*> for dynamic group output ports
}
// Inputs
@property (copy) NSString *inputDeviceName; // device to read/send from
@property (assign) BOOL inputGate; // Gate / toggle mode
@property (assign) BOOL inputExclusive; // Exclusive mode: only one latched note at a time
@property (assign) double inputOnVelocity; // slider: 0–127, velocity for Note ON in Gate mode
@property (assign) double inputOffVelocity; // slider: 0–127, velocity for Note OFF in Gate mode
@property (assign) BOOL inputReset; // Reset: sends Note Off 0 to all notes on rising edge
@property (assign) double inputRow; // Launchpad row (0–8)
// Shared active note bus (0–127, or -1 if none)
@property (assign) double inputSharedActive;
// NOTE:
// • outputNote0..outputNote7 *and* outputSharedActive are now dynamic ports only.
// • They are NOT declared as properties here.
@end
//
// LaunchPadMIDIControlPlugIn.m
//
#import "LaunchPadMIDIControlPlugIn.h"
#import "LaunchPadMIDIControlPlugInViewController.h"
#import <Quartz/Quartz.h>
#import <CoreMIDI/CoreMIDI.h>
#import <Foundation/Foundation.h>
#import <string.h>
#import <stdint.h>
static void LaunchPadMIDIControlReadProc(const MIDIPacketList *pktlist,
void *refCon,
void *srcConnRefCon);
#pragma mark - Formula helper
// Parse a formula like "AB13" into an array of MIDI notes.
// Rows: A..H → Launchpad rows 0..7
// Cols: 1..8 → Launchpad columns 0..7
// Note: row * 16 + colIndex
//
// Examples:
// "A1" → [0]
// "C5" → [row 2, col 4] → [36]
// "AB13" → rows A..B, cols 1..3 → [0,1,2,16,17,18]
static NSArray<NSNumber *> *LPMNotesForFormula(NSString *formula)
{
if (!formula)
return @[];
NSString *trimmed = [[formula stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]]
uppercaseString];
if (trimmed.length == 0)
return @[];
NSMutableString *rowPart = [NSMutableString string];
NSMutableString *colPart = [NSMutableString string];
NSUInteger len = trimmed.length;
BOOL seenDigit = NO;
for (NSUInteger i = 0; i < len; ++i) {
unichar c = [trimmed characterAtIndex:i];
if ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:c]) {
continue;
}
if (c >= 'A' && c <= 'H') {
if (seenDigit) {
// Letters after digits → invalid
return @[];
}
[rowPart appendFormat:@"%C", c];
}
else if (c >= '0' && c <= '9') {
seenDigit = YES;
[colPart appendFormat:@"%C", c];
}
else {
// Unknown character → invalid
return @[];
}
}
if (rowPart.length == 0 || colPart.length == 0 ||
rowPart.length > 2 || colPart.length > 2) {
return @[];
}
// Rows A–H → indices 0–7
int rowStart = (int)([rowPart characterAtIndex:0] - 'A');
int rowEnd = rowStart;
if (rowPart.length == 2) {
rowEnd = (int)([rowPart characterAtIndex:1] - 'A');
}
if (rowStart < 0 || rowStart > 7 || rowEnd < 0 || rowEnd > 7)
return @[];
if (rowStart > rowEnd) {
int tmp = rowStart; rowStart = rowEnd; rowEnd = tmp;
}
// Columns: 1–8 → indices 0–7
int colStart = (int)([colPart characterAtIndex:0] - '0'); // 1..8
int colEnd = colStart;
if (colPart.length == 2) {
colEnd = (int)([colPart characterAtIndex:1] - '0');
}
if (colStart < 1 || colStart > 8 || colEnd < 1 || colEnd > 8)
return @[];
if (colStart > colEnd) {
int tmp = colStart; colStart = colEnd; colEnd = tmp;
}
int colStartIdx = colStart - 1; // 0–7
int colEndIdx = colEnd - 1; // 0–7
NSMutableArray<NSNumber *> *notes = [NSMutableArray array];
for (int r = rowStart; r <= rowEnd; ++r) {
for (int c = colStartIdx; c <= colEndIdx; ++c) {
int note = r * 16 + c; // Launchpad mapping
if (note >= 0 && note < 128) {
[notes addObject:@(note)];
}
}
}
return notes;
}
// Convert a MIDI note number (0–127) to a name like "C-2", "C#-2", "E-1", "G#0", "C2".
// Mapping is:
// 0 -> C-2
// 1 -> C#-2
// 16 -> E-1
// 32 -> G#0
// 48 -> C2
static NSString *LPMNoteNameForMIDINote(int note)
{
if (note < 0 || note > 127) {
return @"?";
}
static NSString *const kNames[12] = {
@"C", @"C#", @"D", @"D#", @"E", @"F",
@"F#", @"G", @"G#", @"A", @"A#", @"B"
};
int pitchClass = note % 12;
int octave = note / 12 - 2; // 0 → C-2, 16 → E-1, 32 → G#0, 48 → C2
return [NSString stringWithFormat:@"%@%d", kNames[pitchClass], octave];
}
#pragma mark - Private extension for formula-group state
@interface LaunchPadMIDIControlPlugIn ()
// Settings tab (NOT QC ports, internal only)
@property (nonatomic, copy) NSString *groupFormula;
@property (nonatomic, assign) BOOL useFormulaGroup;
@end
@implementation LaunchPadMIDIControlPlugIn
// only property-based ports are inputs now
@dynamic inputDeviceName, inputGate, inputExclusive, inputOnVelocity, inputOffVelocity, inputReset, inputRow, inputSharedActive;
// synthesize private settings
@synthesize groupFormula = _groupFormula;
@synthesize useFormulaGroup = _useFormulaGroup;
#pragma mark - QC metadata
+ (NSDictionary *)attributes
{
return @{
QCPlugInAttributeNameKey : @"Launchpad MIDI Relay",
QCPlugInAttributeDescriptionKey : @"MIDI echo/relay with Gate/Exclusive plus 8 boolean outputs for a Launchpad-style row (0–7 → notes 0–119, spaced by 16). Includes a shared active-note bus to enforce global exclusivity across multiple instances, and an optional formula-based group mode for arbitrary 8x8 regions."
};
}
// Provider so the patch can be used as a data source with outputs
+ (QCPlugInExecutionMode)executionMode
{
return kQCPlugInExecutionModeProvider;
}
+ (QCPlugInTimeMode)timeMode
{
return kQCPlugInTimeModeNone;
}
// QC only respects sortedPropertyPortKeys for *property-based* ports.
// We expose only inputs as properties; all outputs are dynamic so we can
// freely reorder them. This also avoids the "protected port" issue.
+ (NSArray *)sortedPropertyPortKeys
{
return @[
// Inputs
@"inputReset", // just under Enable
@"inputGate", // Gate / toggle
@"inputExclusive", // exclusive/mono mode
@"inputDeviceName", // device selection
@"inputRow", // Launchpad row selector
@"inputOnVelocity", // slider for ON velocity
@"inputOffVelocity", // slider for OFF velocity
@"inputSharedActive" // shared active-note input (bus)
// no property-based outputs
];
}
+ (NSArray *)plugInKeys
{
// Internal settings that should be serialized with the composition
// but NOT exposed as input ports.
NSArray *superKeys = [super plugInKeys];
NSArray *myKeys = @[ @"groupFormula", @"useFormulaGroup" ];
return superKeys ? [superKeys arrayByAddingObjectsFromArray:myKeys] : myKeys;
}
// Attributes for property-based ports only (inputs)
+ (NSDictionary *)attributesForPropertyPortWithKey:(NSString *)key
{
// -------- INPUT PORTS --------
if ([key isEqualToString:@"inputDeviceName"])
{
return @{
QCPortAttributeNameKey : @"Device Name",
QCPortAttributeTypeKey : QCPortTypeString,
QCPortAttributeDefaultValueKey : @"Launchpad Mini 9"
};
}
else if ([key isEqualToString:@"inputGate"])
{
return @{
QCPortAttributeNameKey : @"Gate/Toggle",
QCPortAttributeTypeKey : QCPortTypeBoolean,
QCPortAttributeDefaultValueKey : @NO
};
}
else if ([key isEqualToString:@"inputExclusive"])
{
return @{
QCPortAttributeNameKey : @"Exclusive Mode",
QCPortAttributeTypeKey : QCPortTypeBoolean,
QCPortAttributeDefaultValueKey : @NO
};
}
else if ([key isEqualToString:@"inputOnVelocity"])
{
return @{
QCPortAttributeNameKey : @"On Velocity",
QCPortAttributeTypeKey : QCPortTypeNumber,
QCPortAttributeDefaultValueKey : @127.0,
QCPortAttributeMinimumValueKey : @0.0,
QCPortAttributeMaximumValueKey : @127.0
};
}
else if ([key isEqualToString:@"inputOffVelocity"])
{
return @{
QCPortAttributeNameKey : @"Off Velocity",
QCPortAttributeTypeKey : QCPortTypeNumber,
QCPortAttributeDefaultValueKey : @0.0,
QCPortAttributeMinimumValueKey : @0.0,
QCPortAttributeMaximumValueKey : @127.0
};
}
else if ([key isEqualToString:@"inputReset"])
{
return @{
QCPortAttributeNameKey : @"Reset",
QCPortAttributeTypeKey : QCPortTypeBoolean,
QCPortAttributeDefaultValueKey : @NO
};
}
else if ([key isEqualToString:@"inputRow"])
{
// 0–8: 8 rows + an optional “no row” state at 8
return @{
QCPortAttributeNameKey : @"Row",
QCPortAttributeTypeKey : QCPortTypeNumber,
QCPortAttributeDefaultValueKey : @0.0,
QCPortAttributeMinimumValueKey : @0.0,
QCPortAttributeMaximumValueKey : @8.0
};
}
else if ([key isEqualToString:@"inputSharedActive"])
{
// Number: -1.0 = none, 0–127 = MIDI note number
return @{
QCPortAttributeNameKey : @"Shared Active In",
QCPortAttributeTypeKey : QCPortTypeNumber,
QCPortAttributeDefaultValueKey : @(-1.0),
QCPortAttributeMinimumValueKey : @(-1.0),
QCPortAttributeMaximumValueKey : @127.0
};
}
return [super attributesForPropertyPortWithKey:key];
}
#pragma mark - Settings view controller
- (QCPlugInViewController *)createViewController
{
// Settings tab UI: formula text field + "Use formula group" checkbox
return [[LaunchPadMIDIControlPlugInViewController alloc] initWithPlugIn:self
viewNibName:nil];
}
#pragma mark - Dynamic output helpers
// Safe wrapper for dynamic port removal (in case QC marks something protected)
- (void)_safeRemoveOutputPortForKey:(NSString *)key
{
@try {
[self removeOutputPortForKey:key];
} @catch (__unused NSException *ex) {
// Ignore (e.g. protected or doesn't exist)
}
}
// Create the "original" 8 Note outputs as dynamic ports:
// keys: outputNote0..outputNote7, names: "Note 1".."Note 8".
- (void)_createRowOutputPorts
{
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote0"
withAttributes:@{ QCPortAttributeNameKey : @"Note 1" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote1"
withAttributes:@{ QCPortAttributeNameKey : @"Note 2" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote2"
withAttributes:@{ QCPortAttributeNameKey : @"Note 3" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote3"
withAttributes:@{ QCPortAttributeNameKey : @"Note 4" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote4"
withAttributes:@{ QCPortAttributeNameKey : @"Note 5" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote5"
withAttributes:@{ QCPortAttributeNameKey : @"Note 6" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote6"
withAttributes:@{ QCPortAttributeNameKey : @"Note 7" }];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:@"outputNote7"
withAttributes:@{ QCPortAttributeNameKey : @"Note 8" }];
}
// Remove the 8 row outputs (used when formula mode is enabled)
- (void)_destroyRowOutputPorts
{
[self _safeRemoveOutputPortForKey:@"outputNote0"];
[self _safeRemoveOutputPortForKey:@"outputNote1"];
[self _safeRemoveOutputPortForKey:@"outputNote2"];
[self _safeRemoveOutputPortForKey:@"outputNote3"];
[self _safeRemoveOutputPortForKey:@"outputNote4"];
[self _safeRemoveOutputPortForKey:@"outputNote5"];
[self _safeRemoveOutputPortForKey:@"outputNote6"];
[self _safeRemoveOutputPortForKey:@"outputNote7"];
}
// Ensure Shared Active Out exists and is at the very bottom by
// removing and re-adding it last.
- (void)_ensureSharedActiveOutputAtBottom
{
[self _safeRemoveOutputPortForKey:@"outputSharedActive"];
[self addOutputPortWithType:QCPortTypeNumber
forKey:@"outputSharedActive"
withAttributes:@{ QCPortAttributeNameKey : @"Shared Active Out" }];
}
#pragma mark - Init / dealloc
- (id)init
{
self = [super init];
if (self) {
_client = 0;
_outPort = 0;
_inPort = 0;
_sourceEndpoint = 0;
_destEndpoint = 0;
_gateState = NO;
_exclusiveState = NO;
_lastGate = NO;
_lastReset = NO;
_cachedDeviceName = nil;
memset(_latched, 0, sizeof(_latched));
memset(_latchedVelocity, 0, sizeof(_latchedVelocity));
memset(_noteActive, 0, sizeof(_noteActive));
_onVelValue = 127;
_offVelValue = 0;
_rowIndex = 0;
_lastSharedActiveNote = -1;
// Formula-group defaults
_useFormulaGroup = NO;
_groupFormula = @"";
memset(_groupMask, 0, sizeof(_groupMask));
_groupNoteList = @[];
_groupOutputKeys = @[];
// Start with the classic 8 row outputs as dynamic ports
[self _createRowOutputPorts];
// And Shared Active Out at the very bottom
[self _ensureSharedActiveOutputAtBottom];
}
return self;
}
- (void)dealloc
{
if (_inPort) {
if (_sourceEndpoint) {
MIDIPortDisconnectSource(_inPort, _sourceEndpoint);
_sourceEndpoint = 0;
}
MIDIPortDispose(_inPort);
_inPort = 0;
}
if (_outPort) {
_destEndpoint = 0;
MIDIPortDispose(_outPort);
_outPort = 0;
}
if (_client) {
MIDIClientDispose(_client);
_client = 0;
}
}
#pragma mark - Formula-group helpers
// Apply a list of notes as the current group: update mask, list, and dynamic ports
- (void)_applyGroupNotes:(NSArray<NSNumber *> *)notes
{
// 1) Update mask + list
@synchronized (self) {
memset(_groupMask, 0, sizeof(_groupMask));
_groupNoteList = [notes copy];
for (NSNumber *num in _groupNoteList) {
int n = num.intValue;
if (n >= 0 && n < 128) {
_groupMask[n] = YES;
}
}
}
// 2) Rebuild dynamic boolean output ports for each note in the group
// Remove old dynamic group ports
for (NSString *key in _groupOutputKeys) {
[self _safeRemoveOutputPortForKey:key];
}
_groupOutputKeys = @[];
if (!_useFormulaGroup || _groupNoteList.count == 0) {
// Make sure Shared Active Out still exists and is at bottom even if no group
[self _ensureSharedActiveOutputAtBottom];
return;
}
NSMutableArray *newKeys = [NSMutableArray arrayWithCapacity:_groupNoteList.count];
for (NSNumber *num in _groupNoteList) {
int note = num.intValue;
if (note < 0 || note >= 128)
continue;
// Launchpad 8x8 grid mapping:
// rowIndex = 0..7 (A..H)
// colIndex = 0..7 (1..8)
// MIDI mapping is row * 16 + col, but pads are logically 8-wide.
int rowIndex = note / 16;
int colIndex = note % 16; // 0–15; we only keep 0–7 for 8x8
if (rowIndex < 0 || rowIndex > 7 || colIndex < 0 || colIndex > 7) {
// Outside 8x8 grid – skip dynamic port
continue;
}
// --- Pad index: left-to-right, row-by-row, starting at 0 ---
// Row 0: indices 0–7
// Row 1: indices 8–15
// Row 2: indices 16–23
int padIndex = rowIndex * 8 + colIndex;
// Proper musical note name, e.g. "C-2", "E-1", "G#0", "C2"
NSString *noteName = LPMNoteNameForMIDINote(note);
// Final label:
// "E-1 (note 8 / midi 16)"
// So "note" matches your 0..63 grid counting, and "midi" is the raw MIDI note.
NSString *portName = [NSString stringWithFormat:@"%@ (note %d)", // "%@ (note %d / midi %d)"
noteName, padIndex, note];
NSString *portKey = [NSString stringWithFormat:@"group_%d", note];
[self addOutputPortWithType:QCPortTypeBoolean
forKey:portKey
withAttributes:@{ QCPortAttributeNameKey : portName }];
[newKeys addObject:portKey];
}
_groupOutputKeys = [newKeys copy];
// Ensure Shared Active Out is last after group ports
[self _ensureSharedActiveOutputAtBottom];
}
- (void)_rebuildGroupFromSettings
{
if (!_useFormulaGroup || !_groupFormula || _groupFormula.length == 0) {
// When formula mode is off or formula is empty, just clear group ports/masks
[self _applyGroupNotes:@[]];
return;
}
NSArray<NSNumber *> *notes = LPMNotesForFormula(_groupFormula);
[self _applyGroupNotes:notes];
}
// KVC setters, called from Settings UI
- (void)setGroupFormula:(NSString *)groupFormula
{
if (groupFormula == _groupFormula || [groupFormula isEqualToString:_groupFormula]) {
return;
}
_groupFormula = [groupFormula copy];
[self _rebuildGroupFromSettings];
}
- (void)setUseFormulaGroup:(BOOL)useFormulaGroup
{
if (_useFormulaGroup == useFormulaGroup)
return;
_useFormulaGroup = useFormulaGroup;
if (_useFormulaGroup) {
// Formula mode ON: remove the row outputs so we only have formula ports + Shared Active Out
[self _destroyRowOutputPorts];
} else {
// Formula mode OFF: remove any formula ports and recreate the row outputs
for (NSString *key in _groupOutputKeys) {
[self _safeRemoveOutputPortForKey:key];
}
_groupOutputKeys = @[];
[self _createRowOutputPorts];
}
// Rebuild formula group (will also reposition Shared Active Out)
[self _rebuildGroupFromSettings];
// In row mode, ensure Shared Active Out is at bottom after row ports
if (!_useFormulaGroup) {
[self _ensureSharedActiveOutputAtBottom];
}
}
#pragma mark - MIDI endpoint selection
// Helper: match endpoint by display name substring
- (BOOL)endpoint:(MIDIEndpointRef)ep matchesFilterName:(NSString *)filter
{
if (!ep)
return NO;
if (!filter || filter.length == 0)
return YES;
CFStringRef cfName = NULL;
OSStatus err = MIDIObjectGetStringProperty(ep, kMIDIPropertyDisplayName, &cfName);
if (err != noErr || !cfName)
return NO;
NSString *name = CFBridgingRelease(cfName);
if (!name)
return NO;
NSRange r = [name rangeOfString:filter options:NSCaseInsensitiveSearch];
return (r.location != NSNotFound);
}
// Select one source and one destination for the given filter
- (void)selectEndpointsForFilter:(NSString *)filter
{
_sourceEndpoint = 0;
_destEndpoint = 0;
NSString *trimmed = filter ?: @"";
trimmed = [trimmed stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Pick source
ItemCount srcCount = MIDIGetNumberOfSources();
for (ItemCount i = 0; i < srcCount; ++i) {
MIDIEndpointRef src = MIDIGetSource(i);
if (!src)
continue;
if (trimmed.length == 0 || [self endpoint:src matchesFilterName:trimmed]) {
_sourceEndpoint = src;
break;
}
}
// Pick destination
ItemCount destCount = MIDIGetNumberOfDestinations();
for (ItemCount i = 0; i < destCount; ++i) {
MIDIEndpointRef dest = MIDIGetDestination(i);
if (!dest)
continue;
if (trimmed.length == 0 || [self endpoint:dest matchesFilterName:trimmed]) {
_destEndpoint = dest;
break;
}
}
// Connect input to selected source
if (_inPort && _sourceEndpoint) {
MIDIPortConnectSource(_inPort, _sourceEndpoint, (void *)(uintptr_t)_sourceEndpoint);
}
}
- (void)disconnectCurrentSource
{
if (_inPort && _sourceEndpoint) {
MIDIPortDisconnectSource(_inPort, _sourceEndpoint);
_sourceEndpoint = 0;
}
}
- (void)updateEndpointsIfNeeded
{
NSString *current = self.inputDeviceName ?: @"";
current = [current stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
BOOL same = NO;
if (_cachedDeviceName == nil) {
same = (current.length == 0);
} else {
same = [_cachedDeviceName isEqualToString:current];
}
if (same)
return;
[self disconnectCurrentSource];
_destEndpoint = 0;
_cachedDeviceName = [current copy];
[self selectEndpointsForFilter:current];
}
#pragma mark - MIDI send / receive
// Send a simple 3-byte MIDI message (status, data1, data2) to the selected destination
- (void)sendMIDIStatus:(UInt8)status data1:(UInt8)data1 data2:(UInt8)data2
{
if (!_client || !_outPort)
return;
UInt8 bytes[3];
bytes[0] = status;
bytes[1] = data1;
bytes[2] = data2;
MIDIPacketList packetList;
MIDIPacket *packet = MIDIPacketListInit(&packetList);
packet = MIDIPacketListAdd(&packetList,
sizeof(packetList),
packet,
0,
sizeof(bytes),
bytes);
if (!packet)
return;
if (_destEndpoint) {
MIDISend(_outPort, _destEndpoint, &packetList);
} else {
ItemCount destCount = MIDIGetNumberOfDestinations();
for (ItemCount i = 0; i < destCount; ++i) {
MIDIEndpointRef dest = MIDIGetDestination(i);
if (dest) {
MIDISend(_outPort, dest, &packetList);
}
}
}
}
// Handle a single Note message (Note On/Off) with Gate, Exclusive & group/row logic,
// and keep _noteActive[] in sync for the boolean outputs.
- (void)handleStatus:(UInt8)status data1:(UInt8)d1 data2:(UInt8)d2
{
UInt8 type = status & 0xF0;
UInt8 chan = status & 0x0F;
UInt8 note = d1;
if (chan >= 16 || note >= 128)
return;
BOOL gate, exclusive;
UInt8 onVel, offVel;
UInt8 rowIdx;
BOOL useFormula;
BOOL groupMaskLocal[128];
memset(groupMaskLocal, 0, sizeof(groupMaskLocal));
@synchronized (self) {
gate = _gateState;
exclusive = _exclusiveState;
onVel = _onVelValue;
offVel = _offVelValue;
rowIdx = _rowIndex;
useFormula = _useFormulaGroup;
if (useFormula) {
memcpy(groupMaskLocal, _groupMask, sizeof(_groupMask));
}
}
// Launchpad-like mapping:
// rowIdx 0 → notes 0–7, rowIdx 1 → 16–23, ..., rowIdx 7 → 112–119.
UInt8 rowMin = (UInt8)(rowIdx * 16);
UInt8 rowMax = (UInt8)(rowMin + 7); // 8 buttons per row
BOOL isNoteOnPress = (type == 0x90 && d2 > 0);
BOOL isNoteOffLike = (type == 0x80) || (type == 0x90 && d2 == 0);
BOOL inTarget = useFormula
? (note < 128 && groupMaskLocal[note])
: (note >= rowMin && note <= rowMax);
// --- Out-of-group / out-of-row notes: ignore them completely ---
if (!inTarget) {
return;
}
// --- Gate OFF → momentary mode (no latching),
// but we still track ON/OFF state in _noteActive[].
if (!gate) {
if (isNoteOnPress) {
UInt8 useVel = onVel; // 0–127
[self sendMIDIStatus:status data1:note data2:useVel];
@synchronized (self) {
_noteActive[chan][note] = YES;
}
}
else if (isNoteOffLike) {
UInt8 useVel = offVel; // often 0
[self sendMIDIStatus:status data1:note data2:useVel];
@synchronized (self) {
_noteActive[chan][note] = NO;
}
}
return;
}
// --- Gate ON → per-pad toggle driven ONLY by Note On presses
// No Note Off-like messages are forwarded while gate is ON.
if (isNoteOnPress) {
BOOL wasLatched;
UInt8 lastVel;
@synchronized (self) {
wasLatched = _latched[chan][note];
lastVel = _latchedVelocity[chan][note];
}
if (!wasLatched) {
// First press → toggle ON
// If Exclusive mode: turn OFF any other latched notes on this channel
// within the SAME active group (row or formula).
if (exclusive) {
for (int otherNote = 0; otherNote < 128; ++otherNote) {
if (otherNote == note)
continue;
BOOL otherInTarget = useFormula
? groupMaskLocal[otherNote]
: (otherNote >= rowMin && otherNote <= rowMax);
BOOL otherLatched = NO;
@synchronized (self) {
if (_latched[chan][otherNote]) {
otherLatched = YES;
_latched[chan][otherNote] = NO;
_latchedVelocity[chan][otherNote] = 0;
_noteActive[chan][otherNote] = NO;
}
}
if (otherLatched && otherInTarget) {
UInt8 offStatus = (UInt8)(0x90 | chan);
[self sendMIDIStatus:offStatus
data1:(UInt8)otherNote
data2:offVel];
}
}
}
UInt8 useVel = onVel;
if (useVel == 0) {
useVel = (lastVel > 0) ? lastVel : 127;
}
@synchronized (self) {
_latched[chan][note] = YES;
_latchedVelocity[chan][note] = useVel;
_noteActive[chan][note] = YES;
}
UInt8 onStatus = (UInt8)(0x90 | chan);
[self sendMIDIStatus:onStatus data1:note data2:useVel];
} else {
// Second press → toggle OFF
UInt8 useVel = offVel; // usually 0
@synchronized (self) {
_latched[chan][note] = NO;
_latchedVelocity[chan][note] = 0;
_noteActive[chan][note] = NO;
}
UInt8 onStatus = (UInt8)(0x90 | chan);
[self sendMIDIStatus:onStatus data1:note data2:useVel];
}
}
else if (isNoteOffLike) {
// Ignore releases in Gate mode
return;
}
}
// CoreMIDI read callback processing
- (void)processIncomingPacketList:(const MIDIPacketList *)pktlist
fromSource:(MIDIEndpointRef)source
{
if (_sourceEndpoint && source && source != _sourceEndpoint)
return;
const MIDIPacket *packet = &pktlist->packet[0];
for (UInt32 p = 0; p < pktlist->numPackets; ++p) {
const UInt8 *data = packet->data;
UInt16 len = packet->length;
UInt16 i = 0;
while (i + 2 < len) {
UInt8 status = data[i];
if ((status & 0x80) == 0) {
i++;
continue;
}
UInt8 type = status & 0xF0;
if (type == 0x90 || type == 0x80) {
UInt8 d1 = data[i + 1];
UInt8 d2 = data[i + 2];
[self handleStatus:status data1:d1 data2:d2];
i += 3;
} else {
i++;
}
}
packet = MIDIPacketNext(packet);
}
}
#pragma mark - Reset helpers
// Flush all latched notes (turn LEDs off using Off Velocity in Gate mode)
// and clear _noteActive[] as well.
- (void)flushLatchedNotes
{
UInt8 offVel;
@synchronized (self) {
offVel = _offVelValue;
}
@synchronized (self) {
for (int ch = 0; ch < 16; ++ch) {
for (int note = 0; note < 128; ++note) {
if (_latched[ch][note]) {
UInt8 status = (UInt8)(0x90 | (ch & 0x0F));
[self sendMIDIStatus:status data1:(UInt8)note data2:offVel];
_latched[ch][note] = NO;
_latchedVelocity[ch][note] = 0;
_noteActive[ch][note] = NO;
}
}
}
}
}
// Reset → send Note Off 0 to *all* notes on all channels
- (void)sendGlobalResetNotesOff
{
for (int ch = 0; ch < 16; ++ch) {
UInt8 status = (UInt8)(0x80 | (ch & 0x0F));
for (int note = 0; note < 128; ++note) {
[self sendMIDIStatus:status data1:(UInt8)note data2:0];
}
}
@synchronized (self) {
memset(_latched, 0, sizeof(_latched));
memset(_latchedVelocity, 0, sizeof(_latchedVelocity));
memset(_noteActive, 0, sizeof(_noteActive));
}
}
#pragma mark - QC execution lifecycle
// Create CoreMIDI client, ports; endpoints are selected later in execute
- (BOOL)startExecution:(id<QCPlugInContext>)context
{
OSStatus err = MIDIClientCreate(CFSTR("QC LaunchPad MIDI Control PlugIn"),
NULL,
NULL,
&_client);
if (err != noErr || !_client) {
_client = 0;
return NO;
}
err = MIDIOutputPortCreate(_client, CFSTR("Output"), &_outPort);
if (err != noErr || !_outPort) {
if (_client) {
MIDIClientDispose(_client);
_client = 0;
}
return NO;
}
err = MIDIInputPortCreate(_client,
CFSTR("Input"),
LaunchPadMIDIControlReadProc,
(__bridge void *)self,
&_inPort);
if (err != noErr || !_inPort) {
if (_outPort) {
MIDIPortDispose(_outPort);
_outPort = 0;
}
if (_client) {
MIDIClientDispose(_client);
_client = 0;
}
return NO;
}
_sourceEndpoint = 0;
_destEndpoint = 0;
_cachedDeviceName = nil;
BOOL gateNow = self.inputGate;
BOOL resetNow = self.inputReset;
BOOL exclusiveNow = self.inputExclusive;
double onV = self.inputOnVelocity;
double offV = self.inputOffVelocity;
double rowVal = self.inputRow;
// Clamp row to 0–8
if (rowVal < 0.0) rowVal = 0.0;
if (rowVal > 8.0) rowVal = 8.0;
if (onV < 0.0) onV = 0.0;
if (onV > 127.0) onV = 127.0;
if (offV < 0.0) offV = 0.0;
if (offV > 127.0) offV = 127.0;
@synchronized (self) {
_gateState = gateNow;
_exclusiveState = exclusiveNow;
_lastGate = gateNow;
_lastReset = resetNow;
_onVelValue = (UInt8)(onV + 0.5);
_offVelValue = (UInt8)(offV + 0.5);
_rowIndex = (UInt8)(rowVal + 0.5);
_lastSharedActiveNote = -1;
}
memset(_latched, 0, sizeof(_latched));
memset(_latchedVelocity, 0, sizeof(_latchedVelocity));
memset(_noteActive, 0, sizeof(_noteActive));
// group mask is already cleared in init; no change here
return YES;
}
- (void)stopExecution:(id<QCPlugInContext>)context
{
// IMPORTANT: do NOT send any MIDI when the patch stops or is deleted.
// Use the Reset input while the patch is running if you want a global clear.
[self disconnectCurrentSource];
_destEndpoint = 0;
_cachedDeviceName = nil;
if (_inPort) {
MIDIPortDispose(_inPort);
_inPort = 0;
}
if (_outPort) {
MIDIPortDispose(_outPort);
_outPort = 0;
}
if (_client) {
MIDIClientDispose(_client);
_client = 0;
}
// Runtime state is cleared…
memset(_latched, 0, sizeof(_latched));
memset(_latchedVelocity, 0, sizeof(_latchedVelocity));
memset(_noteActive, 0, sizeof(_noteActive));
_lastSharedActiveNote = -1;
// *** CRITICAL CHANGE ***
//
// DO NOT clear the formula group configuration here.
// These are structural settings that need to survive Viewer stop/start.
//
// The following lines were the reason formula groups died after restart:
//
// memset(_groupMask, 0, sizeof(_groupMask));
// _groupNoteList = @[];
// _groupOutputKeys = @[];
//
// Removing them keeps the group’s mask and dynamic ports intact so that
// handleStatus:/execute: will still see notes as “in group” when the
// Viewer is started again, even inside a macro with multiple instances.
}
- (BOOL)execute:(id<QCPlugInContext>)context
atTime:(NSTimeInterval)time
withArguments:(NSDictionary *)arguments
{
if (_client && _inPort && _outPort) {
[self updateEndpointsIfNeeded];
}
// Read inputs
double onV = self.inputOnVelocity;
double offV = self.inputOffVelocity;
double rowV = self.inputRow;
double sharedInRaw = self.inputSharedActive;
if (onV < 0.0) onV = 0.0;
if (onV > 127.0) onV = 127.0;
if (offV < 0.0) offV = 0.0;
if (offV > 127.0) offV = 127.0;
if (rowV < 0.0) rowV = 0.0;
if (rowV > 8.0) rowV = 8.0;
BOOL gate = self.inputGate;
BOOL reset = self.inputReset;
BOOL exclusive = self.inputExclusive;
BOOL lastGate, lastReset;
UInt8 onVel = (UInt8)(onV + 0.5);
UInt8 offVel = (UInt8)(offV + 0.5);
UInt8 rowIdx = (UInt8)(rowV + 0.5);
int sharedIn;
if (sharedInRaw < 0.0) sharedIn = -1;
else if (sharedInRaw > 127) sharedIn = 127;
else sharedIn = (int)(sharedInRaw + 0.5);
BOOL useFormulaGroup = NO;
NSArray *groupNotesSnapshot = nil;
// Update cached state and respond to shared-exclusive bus
@synchronized (self) {
lastGate = _lastGate;
lastReset = _lastReset;
_gateState = gate;
_exclusiveState = exclusive;
_lastGate = gate;
_lastReset = reset;
_onVelValue = onVel;
_offVelValue = offVel;
_rowIndex = rowIdx;
useFormulaGroup = _useFormulaGroup;
if (useFormulaGroup) {
groupNotesSnapshot = [_groupNoteList copy];
}
// Global exclusivity via shared bus:
// When the shared active note changes (and we're in Gate+Exclusive),
// clear any active notes in THIS group that are not equal to the new bus note.
if (gate && exclusive) {
if (sharedIn != _lastSharedActiveNote) {
if (!useFormulaGroup) {
UInt8 rowMin = (UInt8)(_rowIndex * 16);
UInt8 rowMax = (UInt8)(rowMin + 7);
for (int ch = 0; ch < 16; ++ch) {
for (UInt8 note = rowMin; note <= rowMax && note < 128; ++note) {
// Keep the global note if it lies in this row
if (sharedIn >= 0 && note == (UInt8)sharedIn)
continue;
if (_noteActive[ch][note] || _latched[ch][note]) {
UInt8 status = (UInt8)(0x90 | (ch & 0x0F));
[self sendMIDIStatus:status
data1:note
data2:_offVelValue];
_noteActive[ch][note] = NO;
_latched[ch][note] = NO;
_latchedVelocity[ch][note] = 0;
}
}
}
} else {
for (int ch = 0; ch < 16; ++ch) {
for (NSNumber *num in groupNotesSnapshot) {
int n = num.intValue;
if (n < 0 || n >= 128)
continue;
UInt8 note = (UInt8)n;
// Keep the global note if it lies in this group
if (sharedIn >= 0 && note == (UInt8)sharedIn)
continue;
if (_noteActive[ch][note] || _latched[ch][note]) {
UInt8 status = (UInt8)(0x90 | (ch & 0x0F));
[self sendMIDIStatus:status
data1:note
data2:_offVelValue];
_noteActive[ch][note] = NO;
_latched[ch][note] = NO;
_latchedVelocity[ch][note] = 0;
}
}
}
}
_lastSharedActiveNote = (SInt16)sharedIn;
}
} else {
_lastSharedActiveNote = (SInt16)sharedIn;
}
}
// Gate ON → OFF → clear toggled LEDs via Note On + Off Velocity
if (!gate && lastGate) {
[self flushLatchedNotes];
}
// Reset OFF → ON → send Note Off 0 to all notes
if (reset && !lastReset) {
[self sendGlobalResetNotesOff];
}
// ---- Update boolean states & local active note ----
BOOL states[8] = { NO, NO, NO, NO, NO, NO, NO, NO };
int localActiveNote = -1;
// For dynamic ports created from the formula group
NSArray *groupOutputKeysSnapshot = nil;
BOOL dynamicStates[128];
NSUInteger dynamicCount = 0;
@synchronized (self) {
if (!useFormulaGroup || !groupNotesSnapshot) {
// Normal row mode: 8 states for notes row*16..row*16+7
UInt8 baseNote = (UInt8)(_rowIndex * 16);
for (int i = 0; i < 8; ++i) {
UInt8 noteNumber = (UInt8)(baseNote + i);
if (noteNumber >= 128) {
states[i] = NO;
continue;
}
BOOL active = NO;
for (int ch = 0; ch < 16; ++ch) {
if (_noteActive[ch][noteNumber]) {
active = YES;
break;
}
}
states[i] = active;
if (localActiveNote < 0 && active) {
localActiveNote = (int)noteNumber;
}
}
dynamicCount = 0;
}
else {
// Formula mode: 8 "virtual" states = first 8 notes in group (for localActiveNote)
groupOutputKeysSnapshot = [_groupOutputKeys copy];
NSUInteger groupCount = groupNotesSnapshot.count;
for (int i = 0; i < 8; ++i) {
if (i < (int)groupCount) {
int noteNumber = [groupNotesSnapshot[i] intValue];
if (noteNumber < 0 || noteNumber >= 128) {
states[i] = NO;
continue;
}
BOOL active = NO;
for (int ch = 0; ch < 16; ++ch) {
if (_noteActive[ch][noteNumber]) {
active = YES;
break;
}
}
states[i] = active;
if (localActiveNote < 0 && active) {
localActiveNote = noteNumber;
}
} else {
states[i] = NO;
}
}
// Dynamic outputs: one per note in the group
NSUInteger count = MIN(groupNotesSnapshot.count, groupOutputKeysSnapshot.count);
dynamicCount = count > 128 ? 128 : count;
for (NSUInteger idx = 0; idx < dynamicCount; ++idx) {
int noteNumber = [groupNotesSnapshot[idx] intValue];
BOOL active = NO;
if (noteNumber >= 0 && noteNumber < 128) {
for (int ch = 0; ch < 16; ++ch) {
if (_noteActive[ch][noteNumber]) {
active = YES;
break;
}
}
}
dynamicStates[idx] = active;
}
}
}
// ---- Dynamic row outputs (original Note 1..Note 8) ----
// These only exist in non-formula (row) mode.
if (!useFormulaGroup) {
[self setValue:@(states[0]) forOutputKey:@"outputNote0"];
[self setValue:@(states[1]) forOutputKey:@"outputNote1"];
[self setValue:@(states[2]) forOutputKey:@"outputNote2"];
[self setValue:@(states[3]) forOutputKey:@"outputNote3"];
[self setValue:@(states[4]) forOutputKey:@"outputNote4"];
[self setValue:@(states[5]) forOutputKey:@"outputNote5"];
[self setValue:@(states[6]) forOutputKey:@"outputNote6"];
[self setValue:@(states[7]) forOutputKey:@"outputNote7"];
}
// ---- Dynamic formula outputs ----
if (useFormulaGroup && groupOutputKeysSnapshot && dynamicCount > 0) {
NSUInteger count = MIN(dynamicCount, groupOutputKeysSnapshot.count);
for (NSUInteger idx = 0; idx < count; ++idx) {
[self setValue:@(dynamicStates[idx]) forOutputKey:groupOutputKeysSnapshot[idx]];
}
}
// ---- Shared active note bus out (dynamic port) ----
int sharedOut = sharedIn;
if (gate && exclusive && localActiveNote >= 0) {
sharedOut = localActiveNote;
}
double sharedValue = (sharedOut >= 0) ? (double)sharedOut : -1.0;
[self setValue:@(sharedValue) forOutputKey:@"outputSharedActive"];
return YES;
}
@end
// CoreMIDI read callback
static void LaunchPadMIDIControlReadProc(const MIDIPacketList *pktlist,
void *refCon,
void *srcConnRefCon)
{
LaunchPadMIDIControlPlugIn *plugin = (__bridge LaunchPadMIDIControlPlugIn *)refCon;
if (!plugin)
return;
MIDIEndpointRef source = (MIDIEndpointRef)(uintptr_t)srcConnRefCon;
[plugin processIncomingPacketList:pktlist fromSource:source];
}
// LaunchPadMIDIControlPlugInViewController.h
#import <Quartz/Quartz.h>
#import <AppKit/AppKit.h>
@interface LaunchPadMIDIControlPlugInViewController : QCPlugInViewController {
NSTextField *_formulaField;
NSButton *_useFormulaCheckbox;
}
@property(nonatomic, strong) NSTextField *formulaField;
@property(nonatomic, strong) NSButton *useFormulaCheckbox;
@end
#import "LaunchPadMIDIControlPlugInViewController.h"
@implementation LaunchPadMIDIControlPlugInViewController
@synthesize formulaField = _formulaField;
@synthesize useFormulaCheckbox = _useFormulaCheckbox;
- (id)initWithPlugIn:(QCPlugIn *)plugIn viewNibName:(NSString *)nibName
{
self = [super initWithPlugIn:plugIn viewNibName:nibName];
if (self) {
// nothing special
}
return self;
}
- (void)loadView
{
// Root view for Settings tab
NSView *root = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 360, 70)];
root.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
CGFloat padding = 8.0;
CGFloat gap = 6.0;
CGFloat fieldH = 22.0;
// LABEL: "Grid formula:"
NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect];
label.stringValue = @"Grid formula:";
label.bezeled = NO;
label.drawsBackground = NO;
label.editable = NO;
label.selectable = NO;
[label sizeToFit];
NSRect bounds = root.bounds;
NSRect labelFrame = label.frame;
labelFrame.origin.x = padding;
labelFrame.origin.y = NSHeight(bounds) - padding - NSHeight(labelFrame);
label.frame = labelFrame;
label.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
// TEXT FIELD: formula
CGFloat fieldX = NSMaxX(labelFrame) + gap;
CGFloat fieldRight = NSWidth(bounds) - padding;
if (fieldRight < fieldX + 60.0) {
fieldRight = fieldX + 60.0;
}
NSRect fieldFrame = NSMakeRect(fieldX,
NSMinY(labelFrame) - 2.0,
fieldRight - fieldX,
fieldH);
NSTextField *field = [[NSTextField alloc] initWithFrame:fieldFrame];
field.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
[field setTarget:self];
[field setAction:@selector(_formulaFieldChanged:)];
// CHECKBOX: "Use formula group"
NSButton *checkbox = [[NSButton alloc] initWithFrame:NSZeroRect];
[checkbox setButtonType:NSSwitchButton];
checkbox.title = @"Use formula group";
[checkbox sizeToFit];
NSRect cbFrame = checkbox.frame;
cbFrame.origin.x = padding;
cbFrame.origin.y = padding;
checkbox.frame = cbFrame;
checkbox.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[checkbox setTarget:self];
[checkbox setAction:@selector(_useFormulaCheckboxChanged:)];
[root addSubview:label];
[root addSubview:field];
[root addSubview:checkbox];
self.formulaField = field;
self.useFormulaCheckbox = checkbox;
self.view = root;
}
- (void)viewWillAppear
{
[super viewWillAppear];
id plugin = self.plugIn;
if (!plugin)
return;
@try {
id f = [plugin valueForKey:@"groupFormula"];
if (f && f != [NSNull null]) {
self.formulaField.stringValue = [f description];
} else {
self.formulaField.stringValue = @"";
}
NSNumber *useFormula = [plugin valueForKey:@"useFormulaGroup"];
BOOL on = useFormula ? useFormula.boolValue : NO;
self.useFormulaCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
}
@catch (__unused id ex) {
self.formulaField.stringValue = @"";
self.useFormulaCheckbox.state = NSControlStateValueOff;
}
}
#pragma mark - Actions
- (void)_formulaFieldChanged:(id)sender
{
(void)sender;
NSString *formula = [self.formulaField.stringValue
stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:formula forKey:@"groupFormula"];
} @catch (__unused id ex) {
}
}
}
- (void)_useFormulaCheckboxChanged:(id)sender
{
(void)sender;
BOOL on = (self.useFormulaCheckbox.state == NSControlStateValueOn);
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"useFormulaGroup"];
} @catch (__unused id ex) {
}
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment