AutoHotKey

SMOKING HOT KEYS.

My personal collection of AutoHotKey hacks. Gives you back ALT GR = ALT + CTRL, turns your keyboard into a MIDI controller, gives you an automatic save reminder and much more. Works on Windows only.

1 Code snippets to steal
Download Everything
AutoHotkey drop-in_fixes/Autosave_Reminder.ahk
; AUTOSAVE REMINDER
; -> Nudges you to save regularly while Ableton is running. CTRL+S resets the timer. Snoozes automatically when Melodyne is open.

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

; Reminder cadence.
global AutoSaveInterval := 30 * 60 * 1000 ; 30 min = 30 * 60 * 1000 ms
global AutoSaveShortInterval := 10 * 60 * 1000 ; 10 min = 10 * 60 * 1000 ms

; Runtime state.
global AutoSavePopupActive := false
global WaitingForAbletonActive := false
global AutoSaveGui := ""

SetTimer(CheckAbletonAndSetTimer, 1000)

CheckAbletonAndSetTimer() {
    global AutoSaveInterval
    if LiveHack_AbletonWindowExists()
    {
        SetTimer(AutoSaveReminder, AutoSaveInterval)
        SetTimer(CheckAbletonAndSetTimer, 0)
    }
}

AutoSaveReminder() {
    global WaitingForAbletonActive
    SetTimer(AutoSaveReminder, 0)
    WaitingForAbletonActive := true
    SetTimer(WaitForAbletonActive, 1000)
}

WaitForAbletonActive() {
    global WaitingForAbletonActive
    if (!WaitingForAbletonActive)
        return

    if (LiveHack_IsAbletonOrPluginActive() and not LiveHack_IsMelodyneActive())
    {
        WaitingForAbletonActive := false
        SetTimer(WaitForAbletonActive, 0)
        ShowSavePopup()
        return
    }

    if not LiveHack_AbletonWindowExists()
    {
        WaitingForAbletonActive := false
        SetTimer(WaitForAbletonActive, 0)
        SetTimer(CheckAbletonAndSetTimer, 1000)
    }
}

ShowSavePopup() {
    global AutoSavePopupActive, AutoSaveGui
    if (!AutoSavePopupActive)
    {
        AutoSavePopupActive := true
        AutoSaveGui := Gui("+AlwaysOnTop +ToolWindow", "LiveHack: Auto Save")
        AutoSaveGui.MarginX := 14
        AutoSaveGui.MarginY := 14
        AutoSaveGui.SetFont("s10", "Segoe UI")
        AutoSaveGui.Add("Text", , "Time to save your Ableton project.")
        AutoSaveGui.Add("Text", "y+6", "Save now or snooze for 10 minutes.")
        AutoSaveGui.Add("Button", "xm y+14 w100 Default", "Save now").OnEvent("Click", AutoSave_DoSave)
        AutoSaveGui.Add("Button", "x+10 w120", "Remind me later").OnEvent("Click", AutoSave_Later)
        AutoSaveGui.OnEvent("Close", AutoSave_Later)
        AutoSaveGui.OnEvent("Escape", AutoSave_Later)
        AutoSaveGui.Show()
    }
}

AutoSave_DoSave(*) {
    global AutoSavePopupActive, AutoSaveGui, AutoSaveInterval
    AutoSaveGui.Destroy()
    AutoSavePopupActive := false

    if LiveHack_AbletonWindowExists()
    {
        WinActivate("ahk_class Ableton Live Window Class")
        Send("^s")
    }

    SetTimer(AutoSaveReminder, AutoSaveInterval)
}

AutoSave_Later(*) {
    global AutoSavePopupActive, AutoSaveGui, AutoSaveShortInterval
    AutoSaveGui.Destroy()
    AutoSavePopupActive := false
    SetTimer(AutoSaveReminder, AutoSaveShortInterval)
}

#HotIf LiveHack_IsAbletonOrPluginActive()

$^s:: {
    global AutoSavePopupActive, AutoSaveGui, WaitingForAbletonActive, AutoSaveInterval
    Send("^s")
    SetTimer(AutoSaveReminder, 0)
    SetTimer(AutoSaveReminder, AutoSaveInterval)

    if (AutoSavePopupActive)
    {
        AutoSaveGui.Destroy()
        AutoSavePopupActive := false
    }

    if (WaitingForAbletonActive)
    {
        WaitingForAbletonActive := false
        SetTimer(WaitForAbletonActive, 0)
    }
}

#HotIf

LiveHack_AbletonWindowExists() {
    return WinExist("ahk_class Ableton Live Window Class")
}

LiveHack_IsAbletonOrPluginActive() {
    return (
        WinActive("ahk_class Ableton Live Window Class")
        or WinActive("ahk_class Vst3PlugWindow")
        or WinActive("ahk_class AbletonVstPlugClass")
    )
}

