Skip to content
Benjamin Klum edited this page Dec 18, 2024 · 3 revisions

This page shows some ReaLearn main and controller presets that are not related to particular controllers. Feel free to add your own!

Controller presets

Notes to numbers

This preset exposes each note on a MIDI keyboard as a numbered virtual control elements. Each controller mapping is named like the corresponding note (for example "C#2"). That makes it easier to identify in the main compartment which note triggers which action.

You can use this preset whenever you want to map MIDI keyboard notes to targets of your choice.

local mappings = {}

local note_names = {
    "C",
    "C#",
    "D",
    "D#",
    "E",
    "F",
    "F#",
    "G",
    "G#",
    "A",
    "A#",
    "B",
}

for i = 0, 127 do
    local note = {
        name = note_names[i % 12 + 1] .. math.floor(i / 12) - 1,
        feedback_enabled = false,
        source = {
            kind = "MidiNoteVelocity",
            key_number = i,
        },
        target = {
            kind = "Virtual",
            character = "Multi",
            id = i
        },
    }
    table.insert(mappings, note)
end

return {
    kind = "ControllerCompartment",
    value = {
        mappings = mappings,
    },
}

QWERTY to names

This preset exposes each key on your QWERTY keyboard as a named virtual control element (for example "key/p").

local mappings = {}

local special_key_ids = {
    [ "," ] = "comma",
    [ "." ] = "period",
}

function get_key_id(ascii_code)
    local char = string.lower(string.char(ascii_code))
    return special_key_ids[char] or char
end

function key_mapping(modifiers, ascii_code)
    return {
        source = {
            kind = "Key",
            keystroke = {
                modifiers = modifiers,
                key = ascii_code,
            },
        },
        target = {
            kind = "Virtual",
            character = "Button",
            id = "key/" .. get_key_id(ascii_code),
        },
    }
end

-- Digits
for i = 48, 57 do
    table.insert(mappings, key_mapping(1, i))
end

-- Letters
for i = 65, 90 do
    table.insert(mappings, key_mapping(1, i))
end

-- Comma and dot
table.insert(mappings, key_mapping(0, 44))
table.insert(mappings, key_mapping(0, 46))

return {
    kind = "ControllerCompartment",
    value = {
        mappings = mappings,
    },
}

Main presets for DAW control

DAW control

This preset emulates the typical "Mackie Control"-style DAW control. Not completely, but a large part. It is compatible with any controller preset that provides the DAW scheme. It is a perpetual work in progress and will be improved over time.

This preset is a built-in factory preset. You can easily use it by selecting Main preset  Generic  DAW control.

Main presets for live playing

Grid to instrument

This preset works with grid controllers that have column stop buttons, e.g. the Akai APC Key 25 Mk2.

Each column stop button toggles the arm state of a corresponding instrument track, switching it on or off for live playing, as you desire. The name of each instrument track must start with "Instrument 01", "Instrument 02", etc.

Best of all: It works with visual LED feedback!

-- Configuration

local column_count = 8

-- Functions

function format_as_two_digits(n)
    if n < 10 then
        return "0" .. tostring(n)
    else
        return tostring(n)
    end
end

-- Mappings

local mappings = {}

for col = 0, column_count - 1 do
    local human_col = col + 1
    local two_digit_col = format_as_two_digits(human_col)
    local mapping = {
        source = {
            kind = "Virtual",
            id = "col" .. human_col .. "/stop",
            character = "Button",
        },
        glue = {
            absolute_mode = "ToggleButton",
        },
        target = {
            kind = "TrackArmState",
            track = {
                address = "ByName",
                name ="Instrument " .. two_digit_col .. "*",
            },
        },
    }
    table.insert(mappings, mapping)
end

return {
    kind = "MainCompartment",
    value = {
        mappings = mappings,
    },
}

Grid to instrument layers

This preset works with grid controllers, e.g. the Akai APC Key 25 Mk2.

  • Each grid button toggles the mute state of a corresponding instrument layer track, allowing you to add or remove that layer from the audible sound, as you desire.

  • The name of each instrument layer track must start with the layer name followed by 01, 02, etc. Example: "Drums 01: Acoustic".

  • The layers can be configured in the below script.

  • Each scene start button recalls a certain combination of layers. You can save the current combination by pressing a scene button while pressing the "Shift" key (on the grid controller, not on your QWERTY keyboard).

Best of all: It works with visual LED feedback!

-- Configuration

local column_count = 8
local row_count = 5

local layers = {
    "Drums",
    "Bass",
    "Filler",
    "Keys",
}

-- Functions

function format_as_two_digits(n)
    if n < 10 then
        return "0" .. tostring(n)
    else
        return tostring(n)
    end
end

-- Mappings

local parameters = {
    {
        index = 0,
        name = "Shift",
    },
}

local groups = {
    {
        id = "slots",
        name = "Slots",
        tags = { "slot" }
    },
    {
        id = "scenes",
        name = "Scenes",
    },
    {
        id = "modifiers",
        name = "Modifiers",
    },
}

local mappings = {
    {
        name = "Shift",
        group = "modifiers",
        source = {
            kind = "Virtual",
            id = "shift",
            character = "Button",
        },
        target = {
            kind = "FxParameterValue",
            parameter = {
                address = "ById",
                fx = {
                    address = "This",
                },
                index = 0,
            },
        },
    }
}

-- Slots

for col = 0, column_count - 1 do
    for human_row, layer in ipairs(layers) do
        local human_col = col + 1
        local two_digit_col = format_as_two_digits(human_col)
        local mapping = {
            name = layer .. " " .. human_col,
            group = "slots",
            source = {
                kind = "Virtual",
                id = "col" .. human_col .. "/row" .. human_row .. "/pad",
                character = "Button",
            },
            glue = {
                source_interval = { 0.04, 1.0 },
                absolute_mode = "ToggleButton",
                reverse = true,
            },
            target = {
                kind = "TrackMuteState",
                track = {
                    address = "ByName",
                    name = layer .. " " .. two_digit_col .. "*",
                },
            },
        }
        table.insert(mappings, mapping)
    end
