Vendor helgoboss/helgobox (ReaLearn) as basis for custom UI fork

Stripped upstream git history; starting point for replacing the native
SWELL/Win32 mapping UI with something more suited to bulk editing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-15 17:38:29 -04:00
parent 583ed77a67
commit e58f06d9fa
2232 changed files with 685575 additions and 1 deletions
@@ -0,0 +1 @@
../../api/luau/.stylua.toml
@@ -0,0 +1 @@
../../api/luau/.vscode
@@ -0,0 +1 @@
../../api/luau/README.md
@@ -0,0 +1,34 @@
--- name: APC Key 25 mk2 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Akai
--- device_name: APC Key 25 mk2
--- description: |
--- This main preset turns the APC Key 25 mk2 into a capable device for controlling Playtime. Hold Shift
--- to access advanced functions.
--- used_schemes: [akai/apc-key-25-mk2]
--- required_features: [playtime]
--!strict
local commons = require("akai/apc-lib/playtime-commons")
return commons.create_compartment {
use_column_stop_buttons = true,
stop_column_if_slot_empty = true,
mute_track_button_id = "row3/play",
arm_track_button_id = "row4/play",
column_count = 8,
row_count = 5,
up_button_id = "col1/stop",
down_button_id = "col2/stop",
left_button_id = "col3/stop",
right_button_id = "col4/stop",
volume_button_id = "col5/stop",
pan_button_id = "col6/stop",
send_button_id = "col7/stop",
device_button_id = "col8/stop",
stop_all_clips_button_id = "stop-all-clips",
stop_all_clips_need_shift = false,
}
@@ -0,0 +1,33 @@
--- name: APC Key 25 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Akai
--- device_name: APC Key 25
--- description: |
--- This main preset turns the APC Key 25 mk1 into a capable device for controlling Playtime. Hold Shift or Sustain
--- to access advanced functions.
--- used_schemes: [akai/apc-key-25]
--- required_features: [playtime]
--!strict
local commons = require("akai/apc-lib/playtime-commons")
return commons.create_compartment {
use_column_stop_buttons = true,
stop_column_if_slot_empty = true,
mute_track_button_id = "row4/play",
arm_track_button_id = "row3/play",
column_count = 8,
row_count = 5,
up_button_id = "col1/stop",
down_button_id = "col2/stop",
left_button_id = "col3/stop",
right_button_id = "col4/stop",
volume_button_id = "col5/stop",
pan_button_id = "col6/stop",
send_button_id = "col7/stop",
device_button_id = "col8/stop",
stop_all_clips_button_id = "stop-all-clips",
stop_all_clips_need_shift = false,
}
@@ -0,0 +1,886 @@
--!strict
local util = require("util")
local realearn = require("realearn")
local playtime_util = require("playtime_util")
local module = {}
export type ApcPlaytimePresetConfig = {
mute_track_button_id: string,
arm_track_button_id: string,
use_column_stop_buttons: boolean,
stop_column_if_slot_empty: boolean,
column_count: number,
row_count: number,
up_button_id: string,
down_button_id: string,
left_button_id: string,
right_button_id: string,
volume_button_id: string,
pan_button_id: string,
send_button_id: string,
device_button_id: string,
stop_all_clips_button_id: string,
stop_all_clips_need_shift: boolean,
}
function module.create_compartment(config: ApcPlaytimePresetConfig): realearn.Compartment
-- Aliases
local partial_mapping = util.partial_mapping
local turbo = playtime_util.turbo
local scroll_vertically = playtime_util.scroll_vertically
local scroll_horizontally = playtime_util.scroll_horizontally
-- Types
type Mode = {
index: number,
label: string,
}
type ModeMap = { [string]: Mode }
type ParameterMap = { [string]: realearn.Parameter }
type GroupMap = { [string]: realearn.Group }
-- Constants
local column_mode_count = 100
local knob_mode_count = 100
-- Column modes
local column_modes: ModeMap = {
stop = {
index = 0,
label = "Stop clip",
},
solo = {
index = 1,
label = "Solo",
},
record_arm = {
index = 2,
label = "Record arm",
},
mute = {
index = 3,
label = "Mute",
},
select = {
index = 4,
label = "Track select",
},
}
local sorted_column_modes = util.sorted_by_index(column_modes)
-- Knob modes
local knob_modes: ModeMap = {
volume = {
index = 0,
label = "Volume",
},
pan = {
index = 1,
label = "Pan",
},
sends = {
index = 2,
label = "Sends",
},
device = {
index = 3,
label = "Device",
},
}
local sorted_knob_modes = util.sorted_by_index(knob_modes)
-- Parameters
local params: ParameterMap = {
shift = {
index = 0,
name = "Shift modifier",
},
column_mode = {
index = 1,
name = "Column mode",
value_count = column_mode_count,
value_labels = util.extract_labels(sorted_column_modes),
},
knob_mode = {
index = 2,
name = "Knob mode",
value_count = knob_mode_count,
value_labels = util.extract_labels(sorted_knob_modes),
},
send = {
index = 3,
name = "Send",
value_count = 2,
},
sustain = {
index = 4,
name = "Sustain modifier",
},
}
-- Domain functions
local function create_index_expression(variable_name: string, index: number): string
return `{variable_name} + {index}`
end
local function create_col_expression(col: number): string
return create_index_expression("control_unit_column_index", col)
end
local function create_row_expression(row: number): string
return create_index_expression("control_unit_row_index", row)
end
local function create_slot_selector(col: number, row: number): realearn.PlaytimeSlotDescriptor
return realearn.PlaytimeSlotDescriptor.Dynamic {
column_expression = create_col_expression(col),
row_expression = create_row_expression(row),
}
end
local function create_column_selector(col: number): realearn.PlaytimeColumnDescriptor
return realearn.PlaytimeColumnDescriptor.Dynamic {
expression = create_col_expression(col),
}
end
local function create_row_selector(row: number): realearn.PlaytimeRowDescriptor
return realearn.PlaytimeRowDescriptor.Dynamic {
expression = create_row_expression(row),
}
end
local function knob(index: number)
return partial_mapping {
source = realearn.Source.Virtual {
character = "Multi",
id = index,
},
}
end
local function button(id: string)
return partial_mapping {
source = realearn.Source.Virtual {
character = "Button",
id = id,
},
}
end
local function column_stop_button(col: number)
return button(`col{col + 1}/stop`)
end
local function row_play_button(row: number)
return button(`row{row + 1}/play`)
end
local function slot_button(col: number, row: number)
return partial_mapping {
source = realearn.Source.Virtual {
character = "Multi",
id = `col{col + 1}/row{row + 1}/pad`,
},
}
end
local function shift_pressed(on: boolean)
return partial_mapping {
activation_condition = realearn.ActivationCondition.Modifier {
modifiers = {
{
parameter = params.shift.index,
on = on,
},
{
parameter = params.sustain.index,
on = false,
},
},
},
}
end
local function sustain_pressed(on: boolean)
return partial_mapping {
activation_condition = realearn.ActivationCondition.Modifier {
modifiers = {
{
parameter = params.sustain.index,
on = on,
},
{
parameter = params.shift.index,
on = false,
},
},
},
}
end
local function shift_or_sustain_pressed()
return partial_mapping {
activation_condition = realearn.ActivationCondition.Expression {
condition = `p[{params.shift.index}] || p[{params.sustain.index}]`,
},
}
end
local function shift_and_sustain_pressed()
return partial_mapping {
activation_condition = realearn.ActivationCondition.Modifier {
modifiers = {
{
parameter = params.sustain.index,
on = true,
},
{
parameter = params.shift.index,
on = true,
},
},
},
}
end
local function fire_max(millis: number)
return partial_mapping {
glue = {
fire_mode = realearn.FireMode.Normal {
press_duration_interval = { 0, millis },
},
},
}
end
local function fire_after_timeout(millis: number)
return partial_mapping {
glue = {
fire_mode = realearn.FireMode.AfterTimeout {
timeout = millis,
},
},
}
end
local function fire_on_single_press(max_duration: number)
return partial_mapping {
glue = {
fire_mode = realearn.FireMode.OnSinglePress {
max_duration = max_duration,
},
},
}
end
local function fire_on_double_press()
return partial_mapping {
glue = {
fire_mode = realearn.FireMode.OnDoublePress(),
},
}
end
local no_mod = shift_pressed(false)
local shift = shift_pressed(true)
local sustain = sustain_pressed(true)
local shift_or_sustain = shift_or_sustain_pressed()
local short_press = fire_max(200)
local long_press = fire_after_timeout(1000)
local single_press = fire_on_single_press(200)
local double_press = fire_on_double_press()
local function clip_matrix_action(action: realearn.PlaytimeMatrixAction)
return partial_mapping {
target = realearn.Target.PlaytimeMatrixAction {
action = action,
},
}
end
local function clip_column_action(col: number, action: realearn.PlaytimeColumnAction)
return partial_mapping {
target = realearn.Target.PlaytimeColumnAction {
column = create_column_selector(col),
action = action,
},
}
end
local function clip_row_action(row: number, action: realearn.PlaytimeRowAction)
return partial_mapping {
target = realearn.Target.PlaytimeRowAction {
row = create_row_selector(row),
action = action,
},
}
end
local function clip_column_track(col: number): realearn.TrackDescriptor
return realearn.TrackDescriptor.FromClipColumn {
column = create_column_selector(col),
context = "Playback",
}
end
local function column_track_target(col: number, track_target_kind: realearn.TargetKind, exclusive: boolean?)
return partial_mapping {
target = {
kind = track_target_kind,
track = clip_column_track(col),
exclusivity = if exclusive then "WithinFolderOnOnly" else nil,
} :: any,
}
end
local function route_target(col: number, route_target_kind: realearn.TargetKind)
return partial_mapping {
target = {
kind = route_target_kind,
route = realearn.RouteDescriptor.Dynamic {
track = clip_column_track(col),
expression = `p[{params.send.index}]`,
},
} :: any,
}
end
local function clip_transport_action(col: number, row: number, action: realearn.PlaytimeSlotTransportAction)
return partial_mapping {
target = realearn.Target.PlaytimeSlotTransportAction {
slot = create_slot_selector(col, row),
action = action,
stop_column_if_slot_empty = config.stop_column_if_slot_empty,
},
}
end
local function clip_management_action(col: number, row: number, action: realearn.PlaytimeSlotManagementAction)
return partial_mapping {
target = realearn.Target.PlaytimeSlotManagementAction {
slot = create_slot_selector(col, row),
action = action,
},
}
end
local function transport_action(action: realearn.TransportAction)
return partial_mapping {
target = realearn.Target.TransportAction {
action = action,
},
}
end
local function reaper_action(command: realearn.ReaperCommand)
return partial_mapping {
target = realearn.Target.ReaperAction {
command = command,
invocation = "Trigger",
},
}
end
local function toggle()
return partial_mapping {
glue = {
absolute_mode = "ToggleButton",
},
}
end
local function pick_up()
return partial_mapping {
glue = {
takeover_mode = "PickUpTolerant",
},
}
end
local function incremental()
return partial_mapping {
glue = {
absolute_mode = "IncrementalButton",
},
}
end
local function wrap()
return partial_mapping {
glue = {
wrap = true,
},
}
end
local function control_disabled()
return partial_mapping {
control_enabled = false,
visible_in_projection = false,
}
end
local function feedback_disabled()
return partial_mapping {
feedback_enabled = false,
}
end
local function set_param(index: number)
return partial_mapping {
target = realearn.Target.CompartmentParameterValue {
parameter = realearn.CompartmentParameterDescriptor.ById {
index = index,
},
},
}
end
local function name(n: string)
return partial_mapping {
name = n,
}
end
local function group(g: realearn.Group)
return partial_mapping {
group = g.id,
}
end
local function set_mode(mode: Mode, mode_count: number, mode_param_index: number)
local target_value = mode.index / (mode_count - 1)
return partial_mapping {
name = mode.label,
glue = {
target_interval = { target_value, target_value },
out_of_range_behavior = "Min",
},
target = realearn.Target.CompartmentParameterValue {
parameter = realearn.CompartmentParameterDescriptor.ById {
index = mode_param_index,
},
},
}
end
local function set_column_mode(mode: Mode)
return set_mode(mode, column_mode_count, params.column_mode.index)
end
local function set_knob_mode(mode: Mode)
return set_mode(mode, knob_mode_count, params.knob_mode.index)
end
local function column_mode_is(column_mode: Mode)
return realearn.ActivationCondition.Bank {
parameter = params.column_mode.index,
bank_index = column_mode.index,
}
end
local function knob_mode_is(knob_mode)
return realearn.ActivationCondition.Bank {
parameter = params.knob_mode.index,
bank_index = knob_mode.index,
}
end
-- Groups
local groups: GroupMap = {
slot_modes = {
name = "Slot modes",
},
column_modes = {
name = "Column modes",
},
record_settings = {
name = "Record settings",
},
knob_modes = {
name = "Knob modes",
},
slot_feedback = {
name = "Slot feedback",
},
slot_play = {
name = "Slot play",
},
slot_clear = {
name = "Slot clear",
},
slot_quantize = {
name = "Slot quantize",
},
slot_copy_or_paste = {
name = "Slot copy or paste",
},
slot_double = {
name = "Slot double section",
},
slot_halve = {
name = "Slot halve section",
},
column_stop = {
name = "Column stop",
activation_condition = column_mode_is(column_modes.stop),
},
row_play_scene = {
name = "Row play scene",
},
row_build_scene = {
name = "Row build scene",
},
row_copy_or_paste_scene = {
name = "Row copy or paste scene",
},
row_clear_scene = {
name = "Row clear scene",
},
column_solo = {
name = "Column solo",
activation_condition = column_mode_is(column_modes.solo),
},
column_record_arm = {
name = "Column record arm",
activation_condition = column_mode_is(column_modes.record_arm),
},
column_mute = {
name = "Column mute",
activation_condition = column_mode_is(column_modes.mute),
},
column_select = {
name = "Column select",
activation_condition = column_mode_is(column_modes.select),
},
knob_volume = {
name = "Knob volume",
activation_condition = knob_mode_is(knob_modes.volume),
},
knob_pan = {
name = "Knob pan",
activation_condition = knob_mode_is(knob_modes.pan),
},
knob_sends = {
name = "Knob sends",
activation_condition = knob_mode_is(knob_modes.sends),
},
knob_device = {
name = "Knob device",
activation_condition = knob_mode_is(knob_modes.device),
},
}
util.set_keys_as_ids(groups)
-- Mappings
local mappings = {
name("Play/stop") + no_mod + button("play") + toggle() + clip_matrix_action("PlayIgnitedOrEnterSilenceMode"),
name("Shift modifier") + button("shift") + set_param(params.shift.index),
name("Sustain modifier") + button("sustain") + set_param(params.sustain.index),
name("Undo") + shift + button("play") + clip_matrix_action("Undo"),
name("Record") + no_mod + button("record") + clip_matrix_action("SmartRecord"),
name("Redo") + shift + button("record") + clip_matrix_action("Redo"),
name("Build scene") + sustain + button("record") + clip_matrix_action("BuildScene"),
name("Switch send")
+ group(groups.knob_sends)
+ feedback_disabled()
+ shift
+ button("col7/stop")
+ incremental()
+ wrap()
+ set_param(params.send.index),
name("Column stop mode")
+ group(groups.column_modes)
+ shift
+ button("row1/play")
+ set_column_mode(column_modes.stop),
name("Column solo mode")
+ group(groups.column_modes)
+ shift
+ button("row2/play")
+ set_column_mode(column_modes.solo),
name("Column arm mode")
+ group(groups.column_modes)
+ shift
+ button(config.arm_track_button_id)
+ set_column_mode(column_modes.record_arm),
name("Column mute mode")
+ group(groups.column_modes)
+ shift
+ button(config.mute_track_button_id)
+ set_column_mode(column_modes.mute),
name("Column select mode")
+ group(groups.column_modes)
+ shift
+ button("row5/play")
+ set_column_mode(column_modes.select),
}
if config.stop_all_clips_need_shift then
-- We have a device that has a "Stop all clips" button but it needs shift to be pressed
table.insert(
mappings,
name("Stop all clips") + shift + button(config.stop_all_clips_button_id) + clip_matrix_action("Stop")
)
else
-- We have a device with a dedicated "Stop all clips" button
table.insert(
mappings,
name("Stop all clips") + no_mod + button(config.stop_all_clips_button_id) + clip_matrix_action("Stop")
)
table.insert(
mappings,
name("Click") + shift + button(config.stop_all_clips_button_id) + reaper_action(40364)
)
end
if config.use_column_stop_buttons then
-- Scrolling
table.insert(
mappings,
name("Scroll up") + shift_or_sustain + button(config.up_button_id) + turbo() + scroll_vertically(-1)
)
table.insert(
mappings,
name("Scroll down") + shift_or_sustain + button(config.down_button_id) + turbo() + scroll_vertically(1)
)
table.insert(
mappings,
name("Scroll left") + shift_or_sustain + button(config.left_button_id) + turbo() + scroll_horizontally(-1)
)
table.insert(
mappings,
name("Scroll right") + shift_or_sustain + button(config.right_button_id) + turbo() + scroll_horizontally(1)
)
-- Modes
table.insert(
mappings,
name("Knob volume mode")
+ group(groups.knob_modes)
+ shift
+ button(config.volume_button_id)
+ set_knob_mode(knob_modes.volume)
)
table.insert(
mappings,
name("Knob pan mode")
+ group(groups.knob_modes)
+ shift
+ button(config.pan_button_id)
+ set_knob_mode(knob_modes.pan)
)
table.insert(
mappings,
name("Knob send mode")
+ group(groups.knob_modes)
+ shift
+ button(config.send_button_id)
+ set_knob_mode(knob_modes.sends)
)
table.insert(
mappings,
name("Knob device mode")
+ group(groups.knob_modes)
+ shift
+ button(config.device_button_id)
+ set_knob_mode(knob_modes.device)
)
end
-- For each column
for col = 0, config.column_count - 1 do
-- Column stop button functions
if config.use_column_stop_buttons then
table.insert(
mappings,
name("Stop column")
+ group(groups.column_stop)
+ no_mod
+ column_stop_button(col)
+ clip_column_action(col, "Stop")
)
table.insert(
mappings,
name("Solo track")
+ group(groups.column_solo)
+ no_mod
+ toggle()
+ column_stop_button(col)
+ column_track_target(col, "TrackSoloState")
)
table.insert(
mappings,
name("Arm track")
+ group(groups.column_record_arm)
+ no_mod
+ toggle()
+ column_stop_button(col)
+ clip_column_action(col, "ArmStateExclusive")
)
table.insert(
mappings,
name("Mute track")
+ group(groups.column_mute)
+ no_mod
+ toggle()
+ column_stop_button(col)
+ column_track_target(col, "TrackMuteState")
)
table.insert(
mappings,
name("Select track")
+ group(groups.column_select)
+ no_mod
+ toggle()
+ column_stop_button(col)
+ column_track_target(col, "TrackSelectionState", true)
)
end
-- Knob functions
table.insert(
mappings,
name("Track volume") + group(groups.knob_volume) + knob(col) + pick_up() + column_track_target(col, "TrackVolume")
)
table.insert(
mappings,
name("Track pan") + group(groups.knob_pan) + knob(col) + pick_up() + column_track_target(col, "TrackPan")
)
table.insert(
mappings,
name("Track send volume") + group(groups.knob_sends) + knob(col) + pick_up() + route_target(col, "RouteVolume")
)
end
-- For each row
for row = 0, config.row_count - 1 do
table.insert(
mappings,
name("Play scene")
+ group(groups.row_play_scene)
+ feedback_disabled()
+ no_mod
+ row_play_button(row)
+ clip_row_action(row, "PlayScene")
)
table.insert(
mappings,
name("Copy or paste")
+ group(groups.row_copy_or_paste_scene)
+ sustain
+ short_press
+ row_play_button(row)
+ clip_row_action(row, "CopyOrPasteScene")
)
table.insert(
mappings,
name("Long = Clear")
+ group(groups.row_clear_scene)
+ feedback_disabled()
+ sustain
+ long_press
+ row_play_button(row)
+ clip_row_action(row, "ClearScene")
)
end
-- For each slot
for col = 0, config.column_count - 1 do
for row = 0, config.row_count - 1 do
-- Feedback
table.insert(
mappings,
name("Slot feedback")
+ group(groups.slot_feedback)
+ control_disabled()
+ slot_button(col, row)
+ playtime_util.slot_state_text_feedback()
+ clip_transport_action(col, row, "Trigger")
)
-- Control
table.insert(
mappings,
name("Slot trigger")
+ group(groups.slot_play)
+ feedback_disabled()
+ no_mod
+ slot_button(col, row)
+ clip_transport_action(col, row, "Trigger")
)
table.insert(
mappings,
name("Copy or paste")
+ group(groups.slot_copy_or_paste)
+ feedback_disabled()
+ sustain
+ single_press
+ slot_button(col, row)
+ toggle()
+ clip_management_action(col, row, "CopyOrPasteClip")
)
table.insert(
mappings,
name("Long = Delete")
+ group(groups.slot_clear)
+ feedback_disabled()
+ sustain
+ long_press
+ slot_button(col, row)
+ clip_management_action(col, row, "ClearSlot")
)
table.insert(
mappings,
name("2x = Edit")
+ group(groups.slot_quantize)
+ feedback_disabled()
+ sustain
+ double_press
+ slot_button(col, row)
+ toggle()
+ clip_management_action(col, row, "EditClip")
)
--table.insert(mappings, name("Overdub clip") + group(groups.slot_play) + feedback_disabled() + shift + single_press + slot_button(col, row) + toggle() + clip_transport_action(col, row, "RecordStop", false))
--table.insert(mappings, name("2x = Double section") + group(groups.slot_double) + feedback_disabled() + shift + double_press + slot_button(col, row) + adjust_clip_section_length_action(col, row, 2))
--table.insert(mappings, name("1x = Halve section") + group(groups.slot_double) + feedback_disabled() + shift + single_press + slot_button(col, row) + adjust_clip_section_length_action(col, row, 0.5))
table.insert(
mappings,
name("Fill slot")
+ group(groups.slot_quantize)
+ feedback_disabled()
+ shift
+ single_press
+ slot_button(col, row)
+ clip_management_action(col, row, "FillSlotWithSelectedItem")
)
end
end
return realearn.Compartment {
parameters = util.sorted_by_index(params),
groups = util.to_array(groups),
mappings = mappings :: any,
custom_data = {
playtime = {
control_unit = {
column_count = config.column_count,
row_count = config.row_count,
},
},
},
}
end
return module
@@ -0,0 +1,34 @@
--- name: APC mini mk2 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Akai
--- device_name: APC mini mk2
--- description: |
--- This main preset turns the APC mini mk2 into a capable device for controlling Playtime. Hold Shift
--- to access advanced functions.
--- used_schemes: [akai/apc-mini-mk2]
--- required_features: [playtime]
--!strict
local commons = require("akai/apc-lib/playtime-commons")
return commons.create_compartment {
use_column_stop_buttons = true,
stop_column_if_slot_empty = true,
mute_track_button_id = "row3/play",
arm_track_button_id = "row4/play",
column_count = 8,
row_count = 8,
up_button_id = "col5/stop",
down_button_id = "col6/stop",
left_button_id = "col7/stop",
right_button_id = "col8/stop",
volume_button_id = "col1/stop",
pan_button_id = "col2/stop",
send_button_id = "col3/stop",
device_button_id = "col4/stop",
stop_all_clips_button_id = "row8/play",
stop_all_clips_need_shift = true,
}
@@ -0,0 +1,34 @@
--- name: APC mini - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Akai
--- device_name: APC mini
--- description: |
--- This main preset turns the APC mini into a capable device for controlling Playtime. Hold Shift
--- to access advanced functions.
--- used_schemes: [akai/apc-mini]
--- required_features: [playtime]
--!strict
local commons = require("akai/apc-lib/playtime-commons")
return commons.create_compartment {
use_column_stop_buttons = true,
stop_column_if_slot_empty = true,
mute_track_button_id = "row4/play",
arm_track_button_id = "row3/play",
column_count = 8,
row_count = 8,
up_button_id = "col1/stop",
down_button_id = "col2/stop",
left_button_id = "col3/stop",
right_button_id = "col4/stop",
volume_button_id = "col5/stop",
pan_button_id = "col6/stop",
send_button_id = "col7/stop",
device_button_id = "col8/stop",
stop_all_clips_button_id = "row8/play",
stop_all_clips_need_shift = true,
}
@@ -0,0 +1,75 @@
--- name: Generic grid controller - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Generic
--- device_name: Generic
--- description: |
--- This main preset turns any grid-like controller into a device for doing very basic clip launching via Playtime.
---
--- This generic preset is intended to be used as fallback if no device-specific preset exists. It lacks features because it can't take advantage
--- of device-specific features.
--- used_schemes: [grid]
--- required_features: [playtime]
--!strict
-- Configuration
local stop_column_if_slot_empty = true
-- Requires
local realearn = require("realearn")
-- Constants
-- TODO This should be parameterized from the controller preset. One more reason to allow script parameterization :)
local column_count = 4
local row_count = 4
-- Mappings
local mappings = {}
-- For each slot
for col = 0, column_count - 1 do
for row = 0, row_count - 1 do
local mapping = realearn.Mapping {
name = `Trigger slot {col + 1}/{row + 1}`,
source = realearn.Source.Virtual {
character = "Multi",
id = `col{col + 1}/row{row + 1}/pad`,
},
glue = {
feedback = realearn.Feedback.Text {
text_expression = "{{ target.slot_state.id }}",
color = realearn.VirtualColor {
prop = "target.slot.color",
},
},
},
target = realearn.Target.PlaytimeSlotTransportAction {
slot = realearn.PlaytimeSlotDescriptor.Dynamic {
column_expression = `control_unit_column_index + {col}`,
row_expression = `control_unit_row_index + {row}`,
},
action = "Trigger",
stop_column_if_slot_empty = stop_column_if_slot_empty,
},
}
table.insert(mappings, mapping)
end
end
return realearn.Compartment {
mappings = mappings :: any,
custom_data = {
playtime = {
control_unit = {
column_count = column_count,
row_count = row_count,
},
},
},
}
@@ -0,0 +1,122 @@
--- name: Numbered - FX parameters
--- realearn_version: 2.16.0-pre.9
--- author: helgoboss
--- device_manufacturer: Generic
--- device_name: Generic
--- description: |
--- This main preset is built for controllers that offer a set of knobs and buttons. Each knob will control one parameter of
--- the unit FX (which by default resolves to the currently focused FX), starting with parameter 1. Each button will switch
--- to the next bank of parameters. If the controller has `bank-left` and `bank-right` buttons, they can be used to switch
--- between pages, making it possible to control even more parameters.
---
--- At the moment, this preset assumes that the controller has exactly 16 knobs and buttons. In future, this will be variable.
--- used_schemes: [numbered]
--!strict
local realearn = require("realearn")
--- Configuration
local multi_count = 16
local button_count = 16
-- Code
local page_count = 16
local bank_count = button_count
local parameters: { realearn.Parameter } = {
{
index = 0,
name = "Page",
value_count = page_count,
},
{
index = 1,
name = "Bank",
value_count = bank_count,
},
}
local mappings: { realearn.Mapping } = {
-- Side button left => Decrease page
realearn.Mapping {
name = "Page -",
source = realearn.Source.Virtual {
character = "Button",
id = "bank-left",
},
glue = {
absolute_mode = "IncrementalButton",
reverse = true,
},
target = realearn.Target.CompartmentParameterValue {
parameter = realearn.CompartmentParameterDescriptor.ById {
index = 0,
},
},
},
-- Side button right => Increase page
realearn.Mapping {
name = "Page +",
source = realearn.Source.Virtual {
character = "Button",
id = "bank-right",
},
glue = {
absolute_mode = "IncrementalButton",
reverse = false,
},
target = realearn.Target.CompartmentParameterValue {
parameter = realearn.CompartmentParameterDescriptor.ById {
index = 0,
},
},
},
}
-- Encoder movement => Set FX parameter value
for i = 0, multi_count - 1 do
local m = realearn.Mapping {
name = `Set parameter value {i + 1}`,
source = realearn.Source.Virtual {
character = "Multi",
id = i,
},
target = realearn.Target.FxParameterValue {
parameter = realearn.FxParameterDescriptor.Dynamic {
fx = realearn.FxDescriptor.Instance {},
expression = `p[0] * {page_count} * {bank_count} + p[1] * {bank_count} + {i}`,
},
},
}
table.insert(mappings, m)
end
-- Encoder push => Select bank
for i = 0, button_count - 1 do
local normalized_bank = i / (button_count - 1)
local m = realearn.Mapping {
name = `Switch to bank {i + 1}`,
source = realearn.Source.Virtual {
character = "Button",
id = i,
},
glue = {
out_of_range_behavior = "Min",
target_interval = { normalized_bank, normalized_bank },
},
target = realearn.Target.CompartmentParameterValue {
parameter = realearn.CompartmentParameterDescriptor.ById {
index = 1,
},
},
}
table.insert(mappings, m)
end
return realearn.Compartment {
parameters = parameters,
mappings = mappings,
}
@@ -0,0 +1,29 @@
local mappings = {}
for i = 0, 7 do
local m = {
name = tostring(i + 1),
source = {
kind = "Key",
keystroke = {
modifiers = 1,
key = 49 + i,
},
},
glue = {
button_filter = "PressOnly",
},
target = {
kind = "SendMidi",
message = "B0 00 0" .. i,
},
}
table.insert(mappings, m)
end
return {
kind = "MainCompartment",
value = {
mappings = mappings,
}
}
@@ -0,0 +1 @@
../../api/luau/midi_script_source_runtime.luau
@@ -0,0 +1,48 @@
--- name: Launchpad Mini mk3 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Novation
--- device_name: Launchpad Mini mk3
--- description: |
--- This main preset turns the Launchpad Mini mk3 into a capable device for controlling Playtime.
--- used_schemes: [novation/launchpad-mini-mk3/live]
--- required_features: [playtime]
--!strict
local commons = require("novation/launchpad-lib/playtime-commons")
return commons.create_compartment {
stop_column_if_slot_empty = true,
has_dedicated_column_action_buttons = false,
has_shift_button = false,
has_delete_button = false,
has_quantize_button = false,
has_fixed_length_button = false,
has_duplicate_button = false,
has_play_button = false,
has_record_button = false,
stop_clip_button_id = nil,
stop_clip_needs_mixer = false,
mute_button_id = nil,
mute_needs_mixer = false,
solo_button_id = nil,
solo_needs_mixer = false,
record_arm_button_id = nil,
record_arm_needs_mixer = false,
track_select_button_id = nil,
undo_button_id = nil,
undo_needs_shift = true,
redo_button_id = nil,
redo_needs_shift = true,
click_button_id = nil,
click_needs_shift = true,
double_button_id = nil,
double_needs_shift = true,
tap_button_id = nil,
tap_needs_shift = true,
use_last_row_play_button_for_column_modes = true,
use_mk1_colors = false,
}
@@ -0,0 +1,51 @@
--- name: Launchpad mk1 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Novation
--- device_name: Launchpad mk1
--- description: |
--- This main preset turns the Launchpad mk1 into a capable device for controlling Playtime.
---
--- The following features are not yet supported: vol, pan, snd A, snd B
--- used_schemes: [novation/launchpad-mk1]
--- required_features: [playtime]
--!strict
local commons = require("novation/launchpad-lib/playtime-commons")
return commons.create_compartment {
stop_column_if_slot_empty = true,
has_dedicated_column_action_buttons = false,
has_shift_button = false,
has_delete_button = false,
has_quantize_button = false,
has_fixed_length_button = false,
has_duplicate_button = false,
has_play_button = false,
has_record_button = false,
stop_clip_button_id = "row5/play",
stop_clip_needs_mixer = true,
mute_button_id = "row6/play",
mute_needs_mixer = true,
solo_button_id = "row7/play",
solo_needs_mixer = true,
record_arm_button_id = "row8/play",
record_arm_needs_mixer = true,
track_select_button_id = nil,
undo_button_id = nil,
undo_needs_shift = false,
redo_button_id = nil,
redo_needs_shift = false,
click_button_id = nil,
click_needs_shift = false,
double_button_id = nil,
double_needs_shift = false,
tap_button_id = nil,
tap_needs_shift = false,
use_last_row_play_button_for_column_modes = false,
mixer_button_id = "mixer",
use_mk1_colors = true,
}
@@ -0,0 +1,51 @@
--- name: Launchpad Pro mk2 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Novation
--- device_name: Launchpad Pro mk2
--- description: |
--- This main preset turns the Launchpad Pro mk2 into a capable device for controlling Playtime.
---
--- The following features are not yet supported: Note, Device, User, Volume, Pan, Sends, Record,
--- Record Quantise.
--- used_schemes: [novation/launchpad-pro-mk2/live]
--- required_features: [playtime]
--!strict
local commons = require("novation/launchpad-lib/playtime-commons")
return commons.create_compartment {
stop_column_if_slot_empty = true,
has_dedicated_column_action_buttons = false,
has_shift_button = true,
has_delete_button = true,
has_quantize_button = true,
has_fixed_length_button = false,
has_duplicate_button = true,
has_play_button = false,
has_record_button = true,
stop_clip_button_id = "stop-clip",
stop_clip_needs_mixer = false,
mute_button_id = "mute",
mute_needs_mixer = false,
solo_button_id = "solo",
solo_needs_mixer = false,
record_arm_button_id = "record-arm",
record_arm_needs_mixer = false,
track_select_button_id = "track-select",
undo_button_id = "undo",
undo_needs_shift = false,
redo_button_id = "undo",
redo_needs_shift = true,
click_button_id = "click",
click_needs_shift = false,
double_button_id = "double",
double_needs_shift = false,
tap_button_id = nil,
tap_needs_shift = false,
use_last_row_play_button_for_column_modes = false,
mixer_button_id = nil,
}
@@ -0,0 +1,54 @@
--- name: Launchpad Pro mk3 - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Novation
--- device_name: Launchpad Pro mk3
--- description: |
--- This main preset turns the Launchpad Pro mk3 into a capable device for controlling Playtime.
---
--- The following features are not yet supported: Note, Chord, Custom, Sequencer, Projects, Volume, Pan, Sends, Device,
--- Swing, Tempo, Capture MIDI, Patterns, Steps, Pattern Settings, Velocity, Probability,
--- Mutation, Micro Step, Print to Clip.
--- used_schemes: [novation/launchpad-pro-mk3/live]
--- required_features: [playtime]
--!strict
local commons = require("novation/launchpad-lib/playtime-commons")
return commons.create_compartment {
stop_column_if_slot_empty = true,
has_dedicated_column_action_buttons = true,
has_shift_button = true,
has_delete_button = true,
has_quantize_button = true,
has_fixed_length_button = true,
has_duplicate_button = true,
has_play_button = true,
has_record_button = true,
stop_clip_button_id = "stop-clip",
stop_clip_needs_mixer = false,
mute_button_id = "mute",
mute_needs_mixer = false,
solo_button_id = "solo",
solo_needs_mixer = false,
record_arm_button_id = "record-arm",
record_arm_needs_mixer = false,
track_select_button_id = "track-select",
undo_button_id = "record-arm",
undo_needs_shift = true,
redo_button_id = "mute",
redo_needs_shift = true,
click_button_id = "solo",
click_needs_shift = true,
double_button_id = "duplicate",
double_needs_shift = true,
tap_button_id = "sends",
tap_needs_shift = true,
use_last_row_play_button_for_column_modes = false,
mixer_button_id = nil,
use_mk1_colors = false,
}
@@ -0,0 +1,51 @@
--- name: Launchpad X - Playtime
--- realearn_version: 2.16.0-pre.8
--- author: helgoboss
--- device_manufacturer: Novation
--- device_name: Launchpad X
--- description: |
--- This main preset turns the Launchpad X into a capable device for controlling Playtime.
---
--- The following features are not yet supported: Note, Custom, Volume, Pan, Send A, Send B, Capture MIDI
--- used_schemes: [novation/launchpad-x/live]
--- required_features: [playtime]
--!strict
local commons = require("novation/launchpad-lib/playtime-commons")
return commons.create_compartment {
stop_column_if_slot_empty = true,
has_dedicated_column_action_buttons = false,
has_shift_button = false,
has_delete_button = false,
has_quantize_button = false,
has_fixed_length_button = false,
has_duplicate_button = false,
has_play_button = false,
has_record_button = true,
stop_clip_button_id = "row5/play",
stop_clip_needs_mixer = true,
mute_button_id = "row6/play",
mute_needs_mixer = true,
solo_button_id = "row7/play",
solo_needs_mixer = true,
record_arm_button_id = "row8/play",
record_arm_needs_mixer = true,
track_select_button_id = nil,
undo_button_id = nil,
undo_needs_shift = false,
redo_button_id = nil,
redo_needs_shift = false,
click_button_id = nil,
click_needs_shift = false,
double_button_id = nil,
double_needs_shift = false,
tap_button_id = nil,
tap_needs_shift = false,
use_last_row_play_button_for_column_modes = false,
mixer_button_id = "session",
use_mk1_colors = false,
}
@@ -0,0 +1,51 @@
--!strict
type ArbitraryTable = { [any]: any }
--- Returns a new table that is the result of merging t2 into t1.
---
--- Values in t2 have precedence.
---
--- The result will be mergeable as well. This is good for "modifier chaining".
---
--- Typing not ideal because of https://github.com/luau-lang/luau/issues/392#issuecomment-1050344334
function merged<A, B>(t1: A, t2: B): A & B
if type(t1) ~= "table" or type(t2) ~= "table" then
return t1 :: any
end
local result = table.clone(t1)
for key, new_value in pairs(t2 :: ArbitraryTable) do
local old_value = result[key]
if old_value and type(old_value) == "table" and type(new_value) == "table" then
-- Merge table value as well
result[key] = merged(old_value, new_value) :: any
else
-- Simple use new value
result[key] = new_value
end
end
return result :: any
end
type PartialImpl = {
new: (part: ArbitraryTable) -> Partial,
__index: PartialImpl,
__add: (self: Partial, part: Partial) -> Partial,
}
export type Partial = typeof(setmetatable({}, {} :: PartialImpl))
local Partial: PartialImpl = {} :: PartialImpl
Partial.__index = Partial
function Partial.new(part)
return setmetatable(part, Partial)
end
function Partial:__add(part)
return Partial.new(merged(self, part) :: any)
end
return {
Partial = Partial,
}
@@ -0,0 +1 @@
../../api/luau/playtime.luau
@@ -0,0 +1,59 @@
--!strict
local realearn = require("realearn")
local util = require("util")
local playtime_util = {}
--- A partial mapping that enables turbo mode for buttons.
function playtime_util.turbo()
return util.partial_mapping {
glue = {
fire_mode = realearn.FireMode.AfterTimeoutKeepFiring {
timeout = 0,
rate = 100,
},
},
}
end
local function scroll(axis: realearn.Axis, amount: number)
local abs_amount = math.abs(amount)
return util.partial_mapping {
glue = {
absolute_mode = "IncrementalButton",
step_factor_interval = { abs_amount, abs_amount },
reverse = amount < 0,
--- This makes the LED switch off when no scrolling is possible in the given direction
feedback = realearn.Feedback.Numeric {
transformation = "x = y < 1",
},
},
target = realearn.Target.PlaytimeControlUnitScroll {
axis = axis,
},
}
end
function playtime_util.scroll_horizontally(amount: number)
return scroll("X", amount)
end
function playtime_util.scroll_vertically(amount: number)
return scroll("Y", amount)
end
function playtime_util.slot_state_text_feedback()
return util.partial_mapping {
glue = {
feedback = realearn.Feedback.Text {
text_expression = "{{ target.slot_state.id }}",
color = realearn.VirtualColor {
prop = "target.slot.color",
},
},
},
}
end
return playtime_util
@@ -0,0 +1 @@
../../api/luau/preset_runtime.luau
@@ -0,0 +1 @@
../../api/luau/realearn.luau
@@ -0,0 +1,56 @@
--!strict
local realearn = require("realearn")
local partial = require("partial")
local util = {}
--- Creates a partial mapping.
---
--- A partial mapping can be conveniently mixed with other partial mappings by using the + operator.
--- This enables a very intuitive but still flexible way of writing mappings.
---
--- Ideally, we would return Partial & Mapping but Luau has still issues with that. In particular, it
--- can't resolve the + operator when used in an intersection.
function util.partial_mapping(m: realearn.Mapping): partial.Partial
return partial.Partial.new(m)
end
--- Returns the values of the given key-value table as array.
function util.to_array<V>(t: { [string]: V }): { V }
local array = {}
for _, v in t do
table.insert(array, v)
end
return array
end
--- Returns the values of the given key-value table as array, sorted by the values'
--- `index` key.
function util.sorted_by_index<V>(t: { [string]: V }): { V }
local sorted = util.to_array(t)
local compare_index = function(left: any, right: any): boolean
return left.index < right.index
end
table.sort(sorted, compare_index)
return sorted
end
--- Takes a key-value table and adds a new attribute `id` to each value that
--- corresponds to the key.
function util.set_keys_as_ids(t: { [string]: { [string]: any } })
for key, value in pairs(t) do
value.id = key
end
end
--- Puts each `label` property value of the given array of tables into a new array.
function util.extract_labels(array: { { [string]: any } }): { string }
local labels = {}
for _, element in ipairs(array) do
table.insert(labels, element.label)
end
return labels
end
return util