LiveHack_IsMelodyneActive() {
    return WinActive("Melodyne")
}

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey drop-in_fixes/Get_Back_ALT_GR.ahk
; GET BACK ALT GR
; -> Remaps ALT GR to CTRL+ALT so you can perform the CTRL+ALT+U hotkey with one hand.
; !! Only relevant for some international keyboard layouts. You know if this is for you.

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)


#HotIf ((WinActive("ahk_class Ableton Live Window Class") or WinActive("ahk_class Vst3PlugWindow") or WinActive("ahk_class AbletonVstPlugClass")) and not WinActive("Melodyne"))
; this block is now only active in Ableton Live, unless you use Melodyne, where you don't need this remapping.

*RAlt:: {
    Send("{LAlt down}")
    Send("{LControl down}")
}

*RAlt up:: {
    Send("{LAlt up}")
    Send("{LControl up}")
}

#HotIf

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey drop-in_fixes/Make_Better_Use_of_F-Keys.ahk
; MAKE BETTER USE OF F-KEYS
; -> Reassigns F1-F12 in Ableton to genuinely useful actions instead of the default track activators.
; !! Tag your favourite plugins with "AutoHotKey" in Ableton's browser.


; ------------------------------------------------------------------
; WHY OVERRIDE THE F-KEYS?
; Ableton's default F-key assignments mute tracks 1-8. 
; In a real production session with 100+ tracks, this is basically useless.
; Let's use these easily accessible keys for something actually helpful:
;
; WHAT THIS SCRIPT DOES:
; F1-F4        → Insert your most-used plugins instantly (via browser search)
; Ctrl+F1-F4   → Insert more plugins.
; F5-F8        → Assign clip colours with one keypress
; Ctrl+F5-F8   → More clip colour options
; F9           → Record (passthrough)
; F10          → Toggle loop
; F12          → (passthrough, Ableton default)
; F13-F18      → Extended shortcuts for browser, key map, view toggles etc.
;               (requires a keyboard or macro pad that sends F13+)
;
; ------------------------------------------------------------------

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

#HotIf WinActive("ahk_class Ableton Live Window Class") and not WinActive("Melodyne")

; ------------------------------------------------------------------
; F1-F4: INSERT YOUR MOST-USED PLUGINS
; Tag your plugins with "AutoHotKey" in Ableton's browser for one-key loading.
; Customize these to match YOUR workflow — the plugin names below are examples.
; ------------------------------------------------------------------

; F1 → Insert Compressor (Ableton built-in)
F1::AddTaggedPlugin("AutoHotKey", "Compressor", 0)
; Ctrl+F1 → Insert Pro-C 2 (third-party alternative)
^F1::AddTaggedPlugin("AutoHotKey", "Pro-C 2", 0)

; F2 → Insert Custom Audio or Effect Rack
F2::AddTaggedPlugin("AutoHotKey", "Custom Audio or Effect Rack.adg", 0)
; Ctrl+F2 → Insert OTT preset variant
^F2::AddTaggedPlugin("AutoHotKey", "OTT.adv", 0)

; F3 → Insert EQ Eight (Ableton built-in)
F3::AddTaggedPlugin("AutoHotKey", "EQ Eight", 0)
; Ctrl+F3 → Insert Pro-Q 4 (third-party alternative)
^F3::AddTaggedPlugin("AutoHotKey", "Pro-Q 4", 0)

; F4 → Insert Utility (Ableton built-in)
F4::AddTaggedPlugin("AutoHotKey", "Utility", 0)
; Ctrl+F4 → Insert Phase Invert
^F4::AddTaggedPlugin("AutoHotKey", "Phase Invert", 0)

; If a give search query gives you more than one result,
; then the number downArrow sends that many down arrow key strokes before hitting enter.

; ------------------------------------------------------------------
; F5-F8: ASSIGN CLIP COLOURS
; One-keypress colour assignment for the selected clip.
; These navigate the right-click colour menu. Colours are position-based
; and may shift if Ableton changes the palette in a future update.
; ------------------------------------------------------------------

; F5 → Full Green
F5::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Right}{Right}{Right}{Right}{Right}{Enter}")
; F6 → Light Green
F6::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Up}{Right}{Right}{Right}{Right}{Right}{Enter}")
; F7 → Yellow
F7::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Up}{Right}{Right}{Right}{Right}{Enter}")
; F8 → Magenta
F8::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Up}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Enter}")

; Ctrl+F5 → Dark Green
^F5::DoBlockInputSend("{AppsKey}{End}{Up}{Right}{Right}{Right}{Right}{Right}{Enter}")
; Ctrl+F6 → Turquoise
^F6::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Right}{Right}{Right}{Right}{Right}{Right}{Right}{Enter}")
; Ctrl+F7 → White
^F7::DoBlockInputSend("{AppsKey}{End}{Up}{Up}{Up}{Up}{Left}{Enter}")
; Ctrl+F8 → Black
^F8::DoBlockInputSend("{AppsKey}{End}{Up}{Left}{Enter}")