end

-- Scenes

for row = 0, row_count - 1 do
    local human_row = row + 1
    local save_scene_mapping = {
        name = "Save scene " .. human_row,
        group = "scenes",
        activation_condition = {
            kind = "Modifier",
            modifiers = {
                {
                    parameter = 0,
                    on = true,
                },
            },
        },
        source = {
            kind = "Virtual",
            id = "row" .. human_row .. "/play",
            character = "Button",
        },
        target = {
            kind = "TakeMappingSnapshot",
            tags = {
                "slot",
            },
            active_mappings_only = false,
            snapshot_id = "scene_" .. human_row,
        },
    }
    local load_scene_mapping = {
        name = "Scene " .. human_row,
        group = "scenes",
        activation_condition = {
            kind = "Modifier",
            modifiers = {
                {
                    parameter = 0,
                    on = false,
                },
            },
        },
        source = {
            kind = "Virtual",
            id = "row" .. human_row .. "/play",
            character = "Button",
        },
        target = {
            kind = "LoadMappingSnapshot",
            tags = {
                "slot",
            },
            active_mappings_only = false,
            snapshot = {
                kind = "ById",
                id = "scene_" .. human_row,
            },
            default_value = {
                kind = "Unit",
                value = 1
            }
        },
    }
    table.insert(mappings, save_scene_mapping)
    table.insert(mappings, load_scene_mapping)
end

return {
    kind = "MainCompartment",
    value = {
        parameters = parameters,
        groups = groups,
        mappings = mappings,
    },
}

Main presets for Pot Browser control

One-channel DAW controller to Pot Browser

This preset makes it possible to use a one-channel DAW controller (e.g. X-Touch One) to control Pot Browser.

