841 lines
34 KiB
C#
841 lines
34 KiB
C#
using NAudio;
|
|
using NAudio.Midi;
|
|
using T3.Editor.Gui.Interaction.Midi.CommandProcessing;
|
|
using T3.Editor.Gui.Interaction.Variations;
|
|
using T3.Editor.Gui.Interaction.Variations.Model;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
|
|
namespace T3.Editor.Gui.Interaction.Midi.CompatibleDevices;
|
|
|
|
// ReSharper disable InconsistentNaming, UnusedMember.Local, CommentTypo, StringLiteralTypo
|
|
|
|
/// <summary>
|
|
/// MIDI controller implementation for the Akai APC40 Mk2.
|
|
///
|
|
/// The APC40 Mk2 uses RGB LED colors (128 color palette) compared to Mk1's 7 color states.
|
|
///
|
|
/// The device can be initialized to "Generic" mode (0x40), "Ableton" mode (0x41),
|
|
/// or "Ableton with full control" mode (0x42).
|
|
///
|
|
/// Reference: APC40Mk2_Communications_Protocol_v1.2.pdf
|
|
/// </summary>
|
|
|
|
[MidiDeviceProduct("APC40 mkII")]
|
|
public sealed class Apc40Mk2 : CompatibleMidiDevice
|
|
{
|
|
public Apc40Mk2()
|
|
{
|
|
CommandTriggerCombinations =
|
|
[
|
|
// Snapshot activate/create - press clip button to activate or create snapshot
|
|
new CommandTriggerCombination(SnapshotActions.ActivateOrCreateSnapshotAtIndex, InputModes.Default, [SceneTrigger1To40],
|
|
CommandTriggerCombination.ExecutesAt.SingleRangeButtonPressed),
|
|
|
|
// Snapshot save - hold Shift + press clip button to save
|
|
new CommandTriggerCombination(SnapshotActions.SaveSnapshotAtIndex, InputModes.Save, [SceneTrigger1To40],
|
|
CommandTriggerCombination.ExecutesAt.SingleRangeButtonPressed),
|
|
|
|
// Snapshot delete - hold Scene Launch 1 + press clip button to delete
|
|
new CommandTriggerCombination(SnapshotActions.RemoveSnapshotAtIndex, InputModes.Delete, [SceneTrigger1To40],
|
|
CommandTriggerCombination.ExecutesAt.SingleRangeButtonPressed),
|
|
|
|
// Blend between two snapshots - press two clip buttons simultaneously
|
|
new CommandTriggerCombination(BlendActions.StartBlendingSnapshots, InputModes.Default, [SceneTrigger1To40],
|
|
CommandTriggerCombination.ExecutesAt.AllCombinedButtonsReleased),
|
|
|
|
// Start blend towards - hold Scene Launch 2 + press clip button to start blend
|
|
new CommandTriggerCombination(BlendActions.StartBlendingTowardsSnapshot, InputModes.BlendTo, [SceneTrigger1To40],
|
|
CommandTriggerCombination.ExecutesAt.SingleRangeButtonPressed),
|
|
|
|
// Stop blending - press Scene Launch 2 to stop blend operation
|
|
new CommandTriggerCombination(BlendActions.StopBlendingTowards, InputModes.Default, [SceneLaunch2],
|
|
CommandTriggerCombination.ExecutesAt.SingleActionButtonPressed),
|
|
|
|
// Stop blending - press Stop All Clips to stop blend operation
|
|
new CommandTriggerCombination(BlendActions.StopBlendingTowards, InputModes.Default, [ClipStopAll],
|
|
CommandTriggerCombination.ExecutesAt.SingleActionButtonPressed),
|
|
|
|
// Update blend progress with crossfader
|
|
new CommandTriggerCombination(BlendActions.UpdateBlendingTowardsProgress, InputModes.Default, [AbFader],
|
|
CommandTriggerCombination.ExecutesAt.ControllerChange),
|
|
|
|
// Update blend progress with Master Fader
|
|
new CommandTriggerCombination(BlendActions.UpdateBlendingTowardsProgress, InputModes.Default, [MasterFader],
|
|
CommandTriggerCombination.ExecutesAt.ControllerChange),
|
|
|
|
// Update blend values with channel faders
|
|
new CommandTriggerCombination(BlendActions.UpdateBlendValues, InputModes.Default, [Fader1To8],
|
|
CommandTriggerCombination.ExecutesAt.ControllerChange),
|
|
|
|
// Mode switching - Shift + Record/Arm 1/2/3 to switch between modes
|
|
new CommandTriggerCombination(HandleModeSwitch, InputModes.Save, [RecordArmButtons],
|
|
CommandTriggerCombination.ExecutesAt.SingleRangeButtonPressed)
|
|
];
|
|
|
|
ModeButtons =
|
|
[
|
|
new ModeButton(SceneLaunch1, InputModes.Delete),
|
|
new ModeButton(SceneLaunch2, InputModes.BlendTo),
|
|
new ModeButton(Shift, InputModes.Save)
|
|
];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles mode switching based on which Record/Arm button was pressed (with Shift held).
|
|
/// Index 0 = Generic passthrough (0x40), Index 1 = Ableton passthrough (0x41), Index 2 = Ableton control (0x42)
|
|
/// </summary>
|
|
private void HandleModeSwitch(int index)
|
|
{
|
|
switch (index)
|
|
{
|
|
case 0: // Record/Arm 1 - Generic passthrough mode (0x40)
|
|
LogMidiDebug("APC40 Mk2: Setting GENERIC PASSTHROUGH mode (0x40)");
|
|
_useGenericMode = true;
|
|
SendModeInitSysEx();
|
|
SetControlMode(false);
|
|
break;
|
|
|
|
case 1: // Record/Arm 2 - Ableton passthrough mode (0x41)
|
|
LogMidiDebug("APC40 Mk2: Setting ABLETON PASSTHROUGH mode (0x41)");
|
|
_useGenericMode = false;
|
|
SendModeInitSysEx();
|
|
SetControlMode(false);
|
|
break;
|
|
|
|
case 2: // Record/Arm 3 - Ableton control mode (0x41)
|
|
LogMidiDebug("APC40 Mk2: Setting ABLETON CONTROL mode (0x41)");
|
|
_useGenericMode = false;
|
|
SendModeInitSysEx();
|
|
SetControlMode(true);
|
|
break;
|
|
|
|
default:
|
|
LogMidiDebug($"APC40 Mk2: Ignoring mode switch for index {index}");
|
|
return; // Don't clear signals for invalid index
|
|
}
|
|
|
|
// Clear button signals after mode switch to prevent stale signals from
|
|
// blocking subsequent mode switches. The button mapping changes between
|
|
// Generic and Ableton modes, so old signals may not match new button IDs.
|
|
ClearButtonSignals();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the SysEx initialization message to set the APC40 Mk2 mode.
|
|
/// Uses 0x40 (Generic), 0x41 (Ableton), or 0x42 (Ableton with full control).
|
|
/// </summary>
|
|
private void SendModeInitSysEx()
|
|
{
|
|
if (MidiOutConnection == null)
|
|
return;
|
|
|
|
// Clear all LEDs BEFORE mode switch
|
|
// This clears using BOTH mode mappings to ensure all LEDs are off
|
|
ClearAllLedsRaw();
|
|
|
|
var modeIdentifier = _useGenericMode ? (byte)0x40 : (byte)0x41;
|
|
LogMidiDebug($"APC40 Mk2: Sending mode SysEx (0x{modeIdentifier:X2})...");
|
|
|
|
var buffer = new byte[]
|
|
{
|
|
0xF0, // MIDI exclusive start
|
|
0x47, // Manufacturers ID Byte (Akai)
|
|
0x7F, // System Exclusive Device ID (0x7F = any)
|
|
0x29, // Product model ID (APC40 Mk2)
|
|
0x60, // Message type identifier (Introduction message)
|
|
0x00, // Number of data bytes to follow (most significant)
|
|
0x04, // Number of data bytes to follow (least significant) = 4 bytes
|
|
modeIdentifier, // Application/Configuration Identifier
|
|
0x01, // PC application Software version major
|
|
0x01, // PC application Software version minor
|
|
0x01, // PC application Software bug-fix level
|
|
0xF7 // MIDI exclusive end
|
|
};
|
|
|
|
try
|
|
{
|
|
MidiOutConnection.SendBuffer(buffer);
|
|
_initialized = true;
|
|
LogMidiDebug($"APC40 Mk2: Mode switch complete (0x{modeIdentifier:X2})");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Warning($"APC40 Mk2: Failed to send mode SysEx: {e.Message}");
|
|
}
|
|
|
|
// Only update the mode indicator LED (Record/Arm 1, 2, or 3)
|
|
// Don't update any other LEDs - let the normal update cycle handle that
|
|
UpdateRecordArmModeLeds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all LEDs on the device by sending direct MIDI messages.
|
|
/// Bypasses cache and clears using BOTH Generic and Ableton mode mappings.
|
|
/// </summary>
|
|
private void ClearAllLedsRaw()
|
|
{
|
|
if (MidiOutConnection == null)
|
|
return;
|
|
|
|
Array.Fill(CacheControllerColors, -1);
|
|
|
|
// Clear clip grid - Generic mode (Notes 0-39 on Channel 1)
|
|
foreach (var note in GenericClipGridNotes.Indices())
|
|
SendNoteRaw(MidiChannels1To8.StartIndex, note, 0);
|
|
|
|
// Clear clip grid - Ableton mode (Notes 0-4 on Channels 1-8)
|
|
foreach (var note in AbletonClipGridNotes.Indices())
|
|
foreach (var ch in MidiChannels1To8.Indices())
|
|
SendNoteRaw(ch, note, 0);
|
|
|
|
// Clear scene launch LEDs
|
|
foreach (var note in SceneLaunchNotes.Indices())
|
|
SendNoteRaw(MidiChannels1To8.StartIndex, note, 0);
|
|
|
|
// Clear Record/Arm LEDs - Ableton mode (Note 0 on Channels 1-8)
|
|
foreach (var ch in MidiChannels1To8.Indices())
|
|
SendNoteRaw(ch, AbletonRecordArmNote, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a raw MIDI note, bypassing cache. Used for LED clearing.
|
|
/// </summary>
|
|
private void SendNoteRaw(int channel, int note, int velocity)
|
|
{
|
|
try
|
|
{
|
|
var evt = new NoteOnEvent(0, channel, note, velocity, 0);
|
|
MidiOutConnection?.Send(evt.GetAsShortMessage());
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Warning($"Failed to send MIDI note (ch={channel}, note={note}): {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when control mode changes. Reinitialize device when entering control mode.
|
|
/// </summary>
|
|
protected override void OnControlModeChanged(bool isNowInControlMode)
|
|
{
|
|
base.OnControlModeChanged(isNowInControlMode);
|
|
|
|
if (isNowInControlMode)
|
|
{
|
|
_initialized = false;
|
|
}
|
|
|
|
UpdateRecordArmModeLeds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates Record/Arm 1, 2, and 3 LEDs to show current mode.
|
|
/// Green = active mode, Off = inactive
|
|
/// </summary>
|
|
private void UpdateRecordArmModeLeds()
|
|
{
|
|
if (MidiOutConnection == null)
|
|
return;
|
|
|
|
var modeLedColors = new[]
|
|
{
|
|
_useGenericMode ? Apc40Mk2Colors.Green : Apc40Mk2Colors.Off, // Generic passthrough
|
|
!_useGenericMode && !IsInControlMode ? Apc40Mk2Colors.Green : Apc40Mk2Colors.Off, // Ableton passthrough
|
|
!_useGenericMode && IsInControlMode ? Apc40Mk2Colors.Green : Apc40Mk2Colors.Off // Ableton control
|
|
};
|
|
|
|
for (var i = 0; i < modeLedColors.Length; i++)
|
|
{
|
|
var channel = _useGenericMode ? MidiChannels1To8.StartIndex : MidiChannels1To8.StartIndex + i;
|
|
var note = _useGenericMode ? i : AbletonRecordArmNote;
|
|
SendNoteRaw(channel, note, (int)modeLedColors[i]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all LEDs when in passthrough mode. Shows mode highlighting when a modifier is held.
|
|
/// </summary>
|
|
protected override void ClearDeviceLeds()
|
|
{
|
|
if (MidiOutConnection == null)
|
|
return;
|
|
|
|
_updateCount++;
|
|
|
|
for (var i = 0; i < ClipGridSize; i++)
|
|
{
|
|
if (ActiveMode != InputModes.Default)
|
|
CacheControllerColors[i] = -1;
|
|
|
|
SendLedState(MidiOutConnection, i, AddModeHighlight(i, LedState.Off));
|
|
}
|
|
|
|
foreach (var i in SceneLaunchNotes.Indices())
|
|
{
|
|
CacheControllerColors[i] = -1;
|
|
SendLedState(MidiOutConnection, i, LedState.Off);
|
|
}
|
|
}
|
|
|
|
protected override void UpdateVariationVisualization()
|
|
{
|
|
_updateCount++;
|
|
if (!_initialized)
|
|
SendModeInitSysEx();
|
|
|
|
// Update clip launch button LEDs (5x8 grid)
|
|
UpdateRangeLeds(SceneTrigger1To40, mappedIndex =>
|
|
{
|
|
if (!SymbolVariationPool.TryGetSnapshot(mappedIndex, out var v))
|
|
return AddModeHighlight(mappedIndex, LedState.Off).ToCacheKey();
|
|
|
|
var isBlendTarget = BlendActions.BlendTowardsIndex == mappedIndex;
|
|
var state = v.State switch
|
|
{
|
|
Variation.States.Active => new LedState(Apc40Mk2Colors.Red),
|
|
Variation.States.Modified => new LedState(Apc40Mk2Colors.Yellow),
|
|
Variation.States.IsBlended => new LedState(Apc40Mk2Colors.Orange, LedBehavior.Pulse1_16),
|
|
Variation.States.InActive => isBlendTarget
|
|
? new LedState(Apc40Mk2Colors.Orange, LedBehavior.Pulse1_16)
|
|
: new LedState(Apc40Mk2Colors.Green),
|
|
_ => LedState.Off
|
|
};
|
|
|
|
return AddModeHighlight(mappedIndex, state).ToCacheKey();
|
|
});
|
|
|
|
// Update scene launch LEDs only in Ableton control mode
|
|
if (IsInControlMode && !_useGenericMode)
|
|
UpdateSceneLaunchLeds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents an LED state with color and behavior.
|
|
/// </summary>
|
|
private readonly record struct LedState(Apc40Mk2Colors Color, LedBehavior Behavior = LedBehavior.Solid)
|
|
{
|
|
public static readonly LedState Off = new(Apc40Mk2Colors.Off);
|
|
|
|
/// <summary>
|
|
/// Converts to a cache key for change detection.
|
|
/// </summary>
|
|
public int ToCacheKey() => ((int)Behavior << 8) | (int)Color;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the scene launch button LEDs to indicate current input mode.
|
|
/// </summary>
|
|
private void UpdateSceneLaunchLeds()
|
|
{
|
|
if (MidiOutConnection == null)
|
|
return;
|
|
|
|
var states = new[]
|
|
{
|
|
ActiveMode == InputModes.Delete ? new LedState(Apc40Mk2Colors.Red, LedBehavior.Blink1_4) : new LedState(Apc40Mk2Colors.Red),
|
|
ActiveMode == InputModes.BlendTo ? new LedState(Apc40Mk2Colors.Orange, LedBehavior.Pulse1_16) : new LedState(Apc40Mk2Colors.Orange),
|
|
LedState.Off,
|
|
LedState.Off,
|
|
LedState.Off
|
|
};
|
|
|
|
for (var i = 0; i < states.Length; i++)
|
|
SendLedState(MidiOutConnection, SceneLaunchNotes.StartIndex + i, states[i]);
|
|
}
|
|
|
|
private LedState AddModeHighlight(int index, LedState state)
|
|
{
|
|
var indicatedStatus = (_updateCount + index / AbletonClipGridColumns) % 30 < 4;
|
|
if (!indicatedStatus)
|
|
return state;
|
|
|
|
return ActiveMode switch
|
|
{
|
|
InputModes.Save => new LedState(Apc40Mk2Colors.Green, LedBehavior.Pulse1_8),
|
|
InputModes.BlendTo => new LedState(Apc40Mk2Colors.Orange, LedBehavior.Pulse1_8),
|
|
InputModes.Delete => new LedState(Apc40Mk2Colors.Red, LedBehavior.Pulse1_8),
|
|
_ => state
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// APC40 Mk2 LED behavior types.
|
|
/// For non-grid buttons, behavior is controlled via MIDI channel 1-8.
|
|
/// For clip grid in Ableton mode, behavior uses channels 9-15:
|
|
/// - Channel 1-8: Solid (track columns 1-8)
|
|
/// - Channel 9: Pulsing 1/16
|
|
/// - Channel 10: Pulsing 1/8
|
|
/// - Channel 11: Blinking 1/24
|
|
/// - Channel 12: Blinking 1/16
|
|
/// - Channel 13: Blinking 1/8
|
|
/// - Channel 14: Blinking 1/4
|
|
/// - Channel 15: Blinking 1/2
|
|
/// </summary>
|
|
private enum LedBehavior
|
|
{
|
|
Solid = 0,
|
|
Pulse1_16 = 1,
|
|
Pulse1_8 = 2,
|
|
Blink1_24 = 3,
|
|
Blink1_16 = 4,
|
|
Blink1_8 = 5,
|
|
Blink1_4 = 6,
|
|
Blink1_2 = 7,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an LED state to a button on the APC40 Mk2.
|
|
///
|
|
/// Generic Mode: Clip grid uses Notes 0-39 on Channel 1 (solid only).
|
|
/// Ableton Mode: Clip grid uses Notes 0-4, with channels controlling behavior:
|
|
/// - Channel 1-8: Solid for track columns 1-8
|
|
/// - Channel 9-15: Behaviors (Pulse/Blink) using note to encode row+column
|
|
/// </summary>
|
|
private void SendLedState(MidiOut midiOut, int controlIndex, LedState state)
|
|
{
|
|
if (midiOut == null || controlIndex < 0 || controlIndex >= CacheControllerColors.Length)
|
|
return;
|
|
|
|
var cacheKey = state.ToCacheKey();
|
|
if (CacheControllerColors[controlIndex] == cacheKey)
|
|
return;
|
|
|
|
int channel;
|
|
int noteNumber;
|
|
|
|
if (controlIndex < ClipGridSize)
|
|
{
|
|
if (_useGenericMode)
|
|
{
|
|
// Generic mode: Notes 0-39 on Channel 1, solid only
|
|
channel = MidiChannels1To8.StartIndex;
|
|
noteNumber = controlIndex;
|
|
}
|
|
else
|
|
{
|
|
// Ableton mode
|
|
int row = controlIndex / AbletonClipGridColumns;
|
|
int col = controlIndex % AbletonClipGridColumns;
|
|
|
|
if (state.Behavior == LedBehavior.Solid)
|
|
{
|
|
// Solid: Channel = track column (1-8), Note = row (0-4)
|
|
channel = col + MidiChannels1To8.StartIndex;
|
|
noteNumber = row + AbletonClipGridNotes.StartIndex;
|
|
}
|
|
else
|
|
{
|
|
// Behaviors: Channel 9-15, Note = grid index (0-39)
|
|
channel = 8 + (int)state.Behavior;
|
|
noteNumber = controlIndex;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-grid buttons: channel 1-8 for behaviors
|
|
channel = MidiChannels1To8.StartIndex + (int)state.Behavior;
|
|
noteNumber = controlIndex;
|
|
}
|
|
|
|
var noteOnEvent = new NoteOnEvent(0, channel, noteNumber, (int)state.Color, 0);
|
|
try
|
|
{
|
|
midiOut.Send(noteOnEvent.GetAsShortMessage());
|
|
}
|
|
catch (MmException e)
|
|
{
|
|
Log.Warning($"Failed setting LED state for index {controlIndex}: {e.Message}");
|
|
}
|
|
|
|
CacheControllerColors[controlIndex] = cacheKey;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override required by base class - delegates to SendLedState.
|
|
/// </summary>
|
|
protected override void SendColor(MidiOut midiOut, int apcControlIndex, int colorCode)
|
|
{
|
|
// Decode legacy color code format
|
|
var color = (Apc40Mk2Colors)(colorCode & 0xFF);
|
|
var behavior = (LedBehavior)((colorCode >> 8) & 0xFF);
|
|
SendLedState(midiOut, apcControlIndex, new LedState(color, behavior));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts APC40 Mk2 MIDI channel/note to button index.
|
|
/// Generic Mode: Clip grid = Notes 0-39 on Ch1, Track Select = Notes 58-65 on Ch1.
|
|
/// Ableton Mode: Clip grid = Notes 0-4 on Ch1-8, Track Select = Note 51 on Ch1-8.
|
|
/// </summary>
|
|
protected override int ConvertNoteToButtonId(int channel, int noteNumber)
|
|
{
|
|
// Shift button - same in both modes
|
|
if (noteNumber == ShiftButtonNote && channel == MidiChannels1To8.StartIndex)
|
|
{
|
|
LogMidiDebug($"ConvertNoteToButtonId: Shift button Note={noteNumber}, Channel={channel} -> ButtonId={ShiftButtonNote}");
|
|
return ShiftButtonNote;
|
|
}
|
|
|
|
// Clip Launch Grid
|
|
if (_useGenericMode)
|
|
{
|
|
if (GenericClipGridNotes.IncludesButtonIndex(noteNumber) && channel == MidiChannels1To8.StartIndex)
|
|
{
|
|
LogMidiDebug($"ConvertNoteToButtonId [Generic]: Clip grid Note={noteNumber}, Channel={channel} -> ButtonId={noteNumber}");
|
|
return noteNumber;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (AbletonClipGridNotes.IncludesButtonIndex(noteNumber) && MidiChannels1To8.IncludesButtonIndex(channel))
|
|
{
|
|
var row = AbletonClipGridNotes.GetMappedIndex(noteNumber);
|
|
var col = MidiChannels1To8.GetMappedIndex(channel);
|
|
var buttonId = row * AbletonClipGridColumns + col;
|
|
LogMidiDebug($"ConvertNoteToButtonId [Ableton]: Clip grid Note={noteNumber}, Channel={channel} -> row={row}, col={col}, ButtonId={buttonId}");
|
|
return buttonId;
|
|
}
|
|
}
|
|
|
|
// Record/Arm buttons for mode switching
|
|
if (noteNumber == AbletonRecordArmNote && MidiChannels1To8.IncludesButtonIndex(channel))
|
|
{
|
|
var buttonId = RecordArmBaseId + MidiChannels1To8.GetMappedIndex(channel);
|
|
LogMidiDebug($"ConvertNoteToButtonId: Record/Arm (Ableton mapping) Note={noteNumber}, Channel={channel} -> ButtonId={buttonId}");
|
|
return buttonId;
|
|
}
|
|
|
|
// Generic mode additional Record/Arm notes
|
|
if (_useGenericMode && channel == MidiChannels1To8.StartIndex
|
|
&& GenericRecordArmNotes.IncludesButtonIndex(noteNumber))
|
|
{
|
|
var buttonId = RecordArmBaseId + GenericRecordArmNotes.GetMappedIndex(noteNumber);
|
|
LogMidiDebug($"ConvertNoteToButtonId: Record/Arm (Generic mapping) Note={noteNumber}, Channel={channel} -> ButtonId={buttonId}");
|
|
return buttonId;
|
|
}
|
|
|
|
// Track Select buttons
|
|
if (_useGenericMode)
|
|
{
|
|
if (channel == MidiChannels1To8.StartIndex && GenericTrackSelectNotes.IncludesButtonIndex(noteNumber))
|
|
{
|
|
var buttonId = TrackSelectBaseId + GenericTrackSelectNotes.GetMappedIndex(noteNumber);
|
|
LogMidiDebug($"ConvertNoteToButtonId: Track Select (Generic mapping) Note={noteNumber}, Channel={channel} -> ButtonId={buttonId}");
|
|
return buttonId;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (noteNumber == AbletonTrackSelectNote && MidiChannels1To8.IncludesButtonIndex(channel))
|
|
{
|
|
var buttonId = TrackSelectBaseId + MidiChannels1To8.GetMappedIndex(channel);
|
|
LogMidiDebug($"ConvertNoteToButtonId: Track Select (Ableton mapping) Note={noteNumber}, Channel={channel} -> ButtonId={buttonId}");
|
|
return buttonId;
|
|
}
|
|
}
|
|
|
|
return noteNumber;
|
|
}
|
|
|
|
private const int TrackSelectBaseId = 1000;
|
|
|
|
#region MIDI Note/Channel Mapping Constants
|
|
|
|
private const int ShiftButtonNote = 98;
|
|
private const int ClipGridSize = 40;
|
|
|
|
private static readonly ButtonRange MidiChannels1To8 = new(1, 8);
|
|
|
|
// Generic Mode (0x40) MIDI Mappings
|
|
private static readonly ButtonRange GenericClipGridNotes = new(0, 39);
|
|
private static readonly ButtonRange GenericRecordArmNotes = new(48, 55);
|
|
private static readonly ButtonRange GenericTrackSelectNotes = new(58, 65);
|
|
|
|
// Ableton Live Mode (0x41/0x42) MIDI Mappings
|
|
// Clip Launch Grid: Notes 0-4 on Channels 1-8 (5 rows x 8 columns)
|
|
private static readonly ButtonRange AbletonClipGridNotes = new(0, 4);
|
|
private const int AbletonClipGridColumns = 8;
|
|
private const int AbletonRecordArmNote = 48;
|
|
private const int AbletonTrackSelectNote = 51;
|
|
|
|
private static readonly ButtonRange SceneLaunchNotes = new(82, 86);
|
|
|
|
#endregion
|
|
|
|
private int _updateCount;
|
|
|
|
// Clip Launch Button Grid
|
|
private static readonly ButtonRange SceneTrigger1To40 = new(0, 39);
|
|
|
|
// Scene Launch buttons
|
|
private static readonly ButtonRange SceneLaunch1To5 = new(82, 86);
|
|
private static readonly ButtonRange SceneLaunch1 = new(82);
|
|
private static readonly ButtonRange SceneLaunch2 = new(83);
|
|
private static readonly ButtonRange SceneLaunch3 = new(84);
|
|
private static readonly ButtonRange SceneLaunch4 = new(85);
|
|
private static readonly ButtonRange SceneLaunch5 = new(86);
|
|
|
|
// Track control buttons
|
|
private static readonly ButtonRange ClipStopButtons1To8 = new(52, 59);
|
|
private static readonly ButtonRange ClipABButtons1To8 = new(66, 73);
|
|
|
|
// Record/Arm buttons (mapped to button IDs 2000-2007)
|
|
private const int RecordArmBaseId = 2000;
|
|
private static readonly ButtonRange RecordArmButtons = new(RecordArmBaseId, RecordArmBaseId + 7);
|
|
|
|
// Stop all clips button
|
|
private static readonly ButtonRange ClipStopAll = new(81);
|
|
|
|
// Navigation buttons
|
|
private static readonly ButtonRange BankSelectTop = new(94);
|
|
private static readonly ButtonRange BankSelectRight = new(96);
|
|
private static readonly ButtonRange BankSelectBottom = new(95);
|
|
private static readonly ButtonRange BankSelectLeft = new(97);
|
|
private static readonly ButtonRange Shift = new(98);
|
|
private static readonly ButtonRange Bank = new(103);
|
|
|
|
// Device control buttons
|
|
private static readonly ButtonRange DetailView = new(65);
|
|
private static readonly ButtonRange ClipDevView = new(64);
|
|
private static readonly ButtonRange DevLock = new(63);
|
|
private static readonly ButtonRange DevOnOff = new(62);
|
|
private static readonly ButtonRange BankRightArrow = new(61);
|
|
private static readonly ButtonRange BankLeftArrow = new(60);
|
|
private static readonly ButtonRange DeviceRightArrow = new(59);
|
|
private static readonly ButtonRange DeviceLeftArrow = new(58);
|
|
|
|
// Transport and tempo buttons
|
|
private static readonly ButtonRange NudgePlus = new(101);
|
|
private static readonly ButtonRange NudgeMinus = new(100);
|
|
private static readonly ButtonRange User = new(89);
|
|
private static readonly ButtonRange TapTempo = new(99);
|
|
private static readonly ButtonRange Metronome = new(90);
|
|
private static readonly ButtonRange Sends = new(88);
|
|
private static readonly ButtonRange Pan = new(87);
|
|
private static readonly ButtonRange Play = new(91);
|
|
private static readonly ButtonRange Stop = new(92);
|
|
private static readonly ButtonRange Record = new(93);
|
|
private static readonly ButtonRange Session = new(102);
|
|
|
|
// Faders and knobs (Control Change messages)
|
|
private static readonly ButtonRange Fader1To8 = new(7, 7);
|
|
private static readonly ButtonRange RightPerBankKnobs = new(16, 23);
|
|
private static readonly ButtonRange MasterFader = new(14);
|
|
private static readonly ButtonRange AbFader = new(15);
|
|
private static readonly ButtonRange TopKnobs1To8 = new(48, 55);
|
|
private static readonly ButtonRange CueLevelKnob = new(47);
|
|
private static readonly ButtonRange TempoKnob = new(13);
|
|
|
|
/// <summary>
|
|
/// APC40 Mk2 RGB LED color values (128 color palette).
|
|
/// Reference: APC40Mk2_Communications_Protocol_v1.2.pdf
|
|
/// Each row of 4 represents the same hue at different brightness levels:
|
|
/// [Bright/Light] [Normal] [Dark] [Dim]
|
|
/// </summary>
|
|
private enum Apc40Mk2Colors
|
|
{
|
|
// Row 0: Grayscale
|
|
Off = 0, // #000000
|
|
DarkGray = 1, // #1E1E1E
|
|
Gray = 2, // #7F7F7F
|
|
White = 3, // #FFFFFF
|
|
|
|
// Row 1: Red
|
|
LightRed = 4, // #FF4C4C
|
|
Red = 5, // #FF0000
|
|
DarkRed = 6, // #590000
|
|
DimRed = 7, // #190000
|
|
|
|
// Row 2: Orange
|
|
LightOrange = 8, // #FFBD6C
|
|
Orange = 9, // #FF5400
|
|
DarkOrange = 10, // #591D00
|
|
DimOrange = 11, // #271B00
|
|
|
|
// Row 3: Yellow
|
|
LightYellow = 12, // #FFFF4C
|
|
Yellow = 13, // #FFFF00
|
|
DarkYellow = 14, // #595900
|
|
DimYellow = 15, // #191900
|
|
|
|
// Row 4: Chartreuse (Yellow-Green)
|
|
LightChartreuse = 16, // #88FF4C
|
|
Chartreuse = 17, // #54FF00
|
|
DarkChartreuse = 18, // #1D5900
|
|
DimChartreuse = 19, // #142B00
|
|
|
|
// Row 5: Green
|
|
LightGreen = 20, // #4CFF4C
|
|
Green = 21, // #00FF00
|
|
DarkGreen = 22, // #005900
|
|
DimGreen = 23, // #001900
|
|
|
|
// Row 6: Spring Green (Green with hint of Cyan)
|
|
LightSpringGreen = 24, // #4CFF5E
|
|
SpringGreen = 25, // #00FF19
|
|
DarkSpringGreen = 26, // #00590D
|
|
DimSpringGreen = 27, // #001902
|
|
|
|
// Row 7: Turquoise Green
|
|
LightTurquoiseGreen = 28, // #4CFF88
|
|
TurquoiseGreen = 29, // #00FF55
|
|
DarkTurquoiseGreen = 30, // #00591D
|
|
DimTurquoiseGreen = 31, // #001F12
|
|
|
|
// Row 8: Cyan-Green (Seafoam)
|
|
LightSeafoam = 32, // #4CFFB7
|
|
Seafoam = 33, // #00FF99
|
|
DarkSeafoam = 34, // #005935
|
|
DimSeafoam = 35, // #001912
|
|
|
|
// Row 9: Aquamarine
|
|
LightAquamarine = 36, // #4CFFD2
|
|
Aquamarine = 37, // #00FFCC
|
|
DarkAquamarine = 38, // #005949
|
|
DimAquamarine = 39, // #001919
|
|
|
|
// Row 10: Cyan
|
|
LightCyan = 40, // #4CFFFF
|
|
Cyan = 41, // #00FFFF
|
|
DarkCyan = 42, // #005959
|
|
DimCyan = 43, // #001919
|
|
|
|
// Row 11: Sky Blue
|
|
LightSkyBlue = 44, // #4CC3FF
|
|
SkyBlue = 45, // #0099FF
|
|
DarkSkyBlue = 46, // #004152
|
|
DimSkyBlue = 47, // #001019
|
|
|
|
// Row 12: Azure Blue
|
|
LightAzure = 48, // #4C88FF
|
|
Azure = 49, // #0055FF
|
|
DarkAzure = 50, // #001D59
|
|
DimAzure = 51, // #000819
|
|
|
|
// Row 13: Blue
|
|
LightBlue = 52, // #4C4CFF
|
|
Blue = 53, // #0000FF
|
|
DarkBlue = 54, // #000059
|
|
DimBlue = 55, // #000019
|
|
|
|
// Row 14: Violet
|
|
LightViolet = 56, // #874CFF
|
|
Violet = 57, // #5400FF
|
|
DarkViolet = 58, // #190059
|
|
DimViolet = 59, // #0F0030
|
|
|
|
// Row 15: Purple
|
|
LightPurple = 60, // #BC4CFF
|
|
Purple = 61, // #9000FF
|
|
DarkPurple = 62, // #320059
|
|
DimPurple = 63, // #190026
|
|
|
|
// Row 16: Magenta
|
|
LightMagenta = 64, // #FF4CFF
|
|
Magenta = 65, // #FF00FF
|
|
DarkMagenta = 66, // #590059
|
|
DimMagenta = 67, // #190019
|
|
|
|
// Row 17: Pink (Magenta-Red)
|
|
LightPink = 68, // #FF4C87
|
|
Pink = 69, // #FF0054
|
|
DarkPink = 70, // #59001D
|
|
DimPink = 71, // #220013
|
|
|
|
// Row 18: Warm accent colors
|
|
OrangeRed = 72, // #FF1500
|
|
BurntOrange = 73, // #993500
|
|
Olive = 74, // #795100
|
|
DarkOlive = 75, // #436400
|
|
|
|
// Row 19: Cool accent colors
|
|
ForestGreen = 76, // #033900
|
|
Teal = 77, // #005735
|
|
DeepTeal = 78, // #00547F
|
|
Navy = 79, // #0000FF
|
|
|
|
// Row 20: Deep cool tones
|
|
DarkSlateBlue = 80, // #00454F
|
|
Indigo = 81, // #2500CC
|
|
DeepViolet = 82, // #7F00FF
|
|
Fuchsia = 83, // #B4177F
|
|
|
|
// Row 21: Warm earth tones
|
|
Maroon = 84, // #902100
|
|
Rust = 85, // #8C3400
|
|
Amber = 86, // #E97400
|
|
Gold = 87, // #B3B300
|
|
|
|
// Row 22: Yellow-Green tones
|
|
YellowGreen = 88, // #7FB300
|
|
LimeGreen = 89, // #4B9900
|
|
GrassGreen = 90, // #247F00
|
|
DarkForestGreen = 91, // #0F5C00
|
|
|
|
// Row 23: Teal-Blue tones
|
|
SeaGreen = 92, // #007F46
|
|
DarkCyanGreen = 93, // #007F7F
|
|
SteelBlue = 94, // #004CCC
|
|
RoyalBlue = 95, // #1841FF
|
|
|
|
// Row 24: Purple-Pink pastels
|
|
SlateBlue = 96, // #6050FF
|
|
Orchid = 97, // #BC7FE1
|
|
LightOrchid = 98, // #D8A6CF
|
|
PalePink = 99, // #E6AAAC
|
|
|
|
// Row 25: Warm pastels
|
|
Beige = 100, // #D0C9A6
|
|
PaleGold = 101, // #CBAB4F
|
|
Khaki = 102, // #93934F
|
|
OliveGreen = 103, // #6B6B4B
|
|
|
|
// Row 26: Cool pastels
|
|
SageGreen = 104, // #4B6B4F
|
|
GrayTeal = 105, // #4B7F87
|
|
GrayBlue = 106, // #6B6BD1
|
|
Periwinkle = 107, // #9FA1ED
|
|
|
|
// Row 27: Muted pastels
|
|
LavenderGray = 108, // #97A4E1
|
|
MutedViolet = 109, // #8181C9
|
|
MutedPink = 110, // #A797A3
|
|
DustyRose = 111, // #BAA4AC
|
|
|
|
// Row 28: Earth tones
|
|
Tan = 112, // #968476
|
|
DarkTan = 113, // #9A8D64
|
|
DarkKhaki = 114, // #97974C
|
|
MutedOlive = 115, // #737263
|
|
|
|
// Row 29: Gray tones
|
|
GrayGreen = 116, // #636C58
|
|
SlateGray = 117, // #636C7F
|
|
CoolGray = 118, // #6B6BAB
|
|
LightSlateGray = 119, // #8C8CAF
|
|
|
|
// Row 30: Bright accents
|
|
BrightRed = 120, // #FF0000
|
|
BrightOrange = 121, // #EB7F00
|
|
BrightYellow = 122, // #CFBF00
|
|
BrightChartreuse = 123, // #8EE100
|
|
|
|
// Row 31: Bright accents (continued)
|
|
BrightGreen = 124, // #00E133
|
|
BrightTurquoise = 125, // #00D2A8
|
|
BrightCyan = 126, // #00A3E0
|
|
BrightSkyBlue = 127, // #007FFF
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs a debug message if MIDI debug logging is enabled in settings.
|
|
/// </summary>
|
|
private static void LogMidiDebug(string message)
|
|
{
|
|
if (UserSettings.Config.EnableMidiDebugLogging)
|
|
Log.Debug(message);
|
|
}
|
|
|
|
private bool _initialized;
|
|
private bool _useGenericMode = true;
|
|
} |