; ------------------------------------------------------------------
; F9-F12: TRANSPORT & MISC
; ------------------------------------------------------------------

F9::F9                ; Record (passthrough to Ableton default)
F10::Send("^l")       ; Toggle loop on/off
F12::F12              ; Passthrough (Ableton default)

; ------------------------------------------------------------------
; F13-F18: EXTENDED KEYS (for keyboards / macro pads with extra F-keys)
; ------------------------------------------------------------------

F13::Send("^!b")      ; Toggle browser
^F13:: {
    Send("!2")
    Send("A")
}

F15::Send("^k")       ; Toggle Key Map mode
^F15::Send("^m")      ; Toggle MIDI Map mode
F16::return           ; Reserved (e.g. mute via MIDI in the MIDI script)
+F17::Send("u")       ; Expand groups
F17::Send("^!u")      ; Extend take lanes
F18::Send("^!3")      ; Open clip view
!F18::Send("^!m")     ; Open device view
^F18::Send("^!4")     ; Open master view
^!F18::Send("^!E")    ; Toggle full-height clip view

#HotIf

; ------------------------------------------------------------------
; HELPER FUNCTIONS
; ------------------------------------------------------------------

DoBlockInputSend(toSend) {
    BlockInput("On")
    MouseGetPos(&origMouseX, &origMouseY)
    MouseMove(0, 0)
    Sleep(50)
    Send(toSend)
    MouseMove(origMouseX, origMouseY)
    BlockInput("Off")
}

AddTaggedPlugin(tag, pluginName, downArrow) {
    BlockInput("On")
    sleepTime := 300
    Send("^f")
    Sleep(sleepTime)
    Send("{Esc}")
    Sleep(sleepTime)
    Send("^f")
    Sleep(sleepTime)
    Send("{#}" tag)
    Send("{Enter}")
    Sleep(sleepTime)
    Send(pluginName)
    Sleep(sleepTime)
    Send("{Enter}")
    Sleep(sleepTime)
    Loop downArrow
    {
        Send("{Down}")
        Sleep(sleepTime)
    }
    Send("{Enter}")
    Sleep(sleepTime)
    Send("{Esc}")
    Sleep(sleepTime)
    Send("^!b")
    BlockInput("Off")
}

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey drop-in_fixes/Media_Keys_as_Midi_Controller.ahk
; MEDIA KEYS AS MIDI CONTROLLER
; -> Play, stop, skip, and adjust volume in Ableton using your keyboard's media keys.
; ! Install LoopBe1 virtual MIDI port + LiveHack control surface

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

global MidiPortIdentifier := "LoopBe Internal MIDI"
global ControlSurfaceDownloadUrl := "https://livehack.tools/ahk/static/downloads/LiveHack_Ableton_Live_Control_Surface.zip"
global MidiNoteDurationMs := 5
global MidiSetupWarningShown := false
global MidiPortOpenWarningShown := false
global CachedMidiPortIndex := -1
global CachedMidiPortResolvedName := ""

; MIDI device selection:
; Put the Windows MIDI OUTPUT device name you want to use in MidiPortIdentifier.
; Matching is intentionally conservative and tries, in order:
; 1) exact match
; 2) exact match after stripping whitespace
; 3) case-insensitive substring search
; 4) ordered word search (words must appear in sequence)