return {
    kind = "MainCompartment",
    version = "2.15.0-pre.3",
    value = {
        parameters = {
            {
                index = 0,
                name = "Filter bank",
                value_count = 10,
            },
        },
        groups = {
            {
                id = "RZQLaoQOQwZFBCRaH8kHB",
                name = "Filter pages",
            },
            {
                id = "tRUhYa0RGcup1XIyV3zvr",
                name = "Preset browsing",
            },
            {
                id = "C_6AY80tdoM1Cy9FHi7XN",
                name = "Filter page \"Character\"",
                activation_condition = {
                    kind = "Bank",
                    parameter = 0,
                    bank_index = 4,
                },
            },
            {
                id = "EFpF3FW5Lp6k6jKBSh6_I",
                name = "Filter page \"Sub type\"",
                activation_condition = {
                    kind = "Bank",
                    parameter = 0,
                    bank_index = 3,
                },
            },
            {
                id = "lpnoZyrYUdSappcySl38d",
                name = "Filter page \"Type\"",
                activation_condition = {
                    kind = "Bank",
                    parameter = 0,
                    bank_index = 2,
                },
            },
            {
                id = "5A6qajMq6KFFVs8sVEIGV",
                name = "Filter page \"Bank\"",
                activation_condition = {
                    kind = "Bank",
                    parameter = 0,
                    bank_index = 1,
                },
            },
            {
                id = "zbTTUwkU-D-xslTQjGa3m",
                name = "Filter page \"Instrument\"",
                activation_condition = {
                    kind = "Bank",
                    parameter = 0,
                    bank_index = 0,
                },
            },
        },
        mappings = {
            {
                id = "MaMuToR-Qp8dB1it-ESP_",
                name = "Choose instrument",
                group = "RZQLaoQOQwZFBCRaH8kHB",
                source = {
                    kind = "Virtual",
                    id = "f1",
                    character = "Button",
                },
                glue = {
                    target_interval = {0, 0},
                    out_of_range_behavior = "Min",
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    button_filter = "PressOnly",
                },
                target = {
                    kind = "FxParameterValue",
                    parameter = {
                        address = "ById",
                        index = 0,
                    },
                },
            },
            {
                id = "7sIZfWbiSjYGqZR8ebE3m",
                name = "Choose bank",
                group = "RZQLaoQOQwZFBCRaH8kHB",
                source = {
                    kind = "Virtual",
                    id = "f2",
                    character = "Button",
                },
                glue = {
                    target_interval = {0.1111111111111111, 0.1111111111111111},
                    out_of_range_behavior = "Min",
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    button_filter = "PressOnly",
                },
                target = {
                    kind = "FxParameterValue",
                    parameter = {
                        address = "ById",
                        index = 0,
                    },
                },
            },
            {
                id = "1ZSzf-B4xTurAkVFkss3y",
                name = "Choose type",
                group = "RZQLaoQOQwZFBCRaH8kHB",
                source = {
                    kind = "Virtual",
                    id = "f3",
                    character = "Button",
                },
                glue = {
                    target_interval = {0.2222222222222222, 0.2222222222222222},
                    out_of_range_behavior = "Min",
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    button_filter = "PressOnly",
                },
                target = {
                    kind = "FxParameterValue",
                    parameter = {
                        address = "ById",
                        index = 0,
                    },
                },
            },
            {
                id = "t3jwdnxgLSgNwnYhfV3bX",
                name = "Choose sub type",
                group = "RZQLaoQOQwZFBCRaH8kHB",
                source = {
                    kind = "Virtual",
                    id = "f4",
                    character = "Button",
                },
                glue = {
                    target_interval = {0.3333333333333333, 0.3333333333333333},
                    out_of_range_behavior = "Min",
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    button_filter = "PressOnly",
                },
                target = {
                    kind = "FxParameterValue",
                    parameter = {
                        address = "ById",
                        index = 0,
                    },
                },
            },
            {
                id = "XrMjoK-jksKjBvkJR560T",
                name = "Choose character",
                group = "RZQLaoQOQwZFBCRaH8kHB",
                source = {
                    kind = "Virtual",
                    id = "f5",
                    character = "Button",
                },
                glue = {
                    target_interval = {0.4444444444444444, 0.4444444444444444},
                    out_of_range_behavior = "Min",
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    button_filter = "PressOnly",
                },
                target = {
                    kind = "FxParameterValue",
                    parameter = {
                        address = "ById",
                        index = 0,
                    },
                },
            },
            {
                id = "GW6k2reFw5uSNWg2gFGqS",
                name = "Browse",
                group = "zbTTUwkU-D-xslTQjGa3m",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "jog",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Bank",
                },
            },
            {
                id = "nVTUaxgroOo_YfugMyk0X",
                name = "Browse",
                group = "5A6qajMq6KFFVs8sVEIGV",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "jog",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "SubBank",
                },
            },
            {
                id = "LryQpIKLKRMLwHA6NfDTB",
                name = "Browse",
                group = "lpnoZyrYUdSappcySl38d",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "jog",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Category",
                },
            },
            {
                id = "rKGURMBGDTLps-HDQGbz-",
                name = "Browse",
                group = "EFpF3FW5Lp6k6jKBSh6_I",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "jog",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "SubCategory",
                },
            },
            {
                id = "BWAF5SbeC58ABUfgs15Ul",
                name = "Browse",
                group = "C_6AY80tdoM1Cy9FHi7XN",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "jog",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Mode",
                },
            },
            {
                id = "d0avz2uKc1Kc3jot4FJF7",
                name = "Display",
                group = "C_6AY80tdoM1Cy9FHi7XN",
                control_enabled = false,
                source = {
                    kind = "MackieSevenSegmentDisplay",
                    scope = "Tc",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Mode",
                },
            },
            {
                id = "PG926UQqFa1tKbX7klaq1",
                name = "Browse presets coarse",
                group = "tRUhYa0RGcup1XIyV3zvr",
                source = {
                    kind = "Virtual",
                    id = "ch1/fader",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "OpDDrpDG2hAv5gDjYubfk",
                name = "Browse presets fine",
                group = "tRUhYa0RGcup1XIyV3zvr",
                source = {
                    kind = "Virtual",
                    id = "ch1/v-pot",
                },
                glue = {
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "iw_ng6_2JADZESl3X2axi",
                name = "Previous preset",
                group = "tRUhYa0RGcup1XIyV3zvr",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "cursor-left",
                    character = "Button",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    reverse = true,
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "6qovylkC3n0J9DqkeIhTO",
                name = "Next preset",
                group = "tRUhYa0RGcup1XIyV3zvr",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "cursor-right",
                    character = "Button",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "RJDETxxijd4tIv36zzv51",
                name = "Display preset name",
                group = "tRUhYa0RGcup1XIyV3zvr",
                control_enabled = false,
                source = {
                    kind = "MackieLcd",
                    channel = 0,
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "9XuuQ0Tl315m561HPwdz1",
                name = "Say preset name",
                group = "tRUhYa0RGcup1XIyV3zvr",
                enabled = false,
                control_enabled = false,
                source = {
                    kind = "MidiDeviceChanges",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "BrowsePotPresets",
                },
            },
            {
                id = "Kw572-DOMjW2-lAX-lTks",
                name = "Preview preset",
                group = "tRUhYa0RGcup1XIyV3zvr",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "play",
                    character = "Button",
                },
                glue = {
                    step_size_interval = {0.01, 0.05},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "PreviewPotPreset",
                },
            },
            {
                id = "a_OaKe1Uo_FATkupnHi1z",
                name = "Load preset",
                group = "tRUhYa0RGcup1XIyV3zvr",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "ch1/v-select",
                    character = "Button",
                },
                glue = {
                    step_size_interval = {0.01, 0.05},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "LoadPotPreset",
                    fx = {
                        address = "ByIndex",
                        chain = {
                            address = "Track",
                            track = {
                                address = "ById",
                                id = "B63E9F43-C847-FA42-A5DD-EAE998F42C78",
                            },
                        },
                        index = 1,
                    },
                },
            },
            {
                id = "IEVDSWvucmp0a60j0NSeG",
                name = "Show FX on load",
                group = "tRUhYa0RGcup1XIyV3zvr",
                feedback_enabled = false,
                source = {
                    kind = "Virtual",
                    id = "ch1/v-select",
                    character = "Button",
                },
                glue = {
                    target_interval = {1, 1},
                    step_size_interval = {0.01, 0.05},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "FxVisibility",
                    fx = {
                        address = "ByIndex",
                        chain = {
                            address = "Track",
                            track = {
                                address = "ById",
                                id = "B63E9F43-C847-FA42-A5DD-EAE998F42C78",
                            },
                        },
                        index = 1,
                    },
                },
            },
            {
                id = "kuUh62ZJXpQa9AS_s9WvM",
                name = "Display param name",
                group = "tRUhYa0RGcup1XIyV3zvr",
                source = {
                    kind = "MackieLcd",
                    channel = 0,
                    line = 0,
                },
                glue = {
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    feedback = {
                        kind = "Text",
                        text_expression = "{{ target.fx_parameter.name }}",
                    },
                },
                target = {
                    kind = "LastTouched",
                    included_targets = {
                        "RoutePan",
                        "TrackMuteState",
                        "TrackAutomationMode",
                        "TrackArmState",
                        "TrackVolume",
                        "TrackSoloState",
                        "TrackMonitoringMode",
                        "FxOnOffState",
                        "TrackPan",
                        "FxParameterValue",
                        "AutomationModeOverride",
                        "RouteVolume",
                        "BrowseFxPresets",
                        "PlayRate",
                        "TrackSelectionState",
                        "Tempo",
                    },
                },
            },
            {
                id = "VJjcCHS4L9XXAEZaggw_t",
                name = "Display param value",
                group = "tRUhYa0RGcup1XIyV3zvr",
                source = {
                    kind = "MackieLcd",
                    channel = 0,
                    line = 1,
                },
                glue = {
                    step_size_interval = {0.01, 0.05},
                    step_factor_interval = {1, 5},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "LastTouched",
                    included_targets = {
                        "TrackSoloState",
                        "TrackSelectionState",
                        "PlayRate",
                        "RouteVolume",
                        "TrackArmState",
                        "Tempo",
                        "TrackMuteState",
                        "TrackPan",
                        "RoutePan",
                        "AutomationModeOverride",
                        "TrackMonitoringMode",
                        "TrackVolume",
                        "FxParameterValue",
                        "TrackAutomationMode",
                        "BrowseFxPresets",
                        "FxOnOffState",
                    },
                },
            },
            {
                id = "OCsMy1ZHN-pNVtrHPHUqD",
                name = "Display",
                group = "5A6qajMq6KFFVs8sVEIGV",
                control_enabled = false,
                source = {
                    kind = "MackieSevenSegmentDisplay",
                    scope = "Tc",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                        text_expression = "{{ target.item.name }}",
                    },
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "SubBank",
                },
            },
            {
                id = "YCoIZWojUdlV4HLG_rpJu",
                name = "Display",
                group = "zbTTUwkU-D-xslTQjGa3m",
                control_enabled = false,
                source = {
                    kind = "MackieSevenSegmentDisplay",
                    scope = "Tc",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Bank",
                },
            },
            {
                id = "_NzMo9-xSF5LUmblvGLBJ",
                name = "Display",
                group = "EFpF3FW5Lp6k6jKBSh6_I",
                control_enabled = false,
                source = {
                    kind = "MackieSevenSegmentDisplay",
                    scope = "Tc",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                        text_expression = "{{ target.item.name }}",
                    },
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "SubCategory",
                },
            },
            {
                id = "m9DAsw94DscrY0TO0n3Fn",
                name = "Display",
                group = "lpnoZyrYUdSappcySl38d",
                control_enabled = false,
                source = {
                    kind = "MackieSevenSegmentDisplay",
                    scope = "Tc",
                },
                glue = {
                    absolute_mode = "IncrementalButton",
                    step_size_interval = {0.000041237113402061855, 0.000041237113402061855},
                    feedback = {
                        kind = "Text",
                    },
                },
                target = {
                    kind = "BrowsePotFilterItems",
                    item_kind = "Category",
                },
            },
        },
    },
}

