Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/build-installers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build Installers

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
build:
name: Build ${{ matrix.platform }} installer
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: macos
runner: macos-latest
- platform: windows
runner: windows-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install . pyinstaller

- name: Build installer bundle
run: python scripts/build_installers.py --platform ${{ matrix.platform }}

- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: openbeat-${{ matrix.platform }}-installer
path: |
dist/installers/*.pkg
dist/installers/*installer.exe
if-no-files-found: error
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Music for Test/
__pycache__/
*.pyc
*.egg-info/
build/
dist/

# Generated OpenBeat outputs
*.openbeat-clicks.wav
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ Available actions:

## Installation

### 1. Create the Python environment
### Option A (recommended): single-file installer download (no Python required)

CI now builds platform-specific single-file installers that bundle the OpenBeat CLI runtime:

- macOS artifact: `OpenBeat-macos-<version>.pkg`
- Windows artifact: `OpenBeat-windows-<version>-installer.exe`

On macOS, run the `.pkg` in Installer.app (multi-step guided setup). On Windows, run the installer `.exe` directly.
Restart Resolve after install.

### Option B: developer install from source

#### 1. Create the Python environment

```bash
cd /Users/parker/Documents/Code/OpenBeat
Expand All @@ -36,7 +48,7 @@ python3 -m venv .venv
python -m pip install -e .
```

### 2. Link the Resolve scripts
#### 2. Link the Resolve scripts

```bash
cd /Users/parker/Documents/Code/OpenBeat
Expand All @@ -45,6 +57,17 @@ cd /Users/parker/Documents/Code/OpenBeat

Restart Resolve after linking the scripts if it is already open.

## Building installers locally

Install build dependencies, then run:

```bash
python -m pip install . pyinstaller
python scripts/build_installers.py --platform all
```

Generated installer archives are written to `dist/installers/`.

## Outputs

- Click tracks are written next to the source file:
Expand Down
76 changes: 64 additions & 12 deletions resolve/Fusion/Modules/OpenBeat/OpenBeatCommon.lua
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
local OpenBeat = {}

local function is_windows()
return package.config:sub(1, 1) == "\\"
end

local function path_sep()
if is_windows() then
return "\\"
end
return "/"
end

local function dirname(path)
return path:match("^(.*)/[^/]+$") or "."
local normalized = tostring(path):gsub("\\", "/")
return normalized:match("^(.*)/[^/]+$") or "."
end

local function join_path(...)
local items = { ... }
return table.concat(items, "/")
return table.concat(items, path_sep())
end

local function file_exists(path)
Expand All @@ -29,7 +41,11 @@ local function first_value(items)
end

local function shell_quote(value)
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
local as_string = tostring(value)
if is_windows() then
return '"' .. as_string:gsub('"', '\\"') .. '"'
end
return "'" .. as_string:gsub("'", [['"'"']]) .. "'"
end

local function current_script_path()
Expand Down Expand Up @@ -66,7 +82,25 @@ local function repo_root()
end

local function log(message)
local path = os.getenv("HOME") .. "/Library/Application Support/Blackmagic Design/DaVinci Resolve/logs/OpenBeat.log"
local home = os.getenv("HOME")
if not home or home == "" then
home = os.getenv("USERPROFILE")
end
if not home or home == "" then
return
end

local path
if is_windows() then
local appdata = os.getenv("APPDATA")
if appdata and appdata ~= "" then
path = join_path(appdata, "Blackmagic Design", "DaVinci Resolve", "Support", "logs", "OpenBeat.log")
else
path = join_path(home, "AppData", "Roaming", "Blackmagic Design", "DaVinci Resolve", "Support", "logs", "OpenBeat.log")
end
else
path = join_path(home, "Library", "Application Support", "Blackmagic Design", "DaVinci Resolve", "logs", "OpenBeat.log")
end
local handle = io.open(path, "a")
if not handle then
return
Expand All @@ -89,20 +123,38 @@ end

local function temp_path(prefix, extension)
local name = string.format("%s_%d_%d%s", prefix, os.time(), math.random(1000, 9999), extension or "")
return "/tmp/" .. name
local base = os.getenv("TMPDIR") or os.getenv("TEMP") or os.getenv("TMP")
if not base or base == "" then
base = is_windows() and "C:\\Windows\\Temp" or "/tmp"
end
return join_path(base, name)
end

local function python_bin()
if local_config.python_bin and file_exists(local_config.python_bin) then
return local_config.python_bin
end
local candidate = join_path(repo_root(), ".venv", "bin", "python")
if file_exists(candidate) then
return candidate
local candidates = {
join_path(repo_root(), ".venv", "bin", "python"),
join_path(repo_root(), ".venv", "Scripts", "python.exe"),
}
for _, candidate in ipairs(candidates) do
if file_exists(candidate) then
return candidate
end
end
return "python3"
end

local function command_prefix()
local bin = python_bin()
local lowered = string.lower(bin)
if lowered:match("python%.exe$") or lowered:match("python$") then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat python3 fallback as a Python interpreter

When no local config or .venv interpreter is found, python_bin() returns python3, but command_prefix() only appends -m openbeat.cli for names ending in python/python.exe. In that fallback path the generated command becomes "python3" analyze ... (missing module invocation), so OpenBeat commands fail immediately on systems using the fallback interpreter.

Useful? React with 👍 / 👎.

return shell_quote(bin) .. " -m openbeat.cli"
end
return shell_quote(bin)
end

local function project_context()
local resolve_app = resolve or app:GetResolve()
if not resolve_app then
Expand Down Expand Up @@ -260,8 +312,8 @@ end

local function analyze_source(source_path)
local output = temp_path("openbeat_analysis", ".lua")
local final_command = shell_quote(python_bin())
.. " -m openbeat.cli analyze --audio "
local final_command = command_prefix()
.. " analyze --audio "
.. shell_quote(source_path)
.. " --format lua --output "
.. shell_quote(output)
Expand Down Expand Up @@ -348,8 +400,8 @@ end
local function render_click_track(source_path, mode)
local suffix = mode == "raw" and ".openbeat-raw-clicks" or ".openbeat-clicks"
local output = output_path_for(source_path, suffix, ".wav")
local command = shell_quote(python_bin())
.. " -m openbeat.cli click-track --audio "
local command = command_prefix()
.. " click-track --audio "
.. shell_quote(source_path)
.. " --mode "
.. shell_quote(mode)
Expand Down
Loading
Loading