; MIDI setup required:
; 1. Install LoopBe1 (https://www.nerds.de/en/loopbe1.html) — free virtual MIDI port.
;    The port name will be "LoopBe Internal MIDI". If you have a MIDI interface you 
;    can also wire the output into the input. Change "MidiPortIdentifier" to the 
;    MIDI port name of your audio interface.
; 2. Download the LiveHack control surface package:
;    https://livehack.tools/ahk/static/downloads/LiveHack_Ableton_Live_Control_Surface.zip
; 3. Install the control surface folder in Ableton's Remote Scripts location.
; 4. In Ableton, enable the LoopBe Internal MIDI port for the control surface.
; Without this setup, the script will not control Ableton.

#HotIf WinExist("ahk_class Ableton Live Window Class")

; Mute selected track
Volume_Mute::LiveHack_SendMidiNote(1, 3, 127)

; Coarse track volume
Volume_Down::LiveHack_SendMidiNote(1, 4, 127)
Volume_Up::LiveHack_SendMidiNote(1, 5, 127)

; Transport
Media_Play_Pause::LiveHack_SendMidiNote(1, 6, 127)
Media_Stop::LiveHack_SendMidiNote(1, 7, 127)
Media_Prev::LiveHack_SendMidiNote(1, 8, 127)
Media_Next::LiveHack_SendMidiNote(1, 9, 127)

; Fine track volume
+Volume_Down::LiveHack_SendMidiNote(1, 10, 127)
+Volume_Up::LiveHack_SendMidiNote(1, 11, 127)

; Play/Pause (Ctrl variant)
^Media_Play_Pause::LiveHack_SendMidiNote(1, 12, 127)

; Track pan
^Volume_Down::LiveHack_SendMidiNote(1, 13, 127)
^Volume_Up::LiveHack_SendMidiNote(1, 14, 127)
^+Volume_Down::LiveHack_SendMidiNote(1, 15, 127)
^+Volume_Up::LiveHack_SendMidiNote(1, 16, 127)

; Track solo / arm
^Volume_Mute::LiveHack_SendMidiNote(1, 17, 127)
^+Volume_Mute::LiveHack_SendMidiNote(1, 18, 127)

#HotIf

; Debug: Ctrl+Alt+Shift+L — shows MIDI status and key mappings
^!+l::LiveHack_ShowMidiDebugInfo()

LiveHack_SendMidiNote(channel, note, noteVelocity) {
    global MidiPortIdentifier, MidiNoteDurationMs

    midiDevice := LiveHack_GetPreferredMidiOutPort(MidiPortIdentifier)
    if (midiDevice < 0)
    {
        LiveHack_ShowMidiSetupWarning()
        return
    }

    hMidiOut := LiveHack_MidiOutOpen(midiDevice)
    if (!hMidiOut)
        return

    LiveHack_MidiOutShortMsg(hMidiOut, "N1", channel, note, noteVelocity)
    Sleep(MidiNoteDurationMs)
    LiveHack_MidiOutClose(hMidiOut)
}

LiveHack_ShowMidiSetupWarning() {
    global MidiSetupWarningShown, MidiPortIdentifier, ControlSurfaceDownloadUrl

    if (MidiSetupWarningShown)
        return

    MidiSetupWarningShown := true
    msg := 'Could not find a MIDI output named "' MidiPortIdentifier '".'
        . "`n`nInstall LoopBe1 from https://www.nerds.de/en/loopbe1.html"
        . '`nThe virtual MIDI port will appear as "LoopBe Internal MIDI".'
        . "`n`nThen select the same port for the LiveHack control surface in Ableton."
        . "`n`nMatching tries: exact, whitespace-stripped, case-insensitive, then ordered words."
        . "`n`nControl-surface download:`n" ControlSurfaceDownloadUrl
        . LiveHack_MidiPortListText()
    MsgBox(msg, "LiveHack MIDI setup required", "Icon!")
}

LiveHack_ShowMidiDebugInfo() {
    global MidiPortIdentifier, CachedMidiPortIndex, CachedMidiPortResolvedName

    numPorts := LiveHack_MidiOutGetNumDevs()
    resolved := LiveHack_FindMidiOutPort(MidiPortIdentifier)
    mappings := LiveHack_GetMappingTable()

    cBg  := "1E1E2E"
    cGrn := "A6E3A1"
    cDim := "585B70"
    cOK  := resolved >= 0 ? "A6E3A1" : "F38BA8"
    W    := 620

    dg := Gui("+AlwaysOnTop -MaximizeBox +ToolWindow", "LiveHack · MIDI Debug")
    dg.BackColor := cBg
    dg.MarginX   := 24
    dg.MarginY   := 20
    ; Dark titlebar on Win 10 1903+ / Win 11
    DllCall("dwmapi\DwmSetWindowAttribute", "Ptr", dg.Hwnd, "UInt", 20, "Int*", 1, "UInt", 4)

    dg.SetFont("s14 w700 c" cGrn, "Segoe UI")
    dg.Add("Text", "w" W, "LiveHack  ·  MIDI Debug")
    dg.SetFont("s8 c" cDim, "Segoe UI")
    dg.Add("Text", "w" W " y+2", FormatTime(, "ddd d MMM yyyy  HH:mm:ss") "  ·  Esc to close")

    ; ── Port ──────────────────────────────────────────────────────────
    dg.SetFont("s8 w600 c" cDim, "Segoe UI")
    dg.Add("Text", "w" W " y+16", "MIDI OUTPUT")
    dg.SetFont("s10 w600 c" cOK, "Consolas")
    dg.Add("Text", "w" W " y+4", resolved >= 0
        ? "[" resolved "]  " LiveHack_MidiOutNameGet(resolved) "  ✔"
        : "NOT FOUND  ✘  —  check MidiPortIdentifier")
    dg.SetFont("s8 c" cDim, "Segoe UI")
    dg.Add("Text", "w" W " y+2", 'Configured: "' MidiPortIdentifier '"'
        . "   Cache: " (CachedMidiPortIndex >= 0 ? "hit [" CachedMidiPortIndex "]" : "miss"))

    ; Compact device list as plain wrapping text
    devLine := ""
    Loop numPorts {
        i := A_Index - 1
        devLine .= (i ? "   " : "") "[" i "] " LiveHack_MidiOutNameGet(i) (i = resolved ? " ✓" : "")
    }
    dg.SetFont("s8 c" cDim, "Consolas")
    dg.Add("Text", "w" W " y+4", devLine)

    ; ── Ableton setup ──────────────────────────────────────────────
    dg.SetFont("s8 w600 c" cDim, "Segoe UI")
    dg.Add("Text", "w" W " y+16", "ABLETON MIDI SETTINGS")
    dg.SetFont("s8 c" cDim, "Consolas")
    setupText := "  Control Surfaces  :  LiveHack  |  Input: LoopBe Internal MIDI  |  Output: None`n"
        . "  Input Ports       :  LoopBe Internal MIDI  —  Track ○  Sync ○  Remote ○`n"
        . "  Output Ports      :  LoopBe Internal MIDI  —  Track ○  Sync ○  Remote ○`n"
        . "  Signal flow       :  AHK → LoopBe Internal MIDI → Ableton → LiveHack CS"
    dg.Add("Text", "w" W " y+4", setupText)

    ; ── Mappings ──────────────────────────────────────────────────────
    dg.SetFont("s8 w600 c" cDim, "Segoe UI")
    dg.Add("Text", "w" W " y+16", "MIDI MAPPINGS  (" mappings.Length ")")
    lv := dg.Add("ListView", "w" W " r" Min(mappings.Length + 1, 22) " y+4 -Multi +ReadOnly",
        ["Hotkey", "Ch", "Note", "CS Action"])
    DllCall("uxtheme\SetWindowTheme", "Ptr", lv.Hwnd, "Str", "DarkMode_Explorer", "Ptr", 0)
    lv.SetFont("s9", "Consolas")
    for m in mappings
        lv.Add("", m["key"], m["ch"], m["note"], m["desc"])
    lv.ModifyCol(1, 160), lv.ModifyCol(2, 36), lv.ModifyCol(3, 46), lv.ModifyCol(4, W - 260)

    ; ── OK ────────────────────────────────────────────────────────────
    dg.SetFont("s9 w700 c1E1E2E", "Segoe UI")
    btn := dg.Add("Button", "w" W " h32 y+16", "OK")
    btn.Opt("Background" cGrn)
    DllCall("uxtheme\SetWindowTheme", "Ptr", btn.Hwnd, "Str", " ", "Ptr", 0)
    btn.OnEvent("Click", (*) => dg.Destroy())
    dg.OnEvent("Escape", (*) => dg.Destroy())
    dg.OnEvent("Close",  (*) => dg.Destroy())
    dg.Show("AutoSize Center")
}

LiveHack_GetMappingTable() {
    t := [
        Map("key", "Volume Mute",       "ch", 1, "note",  3, "desc", "Mute selected track"),
        Map("key", "Volume Down",       "ch", 1, "note",  4, "desc", "Volume −1 dB"),
        Map("key", "Volume Up",         "ch", 1, "note",  5, "desc", "Volume +1 dB"),
        Map("key", "Media Play/Pause",  "ch", 1, "note",  6, "desc", "Play / pause transport"),
        Map("key", "Media Stop",        "ch", 1, "note",  7, "desc", "Stop transport"),
        Map("key", "Media Prev",        "ch", 1, "note",  8, "desc", "Scroll arranger ←"),
        Map("key", "Media Next",        "ch", 1, "note",  9, "desc", "Scroll arranger →"),
        Map("key", "Shift+Vol Down",    "ch", 1, "note", 10, "desc", "Volume − fine"),
        Map("key", "Shift+Vol Up",      "ch", 1, "note", 11, "desc", "Volume + fine"),
        Map("key", "Ctrl+Media Play",   "ch", 1, "note", 12, "desc", "Continue playing"),
        Map("key", "Ctrl+Vol Down",     "ch", 1, "note", 13, "desc", "Pan ← coarse"),
        Map("key", "Ctrl+Vol Up",       "ch", 1, "note", 14, "desc", "Pan → coarse"),
        Map("key", "Ctrl+Shift+Vol Dn", "ch", 1, "note", 15, "desc", "Pan ← fine"),
        Map("key", "Ctrl+Shift+Vol Up", "ch", 1, "note", 16, "desc", "Pan → fine"),
        Map("key", "Ctrl+Mute",         "ch", 1, "note", 17, "desc", "Solo track"),
        Map("key", "Ctrl+Shift+Mute",   "ch", 1, "note", 18, "desc", "Arm track"),
        Map("key", "—",                 "ch", 1, "note", 19, "desc", "Hide all views"),
        Map("key", "—",                 "ch", 1, "note", 20, "desc", "Prev track"),
        Map("key", "—",                 "ch", 1, "note", 21, "desc", "Next track"),
    ]
    ; Ch2: master_track.devices[0].parameters[1..16]
    Loop 16
        t.Push(Map("key", "—", "ch", 2, "note", A_Index, "desc", "Master device param " A_Index))
    return t
}

LiveHack_MidiOutOpen(uDeviceID := 0) {
    global MidiPortOpenWarningShown

    hMidiOut := 0
    result := DllCall("winmm.dll\midiOutOpen", "Ptr*", &hMidiOut, "UInt", uDeviceID, "Ptr", 0, "Ptr", 0, "UInt", 0)
    if (result != 0)
    {
        if (!MidiPortOpenWarningShown)
        {
            MidiPortOpenWarningShown := true
            MsgBox("Could not open the selected MIDI output. Close any app that is locking the port, then try again.", "LiveHack MIDI output unavailable", "Icon!")
        }
        return 0
    }
    return hMidiOut
}

LiveHack_MidiOutShortMsg(hMidiOut, eventType, channel, param1, param2) {
    ; MIDI status byte: event nibble | zero-based channel
    ; NoteOn = 0x90, NoteOff = 0x80; channel param is 1-based
    if (eventType = "NoteOn" || eventType = "N1")
        midiStatus := 0x8F + channel
    else if (eventType = "NoteOff" || eventType = "N0")
        midiStatus := 0x7F + channel
    else
        return -1
    return DllCall("winmm.dll\midiOutShortMsg", "Ptr", hMidiOut, "UInt", midiStatus | (param1 << 8) | (param2 << 16))
}

LiveHack_MidiOutClose(hMidiOut) {
    if (hMidiOut)
        DllCall("winmm.dll\midiOutClose", "Ptr", hMidiOut)
}

LiveHack_GetPreferredMidiOutPort(preferredName) {
    global CachedMidiPortIndex, CachedMidiPortResolvedName

    ; Cache is valid if the device at the cached index still has the same name
    if (CachedMidiPortIndex >= 0)
    {
        if (LiveHack_MidiOutNameGet(CachedMidiPortIndex) = CachedMidiPortResolvedName)
            return CachedMidiPortIndex
        CachedMidiPortIndex := -1
        CachedMidiPortResolvedName := ""
    }

    idx := LiveHack_FindMidiOutPort(preferredName)
    if (idx >= 0)
    {
        CachedMidiPortIndex := idx
        CachedMidiPortResolvedName := LiveHack_MidiOutNameGet(idx)
    }
    return idx
}

LiveHack_FindMidiOutPort(preferredName) {
    preferredName := Trim(preferredName)
    if (preferredName = "")
        return -1

    midiPorts := []
    numPorts := LiveHack_MidiOutGetNumDevs()
    Loop numPorts
    {
        portIndex := A_Index - 1
        portName := LiveHack_MidiOutNameGet(portIndex)
        if (portName != "")
            midiPorts.Push(Map("index", portIndex, "name", portName))
    }

    ; 1) Exact match.
    for midiPort in midiPorts
    {
        if (midiPort["name"] = preferredName)
            return midiPort["index"]
    }

    ; 2) Exact match after stripping whitespace.
    preferredNoWhitespace := LiveHack_StripWhitespace(preferredName)
    for midiPort in midiPorts
    {
        if (LiveHack_StripWhitespace(midiPort["name"]) = preferredNoWhitespace)
            return midiPort["index"]
    }

    ; 3) Case-insensitive search.
    preferredLower := StrLower(preferredName)
    for midiPort in midiPorts
    {
        if (InStr(StrLower(midiPort["name"]), preferredLower))
            return midiPort["index"]
    }

    ; 4) Ordered word search.
    preferredWords := LiveHack_SplitWords(preferredName)
    if (preferredWords.Length = 0)
        return -1

    for midiPort in midiPorts
    {
        if (LiveHack_PortContainsWordsInOrder(midiPort["name"], preferredWords))
            return midiPort["index"]
    }

    return -1
}