Main presets for Playtime control

Multi-channel DAW controller to Playtime

This preset makes it possible to use multi-channel DAW controllers (e.g. iCON Platform M+) to control Playtime.

-- Constants

local channel_count = 8

-- Utility functions

--- Converts the given key-value table to an array table.
function to_array(t)
    local array = {}
    for _, v in pairs(t) do
        table.insert(array, v)
    end
    return array
end

--- Returns a new table that's the given table turned into an array
--- and sorted by the `index` key.
function sorted_by_index(t)
    local sorted = to_array(t)
    local compare_index = function(left, right)
        return left.index < right.index
    end
    table.sort(sorted, compare_index)
    return sorted
end

--- Clones a table.
function clone(t)
    local new_table = {}
    for k, v in pairs(t) do
        new_table[k] = v
    end
    return new_table
end

--- 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".
function merged(t1, t2)
    local result = clone(t1)
    for key, new_value in pairs(t2) 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)
        else
            -- Simple use new value
            result[key] = new_value
        end
    end
    return make_mergeable(result)
end

--- Makes it possible to merge this table with another one via "+" operator.
function make_mergeable(t)
    local metatable = {
        __add = merged
    }
    setmetatable(t, metatable)
    return t
end

function PartialMapping(t)
    return make_mergeable(t)
end

-- Parameters

local params = {
    column_offset = {
        index = 0,
        name = "Column offset",
        value_count = 10000,
    },
    row_offset = {
        index = 1,
        name = "Row offset",
        value_count = 10000,
    },
}

-- Domain functions

function current_slot(channel_index)
    return {
        address = "Dynamic",
        column_expression = "p[0] + " .. channel_index,
        row_expression = "p[1]"
    }
end

function current_column(channel_index)
    return {
        address = "Dynamic",
        expression = "p[0] + " .. channel_index,
    }
end

function button(id)
    return PartialMapping {
        source = {
            kind = "Virtual",
            character = "Button",
            id = id,
        },
    }
end

function multi(id)
    return PartialMapping {
        source = {
            kind = "Virtual",
            character = "Multi",
            id = id,
        },
    }
end

function transport_action(action)
    return PartialMapping {
        target = {
            kind = "TransportAction",
            action = action,
        },
    }
end

function clip_transport_action(action, channel_index)
    return PartialMapping {
        target = {
            kind = "ClipTransportAction",
            slot = current_slot(channel_index),
            action = action,
            record_only_if_track_armed = true,
            stop_column_if_slot_empty = true,
        },
    }
end

function clip_name_feedback(channel_index)
    return PartialMapping {
        control_enabled = false,
        glue = {
            feedback = {
                kind = "Text",
                text_expression = "{{ target.clip.name }}"
            },
        },
        target = {
            kind = "ClipManagement",
            slot = current_slot(channel_index),
            action = {
                kind = "EditClip",
            },
        },
    }
end