LiveHack_StripWhitespace(text) {
    return RegExReplace(text, "\s+")
}

LiveHack_SplitWords(text) {
    text := Trim(RegExReplace(text, "\s+", " "))
    if (text = "")
        return []
    return StrSplit(text, " ")
}

LiveHack_PortContainsWordsInOrder(portName, preferredWords) {
    if (preferredWords.Length = 0)
        return false

    portNameLower := StrLower(portName)
    searchStartPos := 1

    for preferredWord in preferredWords
    {
        preferredWordLower := StrLower(preferredWord)
        foundPos := InStr(portNameLower, preferredWordLower, true, searchStartPos)
        if (!foundPos)
            return false
        searchStartPos := foundPos + StrLen(preferredWordLower)
    }

    return true
}

LiveHack_MidiPortListText() {
    numPorts := LiveHack_MidiOutGetNumDevs()
    if (numPorts < 1)
        return "`n`nNo MIDI output ports are currently available."

    text := "`n`nAvailable MIDI outputs:"
    Loop numPorts
    {
        portIndex := A_Index - 1
        portName := LiveHack_MidiOutNameGet(portIndex)
        text .= "`n- " . portIndex . ": " . portName
    }
    return text
}

LiveHack_MidiOutGetNumDevs() {
    return DllCall("winmm.dll\midiOutGetNumDevs")
}

LiveHack_MidiOutNameGet(uDeviceID := 0) {
    MidiOutCaps := Buffer(50, 0)
    result := DllCall("winmm.dll\midiOutGetDevCapsA", "UInt", uDeviceID, "Ptr", MidiOutCaps, "UInt", 50)
    if (result != 0)
        return ""
    return StrGet(MidiOutCaps.Ptr + 8, 32, "CP0")
}

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey templates/Block_F-Keys.ahk
; BLOCK F-KEYS
; -> Blocks F1–F8 while a specific plugin is open so you stop accidentally triggering Ableton shortcuts.

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

; Change plugin name to taste or add more.
#HotIf WinActive("Melodyne")

F1::return
F2::return
F3::return
F4::return
F5::return
F6::return
F7::return
F8::return

#HotIf

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey templates/Browser_Quick_Search.ahk
; BROWSER QUICKSEARCH
; -> Type a short abbreviation and the script finds and loads the plugin from Ableton's browser.
; !! Required to add custom tags in the browser. This script works by typing really fast. If you interfere, sometimes keys get fired into the wrong context and mess up stuff. Use at your own risk!

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

global SearchSleepMs := 300

#HotIf WinActive("ahk_class Ableton Live Window Class")

; ==================================================================
; HOTSTRING EXAMPLES (type the abbreviation, it auto-expands)
; ==================================================================