function clip_position_feedback(channel_index)
    return PartialMapping {
        control_enabled = false,
        target = {
            kind = "ClipSeek",
            slot = current_slot(channel_index),
            -- TODO-high-playtime-after-release The Platform M+ seems to don't like resolution High (skips messages). Maybe time for #533.
            feedback_resolution = "Beat",
        },
    }
end

function clip_volume(channel_index)
    return {
        target = {
            kind = "ClipVolume",
            slot = current_slot(channel_index),
        },
    }
end

function toggle()
    return PartialMapping {
        glue = {
            absolute_mode = "ToggleButton",
        },
    }
end

function scroll_horizontally(amount)
    return scroll(params.column_offset.index, amount)
end

function scroll_vertically(amount)
    return scroll(params.row_offset.index, amount)
end

function scroll(param_index, amount)
    local abs_amount = math.abs(amount)
    return {
        glue = {
            absolute_mode = "IncrementalButton",
            step_factor_interval = { abs_amount, abs_amount },
            reverse = amount < 0,
            feedback = {
                kind = "Numeric",
                transformation = "x = 1",
            },
        },
        target = {
            kind = "FxParameterValue",
            parameter = {
                address = "ById",
                index = param_index,
            },
        },
    }
end

function channel_button(channel_index, id)
    return button("ch" .. (channel_index + 1) .. "/" .. id)
end

function channel_multi(channel_index, id)
    return multi("ch" .. (channel_index + 1) .. "/" .. id)
end

function control_off()
    return PartialMapping {
        control_enabled = false,
    }
end

function feedback_off()
    return PartialMapping {
        feedback_enabled = false,
    }
end

function column_track_target(channel_index, target_kind, exclusive)
    return {
        glue = {
            absolute_mode = "ToggleButton",
        },
        target = {
            kind = target_kind,
            track = {
                address = "FromClipColumn",
                column = current_column(channel_index),
                context = "Playback",
            },
            exclusivity = exclusive and "WithinFolderOnOnly",
        },
    }
end

-- Mappings

local mappings = {
    button("play") + toggle() + transport_action("PlayPause"),
    button("stop") + transport_action("Stop"),
    button("cursor-left") + scroll_horizontally(-1),
    button("cursor-right")+ scroll_horizontally(1),
    button("cursor-up") + scroll_vertically(-1),
    button("cursor-down") + scroll_vertically(1),
}

for ch = 0, channel_count - 1 do
    table.insert(mappings, channel_button(ch, "v-select") + toggle() + feedback_off() + clip_transport_action("RecordPlayStop", ch))
    table.insert(mappings, channel_button(ch, "select") + toggle() + control_off() + clip_transport_action("RecordPlayStop", ch))
    table.insert(mappings, channel_multi(ch, "fader") + clip_position_feedback(ch))
    table.insert(mappings, channel_multi(ch, "v-pot") + clip_volume(ch))
    table.insert(mappings, channel_button(ch, "mute") + column_track_target(ch, "TrackMuteState"))
    table.insert(mappings, channel_button(ch, "solo") + column_track_target(ch, "TrackSoloState"))
    table.insert(mappings, channel_button(ch, "record-ready") + column_track_target(ch, "TrackArmState", true))
end

-- Result

return {
    kind = "MainCompartment",
    value = {
        parameters = sorted_by_index(params),
        mappings = mappings,
    },
}

One-channel DAW controller to Playtime

This preset makes it possible to use one-channel DAW controllers (e.g. X-Touch One) to control Playtime.

-- Utility functions

--- Converts the given key-value table to an array table.
function to_array(t)
    local array = {}
    for _, v in pairs(t) do
        table.insert(array, v)
    end
    return array
end

--- Returns a new table that's the given table turned into an array
--- and sorted by the `index` key.
function sorted_by_index(t)
    local sorted = to_array(t)
    local compare_index = function(left, right)
        return left.index < right.index
    end
    table.sort(sorted, compare_index)
    return sorted
end

--- Clones a table.
function clone(t)
    local new_table = {}
    for k, v in pairs(t) do
        new_table[k] = v
    end
    return new_table
end

--- 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".
function merged(t1, t2)
    local result = clone(t1)
    for key, new_value in pairs(t2) 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)
        else
            -- Simple use new value
            result[key] = new_value
        end
    end
    return make_mergeable(result)
end

--- Makes it possible to merge this table with another one via "+" operator.
function make_mergeable(t)
    local metatable = {
        __add = merged
    }
    setmetatable(t, metatable)
    return t
end

function PartialMapping(t)
    return make_mergeable(t)
end

-- Parameters

local params = {
    column_offset = {
        index = 0,
        name = "Column offset",
        value_count = 10000,
    },
    row_offset = {
        index = 1,
        name = "Row offset",
        value_count = 10000,
    },
}

-- Domain functions

function current_slot()
    return PartialMapping {
        address = "Dynamic",
        column_expression = "p[0]",
        row_expression = "p[1]"
    }
end

function button(id)
    return PartialMapping {
        source = {
            kind = "Virtual",
            character = "Button",
            id = id,
        },
    }
end

function multi(id)
    return PartialMapping {
        source = {
            kind = "Virtual",
            character = "Multi",
            id = id,
        },
    }
end

function clip_transport_action(action)
    return PartialMapping {
        target = {
            kind = "ClipTransportAction",
            slot = current_slot(),
            action = action,
            record_only_if_track_armed = true,
            stop_column_if_slot_empty = true,
        },
    }
end

function clip_name_feedback()
    return PartialMapping {
        control_enabled = false,
        glue = {
            feedback = {
                kind = "Text",
                text_expression = "{{ target.clip.name }}"
            },
        },
        target = {
            kind = "ClipManagement",
            slot = current_slot(),
            action = "EditClip",
        },
    }
end

function clip_position_text_feedback()
    return PartialMapping {
        control_enabled = false,
        glue = {
            feedback = {
                kind = "Text",
            },
        },
        target = {
            kind = "ClipSeek",
            slot = current_slot(),
            feedback_resolution = "High",
        },
    }
end

function clip_position_numeric_feedback()
    return PartialMapping {
        control_enabled = false,
        target = {
            kind = "ClipSeek",
            slot = current_slot(),
            feedback_resolution = "High",
        },
    }
end

function clip_volume()
    return {
        target = {
            kind = "ClipVolume",
            slot = current_slot(),
        },
    }
end

function press_only()
    return PartialMapping {
        glue = {
            button_filter = "PressOnly",
        },
    }
end

function toggle()
    return PartialMapping {
        glue = {
            absolute_mode = "ToggleButton",
        },
    }
end

function scroll_horizontally(amount)
    return scroll(params.column_offset.index, amount)
end

function scroll_vertically(amount)
    return scroll(params.row_offset.index, amount)
end

function scroll(param_index, amount)
    local abs_amount = math.abs(amount)
    return {
        glue = {
            absolute_mode = "IncrementalButton",
            step_factor_interval = { abs_amount, abs_amount },
            reverse = amount < 0,
            feedback = {
                kind = "Numeric",
                transformation = "x = 1",
            },
        },
        target = {
            kind = "FxParameterValue",
            parameter = {
                address = "ById",
                index = param_index,
            },
        },
    }
end

-- Mappings

local mappings = {
    button("cycle") + toggle() + clip_transport_action("Looped"),
    button("stop") + clip_transport_action("Stop"),
    button("play") + press_only() + clip_transport_action("PlayStop"),
    button("record") + toggle() + clip_transport_action("RecordStop"),
    button("cursor-left") + scroll_horizontally(-1),
    button("cursor-right")+ scroll_horizontally(1),
    button("cursor-up") + scroll_vertically(-1),
    button("cursor-down") + scroll_vertically(1),
    --multi("ch1/fader") + clip_volume(),
    multi("ch1/fader") + clip_position_numeric_feedback(),
    multi("ch1/lcd/line1") + clip_name_feedback(),
    multi("lcd/timecode") + clip_position_text_feedback(),
}

-- Result

return {
    kind = "MainCompartment",
    value = {
        parameters = sorted_by_index(params),
        mappings = mappings,
    },
}

Grid to Playtime

This preset makes it possible to use a grid controller (e.g. Launchpad) to control Playtime.

-- Constants

local column_count = 4
local row_count = 4
local mode_count = 100

-- Utilities

function set_keys_as_ids(t)
    for key, value in pairs(t) do
        value.id = key
    end
end

function to_array(t)
    local array = {}
    for _, v in pairs(t) do
        table.insert(array, v)
    end
    return array
end

function sorted_by_index(t)
    local sorted = to_array(t)
    local compare_index = function(left, right)
        return left.index < right.index
    end
    table.sort(sorted, compare_index)
    return sorted
end

-- Modes

local modes = {
    -- OK Record/play/stop clips (push)
    -- OK Set clip volume (turn)
    normal = {
        index = 0,
        label = "Normal",
    },
    -- OK Undo/redo
    -- OK Stop all
    -- OK Play
    -- OK Click
    -- Cycle through normal mode knob functions (vol, pan, send)
    global = {
        index = 1,
        label = "Global",
        button = "bank-left",
    },
    -- OK Solo/arm/mute/select column tracks
    track = {
        index = 2,
        label = "Track",
        button = "cursor-left",
    },
    -- OK Delete clip (long press)
    -- OK Quantize clip (double press)
    slot = {
        index = 3,
        label = "Slot",
        button = "ch-left",
    },
    -- OK Scroll horizontally
    -- OK Scroll vertically
    nav = {
        index = 4,
        label = "Navigation",
        button = "bank-right",
    },
    -- OK Stop column
    column = {
        index = 5,
        label = "Column",
        button = "cursor-right",
    },
}
local sorted_modes = sorted_by_index(modes)
local mode_labels = {}
for _, mode in ipairs(sorted_modes) do
    table.insert(mode_labels, mode.label)
end

-- Parameters

local params = {
    column_offset = {
        index = 0,
        name = "Column offset",
        value_count = 10000,
    },
    row_offset = {
        index = 1,
        name = "Row offset",
        value_count = 10000,
    },
    mode = {
        index = 2,
        name = "Mode",
        value_count = mode_count,
        value_labels = mode_labels,
    },
}
-- Groups

function mode_is(mode_index)
    return {
        kind = "Bank",
        parameter = params.mode.index,
        bank_index = mode_index,
    }
end

function display_slot_feedback_condition()
    return {
        kind = "Expression",
        condition = "p[2] == 0 || p[2] == 4 || p[2] == 3",
    }
end

local groups = {
    slot_state_feedback = {
        name = "Slot state feedback",
        activation_condition = display_slot_feedback_condition(),
    },
    clip_play = {
        name = "Clip play",
        activation_condition = mode_is(modes.normal.index),
    },
    clip_volume = {
        name = "Clip volume",
        activation_condition = mode_is(modes.normal.index),
    },
    clip_pos_feedback = {
        name = "Clip position feedback",
        activation_condition = display_slot_feedback_condition(),
    },
    global = {
        name = modes.global.label,
        activation_condition = mode_is(modes.global.index),
    },
    nav = {
        name = modes.nav.label,
        activation_condition = mode_is(modes.nav.index),
    },
    track = {
        name = modes.track.label,
        activation_condition = mode_is(modes.track.index),
    },
    slot = {
        name = modes.slot.label,
        activation_condition = mode_is(modes.slot.index),
    },
    column = {
        name = modes.column.label,
        activation_condition = mode_is(modes.column.index),
    },
}
set_keys_as_ids(groups)

-- Domain functions