; Built-in Ableton devices (two tags: Device + Ableton)
::glue:: {
    SearchAndInsertTaggedPlugin("Device", "Ableton", "Glue Compressor", 0)
}

::eq8:: {
    SearchAndInsertTaggedPlugin("Device", "Ableton", "EQ Eight", 0)
}

::over:: {
    SearchAndInsertTaggedPlugin("Device", "Ableton", "Overdrive", 0)
}

; Third-party VST3 plugins (two tags: vendor + VST3)
::q4:: {
    SearchAndInsertTaggedPlugin("FabFilter", "VST3", "Pro-Q 4", 1)
}

::mb:: {
    SearchAndInsertTaggedPlugin("FabFilter", "VST3", "Pro-MB", 1)
}

::ds:: {
    SearchAndInsertTaggedPlugin("FabFilter", "VST3", "Pro-DS", 1)
}

::ser:: {
    SearchAndInsertTaggedPlugin("XFer Records", "AutoHotKey", "Serum 2", 1)
}

::kon:: {
    SearchAndInsertTaggedPlugin("Native Instruments", "VST3", "Kontakt 8", 1)
}

::dec:: {
    SearchAndInsertTaggedPlugin("Soundtoys", "VST3", "Decapitator", 1)
}

::val:: {
    SearchAndInsertTaggedPlugin("Valhalla DSP", "VST3", "ValhallaRoom", 1)
}

; Presets with a custom tag
::myrack:: {
    SearchAndInsertTaggedPlugin("Preset", "AutoHotKey", "Your Favorite Rack.adg", 1)
}
#HotIf

; ------------------------------------------------------------------
; HOW TAGS WORK:
; Ableton's browser lets you search by tags using the # prefix.
; You can use TWO tags to narrow down results, for example:
;   #Device #Ableton    → only Ableton built-in devices
;   #FabFilter #VST3    → only FabFilter VST3 plugins
;   #Preset #AutoHotKey → only presets you tagged "AutoHotKey"
;
; The `downArrow` parameter lets you pick a specific result when
; the search returns more than one match. Set it to 0 if the search
; string is unique enough to always land on the right result.
;
; BEST PRACTICE: Create a custom tag (e.g. "AutoHotKey") in Ableton
; and assign it to every plugin you want to load via hotkey. That way
; each search always returns exactly one result and you can set
; downArrow to 0 for a clean one-shot insert.
; ------------------------------------------------------------------

; ==================================================================
; SEARCH FUNCTIONS
; ==================================================================

; Two-tag search (for hotstrings):
; Uses two #tag entries to narrow results, then types the plugin name.
SearchAndInsertTaggedPlugin(tag1, tag2, pluginName, downArrow := 0) {
    global SearchSleepMs

    BlockInput("On")
    Send("{#}" tag1)
    Sleep(SearchSleepMs)
    Send("{Enter}")
    Sleep(SearchSleepMs)
    Send("{#}" tag2)
    Sleep(SearchSleepMs)
    Send("{Enter}")
    Sleep(SearchSleepMs)
    Send(pluginName)
    Sleep(SearchSleepMs)

    Loop downArrow
    {
        Send("{Down}")
        Sleep(SearchSleepMs)
    }

    Send("{Enter}")
    Sleep(SearchSleepMs)
    Send("{Esc}")
    BlockInput("Off")
}