function create_cell_id(col, row, id)
    return "col" .. (col + 1) .. "/row" .. (row + 1) .. "/" .. id
end

function create_coordinate_expression(param, index)
    return "p[" .. param .. "] + " .. index
end

function create_col_expression(col)
    return create_coordinate_expression(params.column_offset.index, col)
end

function create_row_expression(row)
    return create_coordinate_expression(params.row_offset.index, row)
end

function create_slot_selector(col, row)
    return {
        address = "Dynamic",
        column_expression = create_col_expression(col),
        row_expression = create_row_expression(row)
    }
end

function clip_play(col, row)
    return {
        name = "Rec/play",
        group = groups.clip_play.id,
        feedback_enabled = false,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        glue = {
            absolute_mode = "ToggleButton",
        },
        target = {
            kind = "ClipTransportAction",
            slot = create_slot_selector(col, row),
            action = "RecordPlayStop",
            record_only_if_track_armed = true,
            stop_column_if_slot_empty = true,
        },
    }
end

function clip_volume(col, row)
    return {
        name = "Vol",
        group = groups.clip_volume.id,
        feedback_enabled = false,
        source = {
            kind = "Virtual",
            character = "Multi",
            id = create_cell_id(col, row, "knob"),
        },
        target = {
            kind = "ClipVolume",
            slot = create_slot_selector(col, row),
        },
    }
end

function slot_state_feedback(col, row)
    return {
        group = groups.slot_state_feedback.id,
        control_enabled = false,
        visible_in_projection = false,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        glue = {
            feedback = {
                kind = "Text",
                text_expression = "{{ target.slot_state.id }}",
            },
        },
        target = {
            kind = "ClipTransportAction",
            slot = create_slot_selector(col, row),
            action = "PlayStop",
        },
    }
end

function clip_position_feedback(col, row)
    return {
        group = groups.clip_pos_feedback.id,
        visible_in_projection = false,
        control_enabled = false,
        source = {
            kind = "Virtual",
            character = "Multi",
            id = create_cell_id(col, row, "knob"),
        },
        target = {
            kind = "ClipSeek",
            slot = create_slot_selector(col, row),
            feedback_resolution = "High",
        },
    }
end

function global_matrix_button(button_id, matrix_action, name)
    return {
        name = name or matrix_action,
        activation_condition = mode_is(modes.global.index),
        source = {
            kind = "Virtual",
            id = button_id,
            character = "Button",
        },
        target = {
            kind = "ClipMatrixAction",
            action = matrix_action,
        },
    }
end

function global_transport_button(button_id, transport_action, absolute_mode)
    return {
        activation_condition = mode_is(modes.global.index),
        source = {
            kind = "Virtual",
            id = button_id,
            character = "Button",
        },
        glue = {
            absolute_mode = absolute_mode,
        },
        target = {
            kind = "TransportAction",
            action = transport_action,
        },
    }
end

function global_reaper_action_button(button_id, command_id, name)
    return {
        name = name,
        activation_condition = mode_is(modes.global.index),
        source = {
            kind = "Virtual",
            id = button_id,
            character = "Button",
        },
        target = {
            kind = "ReaperAction",
            command = command_id,
            invocation = "Trigger",
        },
    }
end

function scroll(multi_id, offset_param_index, name)
    return {
        name = name,
        group = groups.nav.id,
        source = {
            kind = "Virtual",
            id = multi_id,
            character = "Multi",
        },
        glue = {
            step_factor_interval = { -3, -3 },
        },
        target = {
            kind = "FxParameterValue",
            parameter = {
                address = "ById",
                index = offset_param_index,
            },
        },
    }
end

function clip_delete(col, row)
    return {
        name = "Quantize/delete",
        group = groups.slot.id,
        feedback_enabled = false,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        glue = {
            fire_mode = {
                kind = "AfterTimeout",
                timeout = 1000,
            },
        },
        target = {
            kind = "ClipManagement",
            slot = create_slot_selector(col, row),
            action = "ClearSlot",
        },
    }
end

function track_toggle_target(col, row, target_kind, name, exclusive)
    return {
        name = name,
        group = groups.track.id,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        glue = {
            absolute_mode = "ToggleButton",
        },
        target = {
            kind = target_kind,
            track = {
                address = "FromClipColumn",
                column = {
                    address = "Dynamic",
                    expression = create_col_expression(col),
                },
                context = "Playback",
            },
            exclusivity = exclusive and "WithinFolderOnOnly",
        },
    }
end

function column_action(col, row, action)
    return {
        name = action,
        group = groups.column.id,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        target = {
            kind = "ClipColumnAction",
            column = {
                address = "Dynamic",
                expression = create_col_expression(col),
            },
            action = action
        },
    }
end

function clip_quantize(col, row)
    return {
        group = groups.slot.id,
        visible_in_projection = false,
        feedback_enabled = false,
        source = {
            kind = "Virtual",
            character = "Button",
            id = create_cell_id(col, row, "pad"),
        },
        glue = {
            absolute_mode = "ToggleButton",
            fire_mode = {
                kind = "OnDoublePress",
            },
        },
        target = {
            kind = "ClipManagement",
            slot = create_slot_selector(col, row),
            action = "EditClip",
        },
    }
end

-- TODO-high-playtime-after-release Make short press toggle and long press be momentary.
--  Problem 1: "Fire after timeout" somehow doesn't have an effect.
--  Problem 2: "Fire after timeout" doesn't switch off when button released (in new versions it does!)
function mode_button(button_id, mode)
    local target_value = mode.index / (mode_count - 1)
    return {
        name = mode.label,
        feedback_enabled = false,
        source = {
            kind = "Virtual",
            id = button_id,
            character = "Button",
        },
        glue = {
            target_interval = { 0, target_value }
        },
        target = {
            kind = "FxParameterValue",
            parameter = {
                address = "ById",
                index = params.mode.index,
            },
        },
    }