; Single-tag search (for F-key bindings):
; Opens the browser search, applies one custom tag, types the plugin name.
AddTaggedPlugin(tag, pluginName, downArrow) {
    BlockInput("On")
    sleepTime := 300
    Send("^f")
    Sleep(sleepTime)
    Send("{Esc}")
    Sleep(sleepTime)
    Send("^f")
    Sleep(sleepTime)
    Send("{#}" tag)
    Send("{Enter}")
    Sleep(sleepTime)
    Send(pluginName)
    Sleep(sleepTime)
    Send("{Enter}")
    Sleep(sleepTime)
    Loop downArrow
    {
        Send("{Down}")
        Sleep(sleepTime)
    }
    Send("{Enter}")
    Sleep(sleepTime)
    Send("{Esc}")
    Sleep(sleepTime)
    Send("^!b")
    BlockInput("Off")
}

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
AutoHotkey templates/Program_Specific_Hotkeys.ahk
; PROGRAM SPECIFIC HOTKEYS
; -> Template for window-context-aware hotkeys. Different shortcuts activate depending on which app is in focus.

#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode("Input")
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode(2)

; ==================================================================
; CONTEXT 1: ALWAYS ACTIVE (global — runs regardless of which app is in front)
; ==================================================================
; Put hotkeys here that should work everywhere — media key overrides,
; clipboard helpers, global shortcuts, etc.

; Example:
; ^!n::Run("Notepad")


; ==================================================================
; CONTEXT 2: ABLETON LIVE ONLY
; Active when Ableton (or an Ableton plugin window) is in focus
; and Melodyne is NOT open.
; ==================================================================
#HotIf ((WinActive("ahk_class Ableton Live Window Class") or WinActive("ahk_class Vst3PlugWindow") or WinActive("ahk_class AbletonVstPlugClass")) and not WinActive("Melodyne"))
{
    ; Your Ableton-specific hotkeys go here.
    ; Examples:
    ; F1::AddTaggedPlugin("AutoHotKey", "Compressor", 0)
    ; ^!b::Send("^!b")   ; toggle browser
}
#HotIf

; ==================================================================
; CONTEXT 3: ABLETON IS RUNNING (even if it is in the background)
; Useful for media-key MIDI control that should work from any window
; as long as Ableton is open somewhere.
; ==================================================================
#HotIf WinExist("ahk_class Ableton Live Window Class")
{
    ; Your "Ableton exists" hotkeys go here.
    ; Examples:
    ; Media_Play_Pause::SendMidiNote(1, 6, 127)
    ; Volume_Down::SendMidiNote(1, 4, 127)
}
#HotIf

; ==================================================================
; CONTEXT 4: BITWIG / CUBASE (if you use multiple DAWs)
; Active when Bitwig or Cubase is in focus.
; ==================================================================
#HotIf WinActive("ahk_class bitwig")
{
    ; Your Bitwig-specific hotkeys go here.
    ; Example: horizontal scroll with Shift+Wheel
}
#HotIf

#HotIf WinExist("ahk_exe Cubase14.exe")
{
    ; Your Cubase-specific hotkeys go here.
    ; Example: settings window toggle
    ; $^,:: {
    ;     if WinExist("Preferences")
    ;     {
    ;         Send("o")
    ;         return
    ;     }
    ;     else
    ;     {
    ;         Send("^,")
    ;         return
    ;     }
    ; }
}
#HotIf

; ==================================================================
; CONTEXT 5: NON-ABLETON (active when Ableton is NOT in front)
; Useful for global shortcuts that should not be active in Ableton.
; ==================================================================
#HotIf !WinActive("ahk_class Ableton Live Window Class")
{
    ; Your "Ableton is not active" hotkeys go here.
    ; Example: restore Ctrl+Shift+Wheel as system volume
    ; ^+WheelUp::Volume_Up
    ; ^+WheelDown::Volume_Down
}
#HotIf

; April 2026 — Released under MIT license by Yannis Lever
; This code is provided as-is with no warranty. Use at your own risk.
; Check out www.livehack.tools for more LiveHacks!
2 Download & run
Download only the files you want Or simply get everything here. Each code snippet works on its own or copy-paste the snippets you want into your own script.
Customize to taste Customize the hotkeys so they make sense for your workflow. These scripts use AHK v2 syntax.
Install AutoHotKey You can get it on autohotkey.com. Download v2.0.
Launch the AHK script You can launch the script manually or put it into your Autostart folder, so it is always active. In Windows 10 and 11, open the Run dialog by pressing Windows + R and type shell:startup.
Media Keys as MIDI Controller To use your Media Keys as a MIDI controller, install LoopBe1. The AHK script sends MIDI notes into the virtual MIDI device, which Ableton Live can receive. Use the LiveHack control surface to map the media keys to your transport and track controls across sessions without manual remapping.
Download LiveHack Control Surface