end

-- Content

local mappings = {
    global_matrix_button("col4/row4/pad", "Stop", "Stop all clips"),
    global_matrix_button("col3/row1/pad", "Undo"),
    global_matrix_button("col4/row1/pad", "Redo"),
    global_transport_button("col4/row3/pad", "PlayPause", "ToggleButton"),
    global_transport_button("col3/row3/pad", "Stop", "Normal"),
    global_reaper_action_button("col4/row2/pad", 40364, "Click"),
    scroll("col1/row1/knob", params.column_offset.index, "Scroll left/right"),
    scroll("col1/row2/knob", params.row_offset.index, "Scroll up/down"),
}

-- Mode buttons

for _, mode in pairs(modes) do
    if mode.button then
        table.insert(mappings, mode_button(mode.button, mode))
    end
end

-- Grid

for col = 0, column_count - 1 do
    table.insert(mappings, track_toggle_target(col, 0, "TrackSoloState", "Solo"))
    table.insert(mappings, track_toggle_target(col, 1, "TrackArmState", "Arm", true))
    table.insert(mappings, track_toggle_target(col, 2, "TrackMuteState", "Mute"))
    table.insert(mappings, track_toggle_target(col, 3, "TrackSelectionState", "Select", true))
    table.insert(mappings, column_action(col, 3, "Stop"))
    for row = 0, row_count - 1 do
        table.insert(mappings, clip_play(col, row))
        table.insert(mappings, clip_volume(col, row))
        table.insert(mappings, clip_delete(col, row))
        table.insert(mappings, clip_quantize(col, row))
        table.insert(mappings, slot_state_feedback(col, row))
        table.insert(mappings, clip_position_feedback(col, row))
    end
end

-- Result

return {
    kind = "MainCompartment",
    value = {
        parameters = sorted_by_index(params),
        groups = to_array(groups),
        mappings = mappings,
    },
}

TouchOSC to Playtime

This preset makes it possible to use the TouchOSC template "Simple Mk2 / Matrix" to control Playtime.

-- TouchOSC: Simple Mk2 / Matrix

local mappings = {}

local feedback_value_table = {
    kind = "FromTextToContinuous",
    value = {
        ["playtime.slot_state.empty"] = 0.0,
        ["playtime.slot_state.stopped"] = 0.2,
        ["playtime.slot_state.scheduled_for_play_start"] = 0.4,
        ["playtime.slot_state.playing"] = 1.0,
        ["playtime.slot_state.paused"] = 0.3,
        ["playtime.slot_state.scheduled_for_play_stop"] = 0.4,
        ["playtime.slot_state.scheduled_for_play_restart"] = 0.4,
        ["playtime.slot_state.scheduled_for_record_start"] = 0.4,
        ["playtime.slot_state.recording"] = 1.0,
        ["playtime.slot_state.scheduled_for_record_stop"] = 0.4,
    }
}

function multitoggle_button(index)
    return {
        kind = "Osc",
        address = "/4/multitoggle/" .. index,
        argument = {
            index = 0,
        },
    }
end

for col = 0, 7 do
    for row = 0, 7 do
        local mapping = {
            source = multitoggle_button(row * 8 + col + 1),
            glue = {
                -- Standard TouchOSC template tries to be too clever and toggles itself :(
                -- So we use a toggle-always transformation.
                absolute_mode = "Normal",
                control_transformation = "y = y > 0.5 ? 0 : 1",
                feedback = {
                    kind = "Text",
                    text_expression = "{{ target.slot_state.id }}",
                },
                feedback_value_table = feedback_value_table,
            },
            target = {
                kind = "ClipTransportAction",
                slot = {
                    address = "ByIndex",
                    column_index = col,
                    row_index = row,
                },
                action = "RecordPlayStop",
                record_only_if_track_armed = true,
                stop_column_if_slot_empty = true,
            },
        }
        table.insert(mappings, mapping)
    end
end

return {
    kind = "MainCompartment",
    value = {
        mappings = mappings,
    },
}

QWERTY to Playtime

This preset makes it possible to use your QWERTY keyboard to control Playtime.

local column_key_sets = {
    { "1", "q", "a", "z", },
    { "2", "w", "s", "x", },
    { "3", "e", "d", "c", },
    { "4", "r", "f", "v", },
    { "5", "t", "g", "b", },
    { "6", "y", "h", "n", },
    { "7", "u", "j", "m", },
    { "8", "i", "k", "comma", },
}

function scene_play(key, row_index)
    return {
        source = {
            kind = "Virtual",
            character = "Button",
            id = "key/" .. key,
        },
        target = {
            kind = "ClipRowAction",
            row = {
                address = "ByIndex",
                index = row_index,
            },
            action = "PlayScene",
        },
    }
end

local mappings = {
    scene_play("9", 0),
    scene_play("o", 1),
    scene_play("l", 2),
    scene_play("period", 3),
}



for col, column_key_set in ipairs(column_key_sets) do
    for row, row_key in ipairs(column_key_set) do
        local mapping = {
            source = {
                kind = "Virtual",
                character = "Button",
                id = "key/" .. row_key,
            },
            glue = {
                --absolute_mode = "Toggle",
            },
            target = {
                kind = "ClipTransportAction",
                slot = {
                    address = "ByIndex",
                    column_index = col - 1,
                    row_index = row - 1,
                },
                action = "PlayStop",
                stop_column_if_slot_empty = true,
                play_start_timing = {
                    kind = "Immediately"
                },
                play_stop_timing = {
                    kind = "Immediately"
                },
            },
        }
        table.insert(mappings, mapping)
    end
end

return {
    kind = "MainCompartment",
    value = {
        mappings = mappings,
    },
}
Clone this wiki locally