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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Lipscomb
2026-07-15 17:38:29 -04:00
parent 583ed77a67
commit e58f06d9fa
2232 changed files with 685575 additions and 1 deletions
+7 -1
View File
@@ -2,7 +2,13 @@
"permissions": {
"allow": [
"Bash(git add *)",
"Bash(python3 -c \"import ast; ast.parse\\(open\\('main.py'\\).read\\(\\)\\)\")"
"Bash(python3 -c \"import ast; ast.parse\\(open\\('main.py'\\).read\\(\\)\\)\")",
"WebFetch(domain:raw.githubusercontent.com)",
"WebSearch",
"Bash(find /private/tmp/claude-502/-Users-p4piwabl0-Desktop-projects-virtual-controller-clean/29ede664-3f90-4b9a-a462-57bbd9febfef/scratchpad/helgobox -maxdepth 1 -iname target -o -maxdepth 1 -iname *.gitmodules)",
"Bash(SRC=/private/tmp/claude-502/-Users-p4piwabl0-Desktop-projects-virtual-controller-clean/29ede664-3f90-4b9a-a462-57bbd9febfef/scratchpad/helgobox *)",
"Bash(find /Users/p4piwabl0/Desktop/projects/virtual-controller-clean/plugin-reaper-realearn -iname .env -not -iname *.example)",
"Bash(git commit -m ' *)"
]
}
}
+23
View File
@@ -0,0 +1,23 @@
# `+crt-static` enables static linking to C runtime.
#
# In MSVC this would be: "C/C++ => Code Generation => Multithreaded (/MT) instead of Multithreaded-DLL (/MD)"".
# That links the Visual C++ Redistributable stuff statically. So no missing "msvcr*.dll".
[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
# This was an attempt to be compatible with older Linux versions (older libc).
# Doesn't work for now because of: https://github.com/rust-lang/rust/issues/78210
# Also see https://github.com/helgoboss/helgobox/issues/829 for latest ideas.
# TODO-medium Try again later. Not urgent.
#[target.aarch64-unknown-linux-gnu]
#rustflags = ["-Ctarget-feature=+crt-static"]
#
#[target.armv7-unknown-linux-gnueabihf]
#rustflags = ["-Ctarget-feature=+crt-static"]
#
#[target.x86_64-unknown-linux-gnu]
#rustflags = ["-Ctarget-feature=+crt-static"]
+6
View File
@@ -0,0 +1,6 @@
# A build with features "playtime" and "licensing" enabled must have the following
# environment variables set.
PLAYTIME_LICENSE_VERIFYING_KEY=
PLAYTIME_PRESET_VERIFYING_KEY=
PLAYTIME_AUTHENTICITY_SIGNING_KEY=
+4
View File
@@ -0,0 +1,4 @@
* text=auto
# REAPER projects always seem to use CRLF
*.RPP text eol=crlf
+3
View File
@@ -0,0 +1,3 @@
liberapay: helgoboss
custom:
- "https://paypal.me/helgoboss"
@@ -0,0 +1,31 @@
---
name: Bug report
about: Use this if something doesn't work
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. macOS]
- Version [e.g. Ventura 13.6.7]
**Additional context**
Add any other context about the problem here.
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/helgoboss/helgobox/discussions/categories/q-a
about: "GitHub Discussions: Ask or answer a question"
- name: Idea
url: https://github.com/helgoboss/helgobox/discussions/categories/ideas?discussions_q=category%3AIdeas+is%3Aopen+sort%3Adate_created+
about: "GitHub Discussions: Request a new feature or suggest an improvement (please watch out for existing issues first and vote for them)"
- name: General feedback
url: https://github.com/helgoboss/helgobox/discussions/categories/general
about: "GitHub Discussions: Give general feedback"
+50
View File
@@ -0,0 +1,50 @@
name: Build
description: Build and optionally Rust code
inputs:
args:
description: 'Cargo arguments'
required: false
default: ''
targets:
description: 'Additional Rust targets'
required: false
default: ''
test:
description: 'Test?'
required: false
default: 'true'
runs:
using: "composite"
steps:
- name: Install Rust
uses: dtolnay/rust-toolchain@1.84.0
with:
targets: ${{ inputs.targets }}
- name: Set up dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install nasm php libudev-dev libxdo-dev libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev libspeechd-dev libgtk-3-dev
shell: sh
- name: Set up dependencies
run: brew install php
if: runner.os == 'macOS'
shell: sh
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: cargo build
env:
ARGS: ${{ inputs.args }}
run: cargo build $ARGS
shell: sh
- name: cargo test
if: ${{ inputs.test == 'true' }}
env:
RUST_MIN_STACK: 5242880
ARGS: ${{ inputs.args }}
run: cargo test $ARGS -- --nocapture
shell: sh
+30
View File
@@ -0,0 +1,30 @@
name: Check
description: Check Rust code via Clippy
inputs:
args:
description: 'Cargo arguments'
required: false
default: ''
runs:
using: "composite"
steps:
- name: Install Rust
uses: dtolnay/rust-toolchain@1.84.0
with:
components: clippy
- name: Set up dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install nasm php libudev-dev libxdo-dev libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev libspeechd-dev libgtk-3-dev
shell: sh
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: cargo clippy
env:
ARGS: ${{ inputs.args }}
run: cargo clippy $ARGS -- -D warnings
shell: sh
+16
View File
@@ -0,0 +1,16 @@
name: fmt
description: Format Rust code via rustfmt
runs:
using: "composite"
steps:
- name: Install Rust
uses: dtolnay/rust-toolchain@1.84.0
with:
components: rustfmt
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: cargo fmt
run: cargo fmt --all -- --check
shell: sh
@@ -0,0 +1,24 @@
name: Post-checkout
description: Checkout with Playtime Engine
inputs:
playtime-engine-deploy-key:
description: 'Playtime Engine deploy key'
required: true
license-processor-deploy-key:
description: 'License Processor deploy key'
required: true
runs:
using: "composite"
steps:
# Checkout with Playtime Engine
- name: Setup ssh-agent
uses: webfactory/ssh-agent@v0.7.0
with:
ssh-private-key: |
${{ inputs.playtime-engine-deploy-key }}
${{ inputs.license-processor-deploy-key }}
- name: Update submodules
run: |
git submodule update --init
shell: sh
@@ -0,0 +1,42 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: Linux aarch64
jobs:
check:
name: Check
runs-on: ubuntu-22.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/check
with:
args: '--features playtime'
test:
name: Test
runs-on: ubuntu-22.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
args: '--features playtime'
@@ -0,0 +1,54 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: Linux x86_64
jobs:
check:
name: Check
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/check
with:
args: '--features playtime'
test:
name: Test
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
args: '--features playtime'
fmt:
name: Rustfmt
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/fmt
@@ -0,0 +1,31 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: macOS aarch64
jobs:
test:
name: Build
runs-on: macos-14
env:
# The first aarch64 target
MACOSX_DEPLOYMENT_TARGET: 11.0
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
args: '--features playtime,egui'
@@ -0,0 +1,30 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: macOS x86_64
jobs:
check:
name: Check
runs-on: macos-14
env:
MACOSX_DEPLOYMENT_TARGET: 10.9
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
args: '--features playtime,egui'
+193
View File
@@ -0,0 +1,193 @@
on:
create:
ref_type: tag
ref: 'v*'
workflow_dispatch:
name: Create release
jobs:
build-release-artifacts:
name: Build artifact
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- artifact: windows-x86_64
os: windows-2022
lib_file_name: helgobox.dll
extension_file_name: reaper_helgobox.dll
target: x86_64-pc-windows-msvc
profile: release
features: "playtime,egui,licensing"
macosx_deployment_target: ""
lib_file_name_dst: helgobox-windows-x86_64.dll
extension_file_name_dst: reaper_helgobox-windows-x86_64.dll
- artifact: windows-i686
os: windows-2022
lib_file_name: helgobox.dll
extension_file_name: reaper_helgobox.dll
target: i686-pc-windows-msvc
profile: release-llvm-out-of-memory-fix
features: "egui"
lib_file_name_dst: helgobox-windows-i686.dll
extension_file_name_dst: reaper_helgobox-windows-i686.dll
- artifact: macos-x86_64
os: macos-14
lib_file_name: libhelgobox.dylib
extension_file_name: libreaper_helgobox.dylib
target: x86_64-apple-darwin
profile: release-strip
features: "playtime,egui,licensing"
# The minimum version that I tested with is 10.13.
# Writing 10.7 builds, but it won't run, for example, on 10.14 (weird)!
# https://github.com/helgoboss/helgobox/issues/1384
macosx_deployment_target: "10.9"
lib_file_name_dst: helgobox-macos-x86_64.vst.dylib
extension_file_name_dst: reaper_helgobox-macos-x86_64.dylib
- artifact: macos-aarch64
os: macos-14
lib_file_name: libhelgobox.dylib
extension_file_name: libreaper_helgobox.dylib
target: aarch64-apple-darwin
profile: release-strip
features: "playtime,egui,licensing"
# First aarch64 macOS version
macosx_deployment_target: "11.0"
lib_file_name_dst: helgobox-macos-aarch64.vst.dylib
extension_file_name_dst: reaper_helgobox-macos-aarch64.dylib
- artifact: linux-x86_64
os: ubuntu-22.04
lib_file_name: libhelgobox.so
extension_file_name: libreaper_helgobox.so
target: x86_64-unknown-linux-gnu
profile: release-strip
features: "playtime,licensing"
macosx_deployment_target: ""
lib_file_name_dst: helgobox-linux-x86_64.so
extension_file_name_dst: reaper_helgobox-linux-x86_64.so
- artifact: linux-aarch64
os: ubuntu-22.04-arm
lib_file_name: libhelgobox.so
extension_file_name: libreaper_helgobox.so
target: aarch64-unknown-linux-gnu
profile: release-strip
features: "playtime,licensing"
macosx_deployment_target: ""
lib_file_name_dst: helgobox-linux-aarch64.so
extension_file_name_dst: reaper_helgobox-linux-aarch64.so
env:
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
steps:
# Prepare (all)
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- name: Install Rust
uses: dtolnay/rust-toolchain@1.84.0
with:
target: ${{ matrix.target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
# Install OS dependencies (Linux/macOS only)
- name: Set up dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install nasm php libudev-dev libxdo-dev libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev libspeechd-dev libgtk-3-dev
shell: sh
- name: Set up dependencies
run: brew install php
if: runner.os == 'macOS'
shell: sh
# Build (all)
- name: Build release
shell: sh
env:
PLAYTIME_AUTHENTICITY_SIGNING_KEY: "${{ secrets.PLAYTIME_AUTHENTICITY_SIGNING_KEY }}"
PLAYTIME_LICENSE_VERIFYING_KEY: "${{ secrets.PLAYTIME_LICENSE_VERIFYING_KEY }}"
PLAYTIME_PRESET_VERIFYING_KEY: "${{ secrets.PLAYTIME_PRESET_VERIFYING_KEY }}"
AWS_LC_SYS_CMAKE_BUILDER: "1"
run: cargo build --features "${{ matrix.features }}" --profile ${{ matrix.profile }} --target ${{ matrix.target }}
# Upload to artifact
- name: Upload plug-in and extension to artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: |
target/${{ matrix.target }}/${{ matrix.profile }}/${{ matrix.lib_file_name }}
target/${{ matrix.target }}/${{ matrix.profile }}/${{ matrix.extension_file_name }}
target/${{ matrix.target }}/${{ matrix.profile }}/helgobox.pdb
target/${{ matrix.target }}/${{ matrix.profile }}/deps/libhelgobox.dylib.dSYM/
target/${{ matrix.target }}/${{ matrix.profile }}/libhelgobox-debug.so
# Windows x64: Upload PDB to Sentry
- name: Upload PDB
if: startsWith(matrix.artifact, 'windows-x86_64')
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
shell: powershell
run: |
# Install Sentry CLI
npm install -g @sentry/cli
# Upload
sentry-cli debug-files upload --auth-token $env:SENTRY_AUTH_TOKEN -o $env:SENTRY_ORG -p $env:SENTRY_PROJECT "target/${{ matrix.target }}/${{ matrix.profile }}/helgobox.pdb"
- name: Check if release exists
id: check_release
shell: bash
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
run: |
if gh release view ${{ github.ref_name }} &> /dev/null; then
echo "RELEASE_EXISTS=true" >> "$GITHUB_OUTPUT"
else
echo "RELEASE_EXISTS=false" >> "$GITHUB_OUTPUT"
fi
- name: Create release
if: steps.check_release.outputs.RELEASE_EXISTS != 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
run: |
gh release create ${{ github.ref_name }} --draft --prerelease --verify-tag
- name: Download artifacts from build job
uses: actions/download-artifact@v4
- name: Upload lib file
shell: bash
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
run: |
mv "target/${{ matrix.target }}/${{ matrix.profile }}/${{ matrix.lib_file_name }}" "${{ matrix.lib_file_name_dst }}"
gh release upload ${{ github.ref_name }} --clobber "${{ matrix.lib_file_name_dst }}"
- name: Upload extension file
shell: bash
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
run: |
mv "target/${{ matrix.target }}/${{ matrix.profile }}/${{ matrix.extension_file_name }}" "${{ matrix.extension_file_name_dst }}"
gh release upload ${{ github.ref_name }} --clobber "${{ matrix.extension_file_name_dst }}"
@@ -0,0 +1,31 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: Windows i686
jobs:
test:
name: Build
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
# Prevent "LLVM ERROR: out of memory"
args: '--features egui --profile dev-llvm-out-of-memory-fix --target i686-pc-windows-msvc'
targets: 'i686-pc-windows-msvc'
test: 'false'
@@ -0,0 +1,28 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
# For quickly detecting important differences in runner configurations
schedule:
- cron: '0 5 * * MON'
name: Windows x86_64
jobs:
test:
name: Test
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ./.github/actions/post-checkout
with:
playtime-engine-deploy-key: ${{ secrets.PRIVATE_PLAYTIME_CLIP_ENGINE_DEPLOY_KEY }}
license-processor-deploy-key: ${{ secrets.PRIVATE_HELGOBOSS_LICENSE_PROCESSOR_DEPLOY_KEY }}
- uses: ./.github/actions/build
with:
args: '--features playtime,egui'
+132
View File
@@ -0,0 +1,132 @@
# Created by https://www.gitignore.io/api/rust,clion+all
# Edit at https://www.gitignore.io/?templates=rust,clion+all
### CLion+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Rust ###
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
/*/**/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# End of https://www.gitignore.io/api/rust,clion+all
# Created by https://www.gitignore.io/api/visualstudiocode
# Edit at https://www.gitignore.io/?templates=visualstudiocode
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.env
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/visualstudiocode
*.rpp-bak
*.rpp-PROX
*.reapeaks
.vs/
*.aps
*.user
tree.txt
*.env
.DS_Store
+4
View File
@@ -0,0 +1,4 @@
target
main/lib/WDL
*.json
*.lua
+50
View File
@@ -0,0 +1,50 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Unix Portable",
"type": "lldb",
"request": "launch",
"program": "${userHome}/opt/REAPER/reaper",
"args": [],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.vscode/launch.env"
},
{
"name": "Portable Windows 64-bit",
"type": "cppvsdbg",
"request": "launch",
"program": "C:\\REAPER\\reaper.exe",
"args": ["-newinst"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.vscode/launch.env"
},
{
"name": "Portable Windows 32-bit",
"type": "cppvsdbg",
"request": "launch",
"program": "C:\\REAPER32\\reaper.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.vscode/launch.env"
},
{
"name": "Global Windows 64-bit",
"type": "cppvsdbg",
"request": "launch",
"program": "C:\\Program Files\\REAPER (x64)\\reaper.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.vscode/launch.env"
},
{
"name": "Windows attach",
"type": "cppvsdbg",
"request": "attach",
"processId": "${command:pickProcess}",
}
]
}
+196
View File
@@ -0,0 +1,196 @@
= Architecture
:toc:
:sectnums:
:sectnumlevels: 2
This document describes the software architecture of ReaLearn.
It's a work in progress.
== Basics
Technically, ReaLearn is a *third-party native VST plug-in for REAPER which makes heavy use of the REAPER extension API*.
… Woah!
That is a lot to take in!
Lets see what this means in detail:
* First and foremost, ReaLearn is a *plug-in for REAPER*.
That means ReaLearn is a software module which is loaded by REAPER dynamically (at runtime).
It comes in the form of a link:https://en.m.wikipedia.org/wiki/Dynamic-link_library[DLL] on Windows, a link:https://stackoverflow.com/questions/2339679/what-are-the-differences-between-so-and-dylib-on-macos[DYLIB] on macOS and a link:https://superuser.com/questions/71404/what-is-an-so-file[SO] on Linux.
Communication goes both ways: ReaLearn calls REAPER functions and REAPER calls ReaLearn functions ("callbacks").
* It makes heavy use of the *REAPER extension API*.
That means ReaLearn uses functions and data structures of REAPER which are _specific to REAPER_, so they are _not_ part of a DAW-agnostic plug-in standard.
In theory, other DAWs could implement this API as well, but in practice only REAPER itself does it.
The reason is that this API is huge and that many parts of it only make sense for REAPER.
As a consequence, ReaLearn runs in REAPER only!
* At the same time, it is a *VST plug-in*.
That means …
** … it's not loaded immediately at REAPER startup time.
It gets loaded as soon as the user adds the first ReaLearn plug-in instance (in the form of a REAPER FX) or loads a project or FX chain containing a ReaLearn instance.
** … once loaded, there can be arbitrary many instances of ReaLearn (a big difference to REAPER extensions, which can "exist" only once).
Adding more instances is rather cheap because the dynamic library is already loaded at that time.
** … plug-in data is saved per instance (as a VST chunk).
As a consequence, plug-in data is typically saved along with a specific project (but it doesn't have to because you can put ReaLearn on the monitoring FX chain as well).
** … it can _receive_ MIDI and audio data from the FX input pins (using functions and data structures defined by the VST plug-in standard).
** … it can _send_ MIDI and audio data to the FX output pins (using functions and data structures defined by the VST plug-in standard).
* It's a *native plug-in*.
That means ReaLearn *is not a script*!
It's full-blown native machine code that operates on eye level with REAPER.
Once ReaLearn is loaded, it essentially becomes a part of REAPER itself.
It runs in the same process, can open its own threads and is not sandboxed or restrained by a virtual machine (unlike Lua/EEL-based ReaScript or JSFX).
That's great because it means ReaLearn is a "first-class citizen" and doesn't have any inherent disadvantage in terms of speed and possibilities.
Also, it's written in a language that can leverage all of the power and performance potential that comes with running on "bare metal": link:https://www.rust-lang.org[Rust].
Rust is a non-garbage-collected system programming language, just like C or C++ but more modern.
* It's a *third-party product*.
That mean's it's not made by Cockos, the developer of REAPER.
It's made by link:https://www.helgoboss.org/projects/[Helgoboss] and must be installed separately.
== Modules
ReaLearn is built in a modular fashion.
The following diagram shows ReaLearn's most important modules (excluding 3rd-party modules):
[.text-center]
image:doc/architecture/images/modules.svg[ReaLearn modules]
* *main:* The main module of ReaLearn which contains most of its code.
We will learn more about it in the following sections.
* *api:* This contains the data structures for ReaLearn presets.
Its main use case is _ReaLearn Script_, a way to build mappings with the Lua scripting language.
* *swell-ui:* A tiny custom-written GUI framework based on the Win32 API (Windows) and Cockos SWELL (macOS, Linux) respectively.
SWELL makes it possible to write the GUI code only once, using a subset of the Windows-specific Win32 API, but making it work on macOS and Linux as well.
Basically by translating the Win32 API calls to OS-native GUI framework calls (Cocoa on macOS, GTK on Linux).
* *reaper-rs:* Rust bindings to the REAPER API (which itself is based on C and partially C++).
* *helgoboss-learn:* A library which contains reusable and DAW-agnostic code related to MIDI/OSC-learn functionality.
Some of ReaLearn's basic notions such as _Source_, _Glue_ (still called _Mode_ in most parts of the codebase) and _Target_ are defined in this DAW-neutral module.
* *helgoboss-midi:* A general-purpose and carefully designed library for dealing with MIDI messages according to the MIDI 1.0 specification.
== Layers
The _main_ module of ReaLearn is roughly built around an architectural pattern sometimes called link:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html[Onion Architecture].
This means it's divided into multiple "onion" layers:
[.text-center]
image:doc/architecture/images/onion-layers.svg[ReaLearn onion layers]
These layers follow a simple but strict rule:
____
Outer layers use its own code and code of inner layers, *but inner layers are not allowed to use code of outer layers!*
____
This rule prevents "Spaghetti code" between the different layers and makes sure there's a clean separation between different responsibilities.
IMPORTANT: This rule doesn't restrict control and data flow!
Both control and data can still flow in both directions.
In practice they also do because e.g. data from the processing layer needs to be "sent up" to the user interface in order to be displayed!
The rule is concerned with the visibility of code symbols only.
The contents and responsibilities of each layer are described in the following.
=== Base layer
* Contains very generic and reusable utility code that is not specific to ReaLearn and at the same time not substantial enough to put it into a dedicated library module.
* Also, this layer can be considered as the layer that contains the Rust standard library and other Rust crates that provide utility code (although those are obviously not part of ReaLearn's own codebase).
* In addition, it makes very much sense to think of the base layer as the layer that contains REAPER itself.
ReaLearn is built _around_ REAPER, it's not designed to be usable without it.
** As a direct consequence, all layers within ReaLearn are allowed to use the REAPER API!
** This doesn't mean that ReaLearn couldn't be ported to other DAWs.
It could, provided the other DAW is substantially extensible via native modules.
On ReaLearn's side, some effort in this direction has already been done: As mentioned before, the module _helgoboss-learn_ is designed to contain those parts of ReaLearn's logic that are DAW-agnostic.
=== Processing layer
* This layer contains the essence of ReaLearn: Its processing logic.
This includes the complete control and feedback logic.
* If you would take away ReaLearn's graphical user interface, its projection feature, its plug-in nature, its capability to memorize its settings (= persistence) … in short, all the stuff that is more "facade" than "central", then what's left is the processing layer.
The processing layer alone would still be capable of doing ReaLearn's main job: Routing incoming MIDI or OSC messages through the mapping list and controlling the targets accordingly as well as handling feedback.
* Because the processing layer is very independent and doesn't dictate things like user interface and persistence, It would be quite easy to factor it out into a separate module and use it in other ways, e.g. in order to build a totally different user interface on top of it!
* All the data structures in this layer are custom-tailored and optimized with one primary goal in mind: Performance.
ReaLearn should do its main job very fast and efficiently!
=== Management layer
* This layer contains everything related to _managing_ ReaLearn's objects: Mappings, groups, parameters and all that stuff.
* All the data structures in this layer (usually called _models_) are tailored to this purpose.
If you think that there's a lot of duplication between this layer and the processing layer, look twice.
Yes, the data structures look similar at times, but often they are completely different.
That's because they are designed for different purposes.
This strict separation of concerns ensures that no compromises need to be made between performance (processing layer) and managing/GUI (management/infrastructure layers).
* Even though this layer _still_ doesn't dictate a particular user interface, it is user-interface-aware and provides functions and data structures that are typically used by user interfaces.
It also allows user interfaces to register hooks in order to be notified whenever the state of ReaLearn's objects change.
The management layer is built with a _reactive_ GUI in mind which reflects all changes immediately.
=== Infrastructure layer
* This layer is basically responsible for connecting ReaLearn to the outside world: The user (*user interface*), the storage (*data*), the DAW (*plug-in*), the scripting feature (*api*) and the Projection server (*server*).
==== Plug-in
* Contains the VST plug-in implementation of ReaLearn.
* This is the main entry point, the place where ReaLearn's global initialization happens as well as the initialization per instance.
==== User interface (UI)
* Contains the implementation of ReaLearn's main graphical user interface.
* It's based on the _swell-ui_ module.
That means it uses platform-native user interface widgets - which gives ReaLearn the somewhat old-school but extremely professional look ;)
==== Data
* Contains data structures for the serialization/deserialization of all ReaLearn objects (mappings, groups, etc.).
* The data structures in this layer are similar to the corresponding data structures in the management layer but they serve a quite different purpose: Serialization and deserialization of ReaLearn's state.
This is necessary for persistence and features such as copy&paste.
* One could wonder about the code duplication here, but again: The data structures in this layer serve different purposes than the ones in the management layer.
Serialization/deserialization for persistence purposes absolutely needs to be concerned with backward compatibility, which makes these data structures very hard to change.
Keeping things separate ensures that the management data structures can develop freely, without being constrained by backward compatibility considerations.
Again: No compromises.
==== API
* Contains the data structures that make up ReaLearn Script.
* The focus of these data structures is to provide an expressive API with a wording that's straight to the point.
* These data structures are complete in that they can represent and express all valid ReaLearn object states, much like the structures in _data_.
* They were written much later (end of 2021) than the data structures in _data_ (2016) and therefore reflect ReaLearn's current wording and structure much better.
* In future, the API data structures might actually be used for persistence and copy&paste as well, eventually replacing _data_.
==== Server
* Contains HTTP/WebSocket server code for enabling ReaLearn's Projection feature.
* Will also soon contain gRPC server code for enabling full-blown apps built on top of ReaLearn, such as Playtime 2.
== Components
* ReaLearn is made up by a plethora of data structures that resemble components, which can be considered as ReaLearn's main pillars.
* Some of these components are part of each instance, others exist globally only once.
=== Overview
[.text-center]
image:doc/architecture/images/components.svg[ReaLearn components]
=== Focus: Management communication
[.text-center]
image:doc/architecture/images/components-management.svg[ReaLearn components]
=== Focus: Real-time MIDI communication (from/to FX input/output)
[.text-center]
image:doc/architecture/images/components-midi-fx.svg[ReaLearn components]
=== Focus: Real-time MIDI communication (from/to hardware device)
[.text-center]
image:doc/architecture/images/components-midi-device.svg[ReaLearn components]
=== Focus: Real-time OSC communication
[.text-center]
image:doc/architecture/images/components-osc.svg[ReaLearn components]
== Design decisions
See link:doc/architecture/design-decisions.adoc[Design decisions]
+611
View File
@@ -0,0 +1,611 @@
= Contributing
:toc:
:sectnums:
:sectnumlevels: 2
Contributions are welcome! *However, please open an issue first and discuss the details with me, preferably before you start working on a pull request!* And if possible, avoid very large PRs.
I usually can't find the time to process large PRs.
== Basics
ReaLearn is written in the programming language https://www.rust-lang.org/[Rust].
It makes heavy use of
https://github.com/helgoboss/reaper-rs[reaper-rs], which provides Rust bindings for the
https://www.reaper.fm/sdk/plugin/plugin.php[REAPER C++ API]. _reaper-rs_ was developed together with ReaLearn but is designed as independent library that can be used for REAPER plug-ins of all sorts.
Another noteworthy dependency and byproduct of ReaLearn is https://github.com/helgoboss/helgoboss-learn[helgoboss-learn], a crate which provides DAW-agnostic MIDI learn logic (basically the _source_ and _mode_ parts of ReaLearn).
Like _reaper-rs_, it's designed as independent library and could be used to provide similar MIDI-learn functionality in other DAWs.
== Code statistics
Use https://github.com/XAMPPRocky/tokei[Tokei] or https://github.com/o2sh/onefetch:[Onefetch] to display some interesting statistics about the code, e.g. the lines of code and used languages.
The file `.tokeignore` contains code to be ignored when counting the lines of code.
== Architecture
See link:ARCHITECTURE.adoc[here].
== Directory structure
|===
|Directory entry |Content
|`/` |Workspace root
|`/allocator` |A custom global allocator for deferring deallocation in real-time threads
|`/api` |Data structures of ReaLearn's preset API (which is e.g. used in the Lua-based ReaLearn Script)
|`/base` |Very generic utility code used by many crates in the workspace
|`/csi` |Code for interfacing with the ControlSurfaceIntegrator (CSI) project
|`/dialogs` |The single source of truth for ReaLearn's GUI dialogs
|`/doc` |Documentation
|`/extension` |Helgobox REAPER extension (provides some additional convenience around the actual ReaLearn plug-in)
|`/helgoboss-license-processor` |Contains code for license processing (currently a private submodule)
|`/macros` |Various Rust macros for usage in this project only
|`/main` |Main crate: The actual ReaLearn instrument plug-in (`realearn`)
|`/playtime-api` |Playtime data structures for describing e.g. clip engine presets
|`/playtime-clip-engine` |Playtime Clip Engine for playing/recording clips (currently a private submodule). Is a workspace member because that makes it much simpler to use the same dependency versions everywhere.
|`/playtime-clip-engine-placeholder` |A placeholder crate for the Playtime Clip Engine. Users who don't have access to
the Playtime Clip Engine submodule, must rename this directory to `playtime-clip-engine` in order to be able to build
ReaLearn without feature `playtime`.
|`/pot` |Core logic behind Pot Browser, also powers the Pot targets
|`/pot-browser` |The actual Pot Browser user interface
|`/resources` |REAPER projects for manual testing, controller preset files, etc.
|`/rx-util` |Some reactive programming helpers
|`/swell-ui` |Minimalistic UI framework based on https://www.cockos.com/wdl/[SWELL] (`swell-ui`)
|===
== Build
=== All operating systems
==== Update Cockos WDL (optional)
[source,shell]
----
cd main/lib/WDL
git checkout main
git pull
# After updating Cockos WDL, regenerate Rust bindings (because we use WDL's EEL code, for example)
cargo build --features generate
cargo fmt
----
==== Regenerate Luau language bindings (optional)
Luau language bindings should be regenerated from Rust after changing something in link:api[api] or link:playtime-api[playtime-api].
This is done simply by executing all tests like this:
[source,shell]
----
RUST_MIN_STACK=5242880 cargo test --package helgobox-api --lib bindings::luau::export_luau
----
==== Regenerate artwork (optional)
Artwork such as toolbar icons can be regenerated by running a crate:
[source,shell]
----
cargo run helgobox-artwork-processor
----
3 different approaches for generating code ... yes, maybe it's time to unify this ;)
==== Regenerate diagrams in documentation
This is about the diagrams in the Antora documentation, e.g. the glue signal flow.
[source,shell]
----
sh regenerate-doc-diagrams.sh
----
==== Building with Playtime (can only be done by Helgoboss)
Add this to `$HOME/.cargo/config` (otherwise Cargo will have issues fetching the private submodules):
[source,ini]
----
[net]
git-fetch-with-cli = true
----
=== Windows
In the following, you will find the complete instructions for Windows 10/11, including Rust setup.
Points where you have to consider the target architecture (for example, REAPER 32-bit vs. 64-bit) are marked with :star:.
. Enable "Developer mode" in the Windows settings (this is needed because ReaLearn uses link:https://stackoverflow.com/a/59761201[Symlinks within its Git repository])
. Setup "Visual Studio" (currently tested with version 2022)
* Rust uses native build toolchains.
On Windows, it's necessary to use the MSVC (Microsoft Visual Studio C++) toolchain because REAPER plug-ins only work with that.
* https://visualstudio.microsoft.com/downloads/[Visual Studio downloads] → All downloads → Tools for Visual Studio 2022
→ Build Tools for Visual Studio 2022
* Start it and follow the installer instructions
* Required components
** Workloads tab
*** "C++ build tools" (large box on the left)
*** Make sure "Windows 10 SDK" is checked on the right side (usually it is)
*** If on Windows ARM64: Make sure "C++ Clang-Tools" is checked on the right side (normally not checked!, currently necessary for compilation of `ring` dependency)
** Language packs
*** English
. Setup Rust
* https://www.rust-lang.org/tools/install[Download] and execute `rustup-init.exe`
* Accept the defaults
* Set the correct toolchain default :star:
+
[source,shell]
----
rustup default 1.84.0-x86_64-pc-windows-msvc
----
. If you want to regenerate bindings (usually not necessary): Download and install LLVM from https://github.com/llvm/llvm-project/releases (look for something like `LLVM-*-win64.exe`)
. Download and install https://git-scm.com/download/win[Git for Windows]
. Clone the ReaLearn Git repository
+
[source,shell]
----
git clone https://github.com/helgoboss/helgobox.git`
cd helgobox
git checkout v2.16.0 # or any other release tag
# ONLY IF YOU ARE HELGOBOSS
git submodule update --init
# OTHERWISE
git submodule update --init main/lib/WDL main/lib/helgoboss-learn
rmdir playtime-clip-engine
rename playtime-clip-engine-placeholder playtime-clip-engine
----
. Build ReaLearn (after that you should have a `helgobox.dll` in `target\debug`)
+
[source,shell]
----
cargo build --features egui
----
==== Windows 7 support is gone and its future is uncertain
Helgobox 2.16.15 brought back Windows 7 support, which has gone missing at some point before that.
This was primarily achieved by producing the Windows build using the last official Rust version that supported Windows 7, namely Rust 1.77.2.
However, it turned out that it's very hard to stick with that version.
It means having to give up on new Rust features.
But most importantly, it means updating dependencies is not possible if they raise their MSRV (minimal supported Rust version).
A high price to pay for a rarely used OS.
An alternative would be to use the tier 3 target `x86_64-win7-windows-msvc`.
Compilation works using the `RUSTC_BOOTSTRAP` hack:
.PowerShell
[source]
----
$env:RUSTC_BOOTSTRAP=1
# For compiling indexmap
$env:CARGO_FEATURE_STD=1
cargo build -Z build-std --release --target x86_64-win7-windows-msvc
----
However, linking fails because `windows.lib` can't be opened.
Probably related to https://www.reddit.com/r/rust/comments/1dikeq6/compiling_for_win7_missing_windows0485lib/[this] and https://github.com/rust-lang/rust/issues/128218#issuecomment-2251864932[this] issue.
If -- and only if -- users are genuinely interested in a working Windows 7 build of ReaLearn, I would look into it a bit further.
=== Linux
Complete instructions to build ReaLearn from a _fresh_ Ubuntu 18.04.3 LTS installation, including Rust setup:
[source,shell]
----
# Install native dependencies
sudo apt update
sudo apt install -y curl git build-essential pkg-config php nasm llvm-dev libclang-dev clang libudev-dev libxdo-dev libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev libspeechd-dev libgtk-3-dev
# Install Rust (copied from the official Linux installation instructions)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # choose 1 (default)
source $HOME/.cargo/env
# Set the correct toolchain default
rustup default 1.84.0-x86_64-unknown-linux-gnu
# Clone ReaLearn repository
git clone https://github.com/helgoboss/helgobox.git
cd helgobox
git checkout v2.16.0 # or any other release tag
# ONLY IF YOU ARE HELGOBOSS
git submodule update --init
# OTHERWISE
git submodule update --init main/lib/WDL main/lib/helgoboss-learn
rmdir playtime-clip-engine
mv playtime-clip-engine-placeholder playtime-clip-engine
# Build (after that you should have a "libhelgobox.so" in "target/debug")
cargo build --features egui
----
Some words about the native dependencies:
* `curl git build-essential pkg-config` are bare essentials.
* `php` is needed to translate the ReaLearn dialog resource file to C++ so it can be processed by the SWELL
dialog generator. It's also necessary for generating the 64-bit EEL assembler code. All of this is the
typical WDL C++ way of doing things, no Rust specifics here.
* `nasm` is needed for assembling the 64-bit EEL assembler code to produce `asm-nseel-x64.o`, which is necessary to make the custom https://www.cockos.com/EEL2/[EEL] control and feedback transformations in ReaLearn's absolute mode work.
* `llvm-dev libclang-dev clang` are necessary for building with feature `generate` (to generate bindings to C).
* `libxdo-dev` is needed to control the mouse (see target "Global: Mouse")
* `libudev-dev` is needed for connecting to Stream Deck via HID API
* `libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev` are necessary for https://github.com/BillyDM/egui-baseview[egui-baseview] (https://github.com/emilk/egui[egui] is the GUI framework used for ReaLearn's control transformation editor)
* `libspeechd-dev` is necessary for the speech source
* `libgtk-3-dev` is necessary to obtain the X window and X display from a SWELL OS window, in order to fire up OpenGL/egui in it
=== macOS
The following instructions include Rust setup.
However, it's very well possible that some native toolchain setup instructions are missing, because I don't have a bare macOS installation at my disposal.
The Rust installation script should provide you with the necessary instructions if something is missing.
[source,shell]
----
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # choose 1 (default)
source $HOME/.cargo/env
rustup default 1.84.0-x86_64-apple-darwin
# Clone ReaLearn
cd Downloads
git clone https://github.com/helgoboss/helgobox.git
cd helgobox
git checkout v2.16.0 # or any other release tag
# ONLY IF YOU ARE HELGOBOSS
git submodule update --init
# OTHERWISE
git submodule update --init main/lib/WDL main/lib/helgoboss-learn
rmdir playtime-clip-engine
mv playtime-clip-engine-placeholder playtime-clip-engine
# Install build dependencies
brew install php
# Build ReaLearn
cargo build --features egui
----
== GUI
The GUI dialogs are defined in the `dialogs` directory.
Whenever ReaLearn is built, the code there generates an old-school Windows dialog resource file (`target/generated/msvc.rc`) and a Rust file which contains all the resource ID constants (`main/src/infrastructure/ui/bindings.rs`).
Previously I used the Visual Studio C++ 2019 resource editor to WYSIWYG-edit this file as part of the solution
link:main/src/infrastructure/ui/msvc/msvc.sln[msvc.sln], but this was too tedious.
WARNING: You can still preview the generated file in Visual Studio but don't edit the RC file, the changes will be overwritten at build time!
Adjust the Rust code in the `dialogs` directory instead.
On macOS and Linux, an extra step will happen at build time: It will try to use a PHP script (part of Cockos SWELL) to generate
`target/generated/msvc.rc_mac_dlg`, which is a translation of the RC file to C code using SWELL.
So make sure you have PHP installed on these platforms!
== Test
Yes, there are tests but there should be more.
While ReaLearn's basic building blocks
https://github.com/helgoboss/helgoboss-learn[helgoboss-learn] and https://github.com/helgoboss/reaper-rs[reaper-rs]
are tested pretty thoroughly, ReaLearn itself has room for improvement in that aspect.
=== Unit tests
Unit tests should be executed with a higher stack size because there's one unit test that generates and formats Lua code and this currently overflows the stack in debug builds.
[source,shell]
----
RUST_MIN_STACK=104857600 cargo test
----
=== Integration tests
There's a growing built-in integration test, launchable via action `[developer] ReaLearn: Run integration test`.
In future, it would be nice to run this integration test during continuous integration, just like in _reaper-rs_.
== Log
It's possible to make ReaLearn output log messages to `stdout` by setting the `HELGOBOX_LOG` environment variable, e.g. to `debug,vst=info`.
It follows https://docs.rs/env_logger/0.8.2/env_logger/index.html[this] format.
Beware that e.g. on Windows, `stdout` is not shown, not even when executing REAPER from the command line.
One way to make it visible is to execute REAPER with a debugger.
== Metrics
It's possible to make ReaLearn expose execution metrics.
=== Prometheus endpoint
* If the projection server is running, metrics will then be exposed at `/realearn/metrics` in the popular
https://prometheus.io/[Prometheus] format.
That's great for visualization.
** Just add this to your `prometheus.yml` (you might need to adjust the port):
[source,yaml]
----
scrape_configs:
- job_name: 'realearn'
metrics_path: '/realearn/metrics'
static_configs:
- targets: ['localhost:39080']
----
** If you don't have any metrics enabled, this will show zeros only.
Prometheus is usually available at http://localhost:9090/.
=== ReaLearn metrics
- You can turn on ReaLearn metrics by setting the environment variable `HELGOBOX_METRICS` (value doesn't matter).
- If this environment variable is set (value doesn't matter), ReaLearn will record some metrics and expose them on the Prometheus endpoint mentioned above.
- If ReaLearn is built with the Playtime Clip Engine, this flag will also enable Clip Engine metrics.
This can negatively effect clip playing performance because many clip engine metrics are captured in real-time threads.
== Debug
=== Debug REAPER scanning ReaLearn
Set `vst_scan=1` in the `[reaper]` section of `reaper.ini`.
That makes the debugged REAPER process itself do the scanning.
=== Obtain debug symbols
Debug symbols are stripped from release builds but stored as build artifact of the GitHub Actions "Create release" workflow.
If you want the symbols for a specific build, proceed as follows:
. Open the https://github.com/helgoboss/helgobox/actions?query=workflow%3A%22Create+release%22[list of ReaLearn "Create release" workflows].
. Use the branch filter to show all releases builds made for a specific version, e.g. "v1.11.0".
. Click the desired workflow.
** GitHub seems to do a fuzzy search, so if there are pre-releases (e.g. "v1.11.0-pre2"), you will see them, too.
** In that case, just choose the latest one.
. You will see a list of artifacts, one for each OS-architecture combination.
. Download the one you need and unzip it.
** You will find both the library file and the symbol file (e.g. `realearn.pdb` for a Windows build).
=== Turn on complete backtraces
As soon as you have the debug symbols, you can make ReaLearn print full backtraces (including line number etc.) in the REAPER ReaScript console.
Here's how you do it.
==== Windows
. Set the environment variable `_NT_ALT_SYMBOL_PATH` to some directory of your choice.
. Copy the PDB file in there.
. Fire up REAPER with ReaLearn an make it panic.
You should see a complete backtrace now.
=== Lookup symbols for symbol-less backtraces
The problem with release builds is that they don't contain debug symbols and therefore backtraces usually contain not much more than memory addresses.
Especially backtraces generated by Windows release builds leave a lot to be desired.
ReaLearn has a built-in REAPER action which attempts to look up symbol information for a given error report:
"ReaLearn: Resolve symbols from clipboard".
Works on Windows only.
To be used like this:
. Make sure the PDB for the release build in question is on the search path (see section above).
. Fire up an ReaLearn using exactly that release build.
. Copy the error report to the clipboard.
. Execute the action.
=== Differences between debug levels
==== macOS
Insights:
* The size difference between `debug = 1` and `debug = 2` is almost nothing (both 58 MB), and there's nothing to gain from `debug = 2` in terms of stack traces.
* The size difference between `debug = 0` and `debug = 1` is around 5 MB (53 MB vs. 58 MB), and `debug = 1` only makes a difference if the source files exist (showing line numbers), and only for panics.
* Hard crash stack traces are completely independent of the `debug` value.
They are always helpful except when stripping the symbols.
* `strip = symbols` leads to the smallest binaries (38 MB) but also to completely useless stack traces, both in soft and hard crashes.
However, `split-debuginfo = "packed" seems to fix this at least for hard crashes, at a similar-sized binary (not for panics though) ... even if the DSYM directories are not on disk. What also fixes this for hard crashes is stripping via `strip -u -r`.
We shouldn't do that anymore!
* `strip = debuginfo` leads to an okay size reduction (53 MB) but removes line numbers even if source files exist.
However, `split-debuginfo = "packed"` solves this by creating dSYM directories, as long as they are there.
Takeaway:
* We should build with `debug = 2`
** While `debug = 1` is actually enough for most purposes, it can't hurt building with `debug = 2` since we strip debuginfo anyway.
So there's no size difference for the final binary.
That way we have more debuginfo on the server whenever we need it.
* `strip = debuginfo` is the max we can strip away if we want panics to contain something useful
* `strip = symbols` is only okay if we are fine with bogus stack traces in panics.
In that case, we must use `split-debuginfo = "packed"` to get at least detailed stack traces in case of hard crashes.
* We should use `split-debuginfo = "packed"` in all cases.
* Would be good to find a way to leverage symbols for panic stack traces, but I think there is none.
===== "debug = 0"
.Panic (sources don't matter)
----
7: 0x120531450 - helgobox::infrastructure::plugin::sandbox::execute::h9608b1370cb08106
----
.Hard crash (sources don't matter)
----
4 helgobox-arm64.vst.dylib 0x120527df4 _$LT$helgobox..domain..targets..track_volume_target..TrackVolumeTarget$u20$as$u20$helgoboss_learn..mode..target..Target$GT$::current_value::hff7df2fe68cec5d5 + 20
----
===== "debug = 1"
.Panic if sources exist
----
7: 0x13071ffdc - helgobox::infrastructure::plugin::sandbox::execute::hb564445c2d9211ee
at /Users/helgoboss/Documents/projects/dev/realearn/main/src/infrastructure/plugin/sandbox.rs:3:5
----
.Panic if sources are gone
----
7: 0x140f1ffdc - helgobox::infrastructure::plugin::sandbox::execute::hb564445c2d9211ee
----
.Hard crash (sources don't matter)
----
6 helgobox-arm64.vst.dylib 0x1302d4a64 _$LT$helgobox..domain..mapping..CompoundMappingTarget$u20$as$u20$helgoboss_learn..mode..target..Target$GT$::current_value::hadfcb1be993900b3 + 20 (mapping.rs:2498) [inlined]
----
===== "debug = 2"
.Panic if sources exist
----
7: 0x12160dafc - helgobox::infrastructure::plugin::sandbox::execute::h18fb689d4112e2d3
at /Users/helgoboss/Documents/projects/dev/realearn/main/src/infrastructure/plugin/sandbox.rs:3:5
----
.Panic if sources are gone
----
7: 0x153f81afc - helgobox::infrastructure::plugin::sandbox::execute::h18fb689d4112e2d3
----
.Hard crash (sources don't matter)
----
6 helgobox-arm64.vst.dylib 0x1212d8148 _$LT$helgobox..domain..mapping..CompoundMappingTarget$u20$as$u20$helgoboss_learn..mode..target..Target$GT$::current_value::h15041e3226fa455d + 20 (mapping.rs:2498) [inlined]
----
===== "debug = 2; strip = debuginfo"
.Panic (sources don't matter)
----
7: 0x138616570 - helgobox::infrastructure::plugin::sandbox::execute::hd0d406afe4d62df9
----
.Hard crash (sources don't matter)
----
4 helgobox-arm64.vst.dylib 0x13885aab4 _$LT$helgobox..domain..targets..track_volume_target..TrackVolumeTarget$u20$as$u20$helgoboss_learn..mode..target..Target$GT$::current_value::h35c07d80eb0a312b + 20
----
===== "debug = 2; strip = symbols"
.Soft crash (sources don't matter)
----
0: 0x14bdef0ec - _NSEEL_HOSTSTUB_EnterMutex
1: 0x14bd56f08 - _NSEEL_HOSTSTUB_EnterMutex
2: 0x14bfbe4e4 - _cpp_to_rust_ProjectStateContext_SetTempFlag
3: 0x14bfbddcc - _cpp_to_rust_ProjectStateContext_SetTempFlag
4: 0x14bfbca08 - _cpp_to_rust_ProjectStateContext_SetTempFlag
5: 0x14bfbdabc - _cpp_to_rust_ProjectStateContext_SetTempFlag
6: 0x14c075710 - _cpp_to_rust_ProjectStateContext_SetTempFlag
7: 0x14af50aa0 - _ReaperPluginEntry
8: 0x14bd58fcc - _NSEEL_HOSTSTUB_EnterMutex
9: 0x14bd82844 - _NSEEL_HOSTSTUB_EnterMutex
----
.Hard crash (sources don't matter)
----
7 helgobox-arm64.vst.dylib 0x14b34b5e0 0x14a65c000 + 13563360
----
== Documentation
All documentation is written in AsciiDoc.
- link:doc/realearn/README.adoc[ReaLearn Reference]
- link:ARCHITECTURE.adoc[Software architecture documentation]
Some SVGs embedded in the architecture documentation are generated via link:https://nodejs.org/[NodeJS] / link:https://svgjs.dev/[SVG.js] in link:doc/svg-gen/index.js[].
After modifying this file, you need to execute the following command in the project root:
node doc/svg-gen/index.js
== License check
It's important to make sure that the licenses of all dependencies are compatible with the final license.
We use link:https://github.com/EmbarkStudios/cargo-deny[cargo-deny] for this.
Installation:
[source]
----
cargo install --locked cargo-deny
----
Check:
[source]
----
cargo deny check licenses
----
== License report
It's important to make sure that the licenses of all dependencies are compatible with the final license.
We use link:https://github.com/EmbarkStudios/cargo-deny[cargo-deny] for this.
Installation:
[source]
----
cargo install --locked cargo-about
----
Generate the report:
[source]
----
cargo about generate --fail --workspace --all-features --threshold 0.93 about.hbs > about.html
----
== Release
This serves mainly as a checklist for Helgobox's author.
. Check licenses via `cargo deny check licenses` and make sure the outcome is "licenses ok"
. Update license report (see above)
. Take care of app versioning
** Plug-in repository: Adjust `HOST_API_VERSION` and `MIN_APP_API_VERSION`
** App repository: Adjust `appApiVersion` (macOS, Swift), `APP_API_VERSION` (Windows, C++) and `_minHostApiVersionString` (Dart)
. Bump up the app version number in `pubspec.yaml`.
. Bump up the plug-in version number in link:main/Cargo.toml[main/Cargo.toml].
** Either to a prerelease (e.g. `2.0.0-pre1`) or a final release (e.g. `2.0.0`).
** This is important for having the correct version number displayed in ReaLearn UI.
. Build at least once via `cargo build --features playtime,egui`.
** This updates `Cargo.lock` and is important for not having the `-dirty` display in ReaLearn UI.
. Update the user guide if not done already.
. Create a version tag via `git tag v2.0.0-pre1`.
. Push via `git push origin v2.0.0-pre.1`.
. While GitHub Actions executes the release job, take care of the following.
** Can only be done by @helgoboss because it needs access to the https://github.com/helgoboss/helgoboss-org[helgoboss.org website repository].
** Add a changelog entry in https://github.com/helgoboss/helgoboss-org/blob/master/src/data/projects/realearn/data.yaml[data.yaml].
** In `src/snippets/projects/realearn/repo`, enter `git checkout master` and `git pull` to pull the latest user guide changes.
** Push via `git push origin HEAD` and wait until Netlify deployed the page.
** All the following stuff needs to be done using Netlify's branch preview if it's a prerelease!
** Update https://github.com/helgoboss/reaper-packages/blob/master/index.xml[helgoboss ReaPack index].
*** Generate ReaLearn-only ReaPack index by requesting https://www.helgoboss.org/projects/realearn/reapack.txt[/projects/realearn/reapack.txt].
*** Integrate the generated index by copying everything from `&lt;category name=&quot;Extensions&quot;&gt;` and pasting it to the
https://github.com/helgoboss/reaper-packages/blob/master/index.xml[helgoboss ReaPack index] without overwriting the preset categories on the top of the file.
*** Don't push the index yet!
** Create a REAPER forum ReaLearn thread entry with help of https://www.helgoboss.org/projects/realearn/reaper-forum.txt[/projects/realearn/reaper-forum.txt]
but don't submit yet!
** Download the user guide by requesting https://www.helgoboss.org/projects/realearn/user-guide[/projects/realearn/user-guide].
** Copy the corresponding changelog entry in markdown format by requesting https://www.helgoboss.org/projects/realearn/changelog.md[/projects/realearn/changelog.md].
. Once the release job has finished successfully, edit the not-yet-published release that has been created.
** Paste the copied changelog entry to the release notes.
** Manually add the previously downloaded user guide as release artifact named `realearn-user-guide.pdf`.
. Publish the release.
. Push the https://github.com/helgoboss/reaper-packages/blob/master/index.xml[helgoboss ReaPack index].
. Submit the REAPER forum ReaLearn thread entry.
. Check if synchronization of the ReaPack repository works.
. Update website's `config.yaml` latest versions (for update notifications)
== Troubleshooting
=== Windows: ReaLearn DLL doesn't unload
In REAPER for Windows it's possible to enable complete unload of VST plug-ins (Options -> Settings/Preferences -> Plug-ins -> VST -> Allow complete unload of VST plug-ins).
This also affects ReaLearn.
Removing the last ReaLearn instance should work with and without this flag enabled, it's important to test this.
I ran into a case in which Windows was *not* unloading ReaLearn even though that option was enabled.
The reason turned out to be a registry entry that Windows must have created automatically at some point:
`HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers` -> `C:\REAPER\reaper.exe` with value `$ IgnoreFreeLibrary<realearn.dll>`
Removing this entry made unloading work again.
What a nasty trap!
+8962
View File
File diff suppressed because it is too large Load Diff
+272
View File
@@ -0,0 +1,272 @@
[workspace]
resolver = "2"
members = [
"allocator",
"api",
"dialogs",
"extension",
"macros",
"playtime-clip-engine",
"playtime-api",
"pot",
"pot-browser",
"csi",
"main",
"rx-util",
"swell-ui",
"base",
"artwork-processor",
# Will probably be excluded from the workspace in future
"main/lib/helgoboss-learn"
]
[workspace.dependencies]
# Own
base = { path = "base" }
pot = { path = "pot" }
pot-browser = { path = "pot-browser" }
helgobox-dialogs = { path = "dialogs" }
reaper-common-types = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
reaper-rx = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
reaper-fluent = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
reaper-high = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master", features = ["serde"] }
reaper-medium = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master", features = ["serde"] }
reaper-low = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
reaper-macros = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
rppxml-parser = { git = "https://github.com/helgoboss/reaper-rs.git", branch = "master" }
swell-ui = { path = "swell-ui" }
rx-util = { path = "rx-util" }
playtime-clip-engine = { path = "playtime-clip-engine" }
helgobox-api = { path = "api" }
helgobox-macros = { path = "macros" }
helgobox-allocator = { path = "allocator" }
playtime-api = { path = "playtime-api" }
realearn-csi = { path = "csi" }
helgoboss-learn = { path = "main/lib/helgoboss-learn", features = ["reaper-low"] }
helgoboss-midi = { version = "0.4", features = ["serde", "serde_repr"] }
helgoboss-license-api = { git = "https://github.com/helgoboss/helgoboss-license-api" }
# 3rd-party
scopeguard = "1.1.0"
rxrust = { git = "https://github.com/rxRust/rxRust", rev = "349e50b3197e05926e2378ef8fc45cb67ad43b83" }
indexmap = "2.1.0"
itertools = "0.12.0"
wildmatch = "2.1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde_plain = "1.0.2"
derive_more = "0.99.16"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros", "time"] }
enum-map = { version = "2.4.1", features = ["serde"] }
once_cell = "1.4.0"
strum = { version = "0.25.0", features = ["derive"] }
regex = "1"
walkdir = "2"
either = "1.8.0"
tracing = "0.1.40"
tracing-core = "0.1.32"
tracing-subscriber = "0.3.7"
futures = { version = "0.3", default-features = false }
derivative = "2.2.0"
tempfile = "3.1.0"
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
crossbeam-channel = "0.5"
futures-timer = "3.0.2"
metrics = "0.22.0"
ascii = "1.0"
winapi = "0.3"
bindgen = "0.69.2"
enumflags2 = "0.7.4"
nanoid = "0.4.0"
slug = "0.1.4"
num_enum = "0.7.2"
nom = "7.0.0"
semver = { version = "1.0.17", features = ["serde"] }
enumset = "1.0.12"
mlua = { version = "0.10.2", features = ["vendored", "luau", "serialize", "anyhow"] }
chrono = "0.4.11"
dirs = "5.0.1"
libloading = "0.8"
tokio-stream = { version = "0.1.8", features = ["sync"] }
raw-window-handle = "0.4.2"
egui = "0.21.0"
egui_extras = "0.21.0"
egui-toast = "0.6.0"
bytesize = "1.0.1"
hex = "0.4.2"
rmp-serde = "1.1.1"
anyhow = "1.0.71"
thiserror = "1.0.45"
enum_dispatch = "0.3.6"
simple_moving_average = "1.0.2"
tinyvec = "1.6.0"
erased-serde = "0.4.2"
fragile = "2.0.0"
approx = "0.5.1"
serde_repr = "0.1.5"
serde_with = "3.4.0"
lazycell = "1.2"
rosc = "0.10.1"
rust-ini = "0.20.0"
function_name = "0.3.0"
num = "0.4.1"
logos = "0.13.0"
camino = "1.1.7"
auto_impl = "1.1.0"
bytemuck = "1.13.1"
palette = "0.7.4"
libc = "0.2.153"
path-slash = "0.2.1"
pathdiff = "0.2.1"
open = "5.0.1"
url = "2.5.2"
atomic = "0.6.0"
static_assertions = "1.1.0"
image = { version = "0.25.2", default-features = false }
cached = "0.53.1"
imageproc = "0.25.0"
tower = "0.5.2"
axum = "0.7.9"
axum-server = "0.7.1"
tower-http = "0.6.2"
tonic = "0.12.3"
prost = "0.13.4"
rcgen = "0.12.0"
dns-lookup = "2.0.4"
hostname = "^0.3"
askama = "0.12.1"
serde_ini = "0.2.0"
webbrowser = "0.8.12"
runas = "1.1.0"
qrcode = "0.14.1"
uuid = "1.6.1"
vst = "0.4.0"
c_str_macro = "1.0.2"
arboard = "3.3.0"
smallvec = "1.7.0"
backtrace = "0.3.74"
serde_yaml = "0.8.17"
fasteval = { version = "0.2.4", default-features = false }
maplit = "1.0.2"
metrics-exporter-prometheus = { version = "0.13.0", default-features = false }
embed-resource = "2.4.1"
sysinfo = "0.30.5"
sys-info = "0.9.1"
# We use a special version because of "link_lib_modifiers", which allows us to add "+whole-archive"
# in a convenient way. TODO-low-wait https://github.com/rust-lang/cc-rs/pull/671
cc = { git = "https://github.com/petrochenkov/cc-rs.git", rev = "4d52bd211aeb2b4ddccd1b9c0a0841e03aaaef7c" }
built = "0.7.1"
tts = { git = "https://github.com/helgoboss/tts-rs", branch = "helgoboss-fixes" }
edit = { git = "https://github.com/helgoboss/edit", branch = "realearn" }
async-channel = "2.1.1"
env_logger = "0.10.1"
egui-baseview = { git = "https://github.com/helgoboss/egui-baseview.git", branch = "realearn" }
baseview = { git = "https://github.com/helgoboss/baseview.git", branch = "realearn" }
include_dir = "0.7.3"
whoami = "1.4.1"
streamdeck = "0.9.0"
ab_glyph = "0.2.29"
hidapi = "2.4"
xcap = "0.0.13"
syn = "2.0.48"
darling = "0.20.3"
heck = "0.4.0"
stylua = "0.19.1"
resvg = "0.44.0"
enigo = "0.0.14"
device_query = "1.1.1"
macos-accessibility-client = "0.0.1"
base64 = "0.21.2"
rtrb = "0.3.0"
envcrypt = "0.5.0"
glidesort = "0.1.2"
ebur128 = "0.1.8"
tracing-test = "0.2.4"
rstest = "0.18.2"
lexical-sort = "0.3.1"
splitty = "1.0.1"
riff-io = "0.1.2"
rusqlite = "0.30.0"
sanitize-filename = "0.5.0"
lru = "0.12.1"
pulldown-cmark = "0.9.2"
rfd = "0.12.1"
opener = "0.7.1"
objc2 = "0.3.0-beta.3"
reqwest = { version = "0.12.9", default-features = false }
rustls = { version = "0.23.20", default-features = false }
[profile.release]
debug = 2
[profile.release-strip]
inherits = "release"
# We use this profile on Linux and macOS only. To support Windows 7, we should build with Rust
# version 1.77.2 on Windows. This old version strips away too much due to an implementation
# error in rustc:
#
# "Prior to 1.79, this unintentionally disabled the generation of *.pdb files on MSVC, resulting
# in the absence of symbols."
# (https://doc.rust-lang.org/rustc/codegen-options/index.html#strip).
#
# Not stripping on Windows is okay because the PDB is a separate file anyway. We do the following
# mainly for macOS. There's a long reasoning about the concrete values in CONTRIBUTING.adoc.
# BTW, switching to "symbols" would reduce size even more but leads to useless stack traces when
# panicking.
strip = "debuginfo"
split-debuginfo = "packed"
[profile.dev-llvm-out-of-memory-fix]
inherits = "dev"
# Lately, i686 Windows builds often fail due to "LLVM ERROR: out of memory".
# Lowering the amount of debug information included in the binary is supposed to fix that.
debug = 1
[profile.release-llvm-out-of-memory-fix]
inherits = "release"
# Lately, i686 Windows builds often fail due to "LLVM ERROR: out of memory".
# Lowering the amount of debug information included in the binary is supposed to fix that.
debug = 1
[patch.crates-io]
# TODO-low-wait
# I absolutely need Flutter to be able to connect with ReaLearn based on a LAN IP address. Without this hack, Flutter
# will fail to connect with a TLSV1_ALERT_DECODE_ERROR. As soon as https://github.com/briansmith/webpki/issues/54 is
# closed and rustls has updated the dependency, we should remove this!
#webpki = { git = "https://github.com/helgoboss/webpki.git", branch = "workaround/54-alert-decode-error-0.22.0" }
# Yes! Thanks to the use of axum-server (instead of warp) we don't need this Hyper patch anymore! axum-server seems to
# use lower-level Hyper features and implements a proper graceful shutdown feature on it that allows one to specify a
# timeout. If not all connections are closed until that timeout, it just shuts down forcibly. That's important and
# exactly what we need. We need the server shutdown happen immediately before ReaLearn is unloaded without having to
# force the user to wait for open connections to finish. The following was a temporary hack to make this possible.
# See https://github.com/hyperium/hyper/issues/1885, https://github.com/hyperium/hyper/issues/2386.
# For a moment, I thought that I need to reintroduce this patch for gRPC, which is driven by tonic. Indeed, when using
# tonic's serve_with_shutdown() feature, REAPER doesn't quit until Playtime gRPC client has disconnected :/ But
# fortunately, using the tokio::select! workaround (mentioned in issue 2386) works and *also* closes the ports this
# time! Turns out the same is actually true for axum, so we use select! there as well.
#hyper = { git = "https://github.com/helgoboss/hyper.git", branch = "feature/realearn" }
# We need to use our on "vst" crate that contains a bunch of improvements
vst = { git = "https://github.com/helgoboss/vst-rs.git", branch = "feature/param-props" }
#vst = { path = "../vst-rs" }
# This is for temporary development with local reaper-rs.
#[patch.'https://github.com/helgoboss/reaper-rs.git']
#reaper-common-types = { path = "../reaper-rs/main/common-types" }
#reaper-fluent = { path = "../reaper-rs/main/fluent" }
#reaper-high = { path = "../reaper-rs/main/high" }
#reaper-medium = { path = "../reaper-rs/main/medium" }
#reaper-macros = { path = "../reaper-rs/main/macros" }
#reaper-low = { path = "../reaper-rs/main/low" }
#reaper-rx = { path = "../reaper-rs/main/rx" }
#rppxml-parser = { path = "../reaper-rs/main/rppxml-parser" }
## This is for temporary development with local egui-baseview.
#[patch.'https://github.com/helgoboss/egui-baseview.git']
#egui-baseview = { path = "../egui-baseview" }
#
## This is for temporary development with local baseview.
#[patch.'https://github.com/helgoboss/baseview.git']
#baseview = { path = "../baseview" }
+5
View File
@@ -0,0 +1,5 @@
[target.aarch64-unknown-linux-gnu]
pre-build = ["dpkg --add-architecture arm64 && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 php libxdo-dev:arm64 libudev-dev:arm64 libx11-dev:arm64 libxcursor-dev:arm64 libxcb-dri2-0-dev:arm64 libxcb-icccm4-dev:arm64 libx11-xcb-dev:arm64 mesa-common-dev:arm64 libgl1-mesa-dev:arm64 libglu1-mesa-dev:arm64 libspeechd-dev:arm64 libgtk-3-dev:arm64"]
[target.armv7-unknown-linux-gnueabihf]
pre-build = ["dpkg --add-architecture armhf && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 php libxdo-dev:armhf libudev-dev:armhf libx11-dev:armhf libxcursor-dev:armhf libxcb-dri2-0-dev:armhf libxcb-icccm4-dev:armhf libx11-xcb-dev:armhf mesa-common-dev:armhf libgl1-mesa-dev:armhf libglu1-mesa-dev:armhf libspeechd-dev:armhf libgtk-3-dev:armhf"]
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+40
View File
@@ -0,0 +1,40 @@
= Helgobox: ReaLearn & Playtime
:toc: preamble
:sectnumlevels: 2
image:https://github.com/helgoboss/helgobox/actions/workflows/windows-x86_64.yml/badge.svg[Windows x86_64,link=https://github.com/helgoboss/helgobox/actions/workflows/windows-x86_64.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/windows-i686.yml/badge.svg[Windows i686,link=https://github.com/helgoboss/helgobox/actions/workflows/windows-i686.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/macos-x86_64.yml/badge.svg[macOS x86_64,link=https://github.com/helgoboss/helgobox/actions/workflows/macos-x86_64.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/macos-aarch64.yml/badge.svg[macOS aarch64,link=https://github.com/helgoboss/helgobox/actions/workflows/macos-aarch64.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/linux-x86_64.yml/badge.svg[Linux x86_64,link=https://github.com/helgoboss/helgobox/actions/workflows/linux-x86_64.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/linux-aarch64.yml/badge.svg[Linux aarch64,link=https://github.com/helgoboss/helgobox/actions/workflows/linux-aarch64.yml]
image:https://github.com/helgoboss/helgobox/actions/workflows/linux-armv7.yml/badge.svg[Linux armv7,link=https://github.com/helgoboss/helgobox/actions/workflows/linux-armv7.yml]
image:https://img.shields.io/badge/license-GPL-blue.svg[GitHub license,link=https://raw.githubusercontent.com/helgoboss/realearn/master/LICENSE]
image:https://img.shields.io/badge/Donate-PayPal-orange.svg[Donate,link=https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9CTAK2KKA8Z2S&source=url]
Helgobox is a link:https://www.reaper.fm[REAPER] plug-in that unites multiple creative products by link:https://www.helgoboss.org[Helgoboss].
It currently contains link:https://www.helgoboss.org/projects/realearn[ReaLearn] (a versatile controller integration tool, free) and
link:https://www.helgoboss.org/projects/playtime[Playtime] (a modern session view, paid).
== Installation
See section link:https://docs.helgoboss.org/helgobox/installation.html[Installation] of the Helgobox Reference.
== Usage
See section link:https://docs.helgoboss.org/helgobox/usage.html[Usage] of the Helgobox Reference.
== Documentation
We have:
- link:https://github.com/helgoboss/helgobox/wiki[Helgobox Wiki] (suitable for beginners, includes links to video tutorials)
- link:https://docs.helgoboss.org[Helgoboss Docs website] (extensive references, includes PDF downloads)
== Architecture
See link:ARCHITECTURE.adoc[architecture documentation].
== Contributing
See link:CONTRIBUTING.adoc[contributing documentation].
+79
View File
@@ -0,0 +1,79 @@
<html>
<head>
<style>
@media (prefers-color-scheme: dark) {
body {
background: #333;
color: white;
}
a {
color: skyblue;
}
}
.container {
font-family: sans-serif;
max-width: 800px;
margin: 0 auto;
}
.intro {
text-align: center;
}
.licenses-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.license-used-by {
margin-top: -10px;
}
.license-text {
max-height: 200px;
overflow-y: scroll;
white-space: pre-wrap;
}
</style>
</head>
<body>
<main class="container">
<div class="intro">
<h1>Third Party Licenses</h1>
<p>This page lists the licenses of the projects used in the Helgobox Plug-In and
Extension.</p>
</div>
<h2>Overview of licenses:</h2>
<ul class="licenses-overview">
{{#each overview}}
<li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
{{/each}}
</ul>
<h2>All license text:</h2>
<ul class="licenses-list">
{{#each licenses}}
<li class="license">
<h3 id="{{id}}">{{name}}</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
{{#each used_by}}
<li><a href="{{#if
crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a>
</li>
{{/each}}
</ul>
<pre class="license-text">{{text}}</pre>
</li>
{{/each}}
</ul>
</main>
</body>
</html>
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
accepted = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"BSL-1.0",
"CC0-1.0",
"OFL-1.1",
"Zlib",
"MPL-2.0",
"Unicode-DFS-2016",
"LicenseRef-UFL-1.0",
"OpenSSL",
]
workarounds = [
"ring",
]
private = { ignore = true }
ignore-dev-dependencies = true
ignore-build-dependencies = true
@@ -0,0 +1,9 @@
[package]
name = "helgobox-allocator"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[lints.clippy]
enum_glob_use = "deny"
+312
View File
@@ -0,0 +1,312 @@
//! A variation of https://github.com/Windfisch/rust-assert-no-alloc.git with the following changes:
//!
//! - Automatically offloads deallocation to other thread when in real-time thread, both in debug
//! and release builds
//! - Provides a pluggable deallocation function (for recording stats etc.)
//! - Assertions are active in debug builds only
//! - When assertion violated, it always panics instead of aborting or printing an error (mainly for
//! good testability but also nice otherwise as long as set_alloc_error_hook() is still unstable)
use std::alloc::{GlobalAlloc, Layout, System};
use std::ffi::c_void;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TrySendError};
use std::sync::{mpsc, OnceLock};
use std::thread;
use std::thread::JoinHandle;
#[cfg(debug_assertions)]
thread_local! {
static ALLOC_FORBID_COUNT: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static ALLOC_PERMIT_COUNT: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
static UNDESIRED_ALLOCATION_COUNTER: AtomicU32 = AtomicU32::new(0);
pub fn undesired_allocation_count() -> u32 {
UNDESIRED_ALLOCATION_COUNTER.load(Ordering::Relaxed)
}
#[cfg(not(debug_assertions))]
pub fn assert_no_alloc<T, F: FnOnce() -> T>(func: F) -> T {
// no-op
func()
}
#[cfg(not(debug_assertions))]
pub fn permit_alloc<T, F: FnOnce() -> T>(func: F) -> T {
// no-op
func()
}
#[cfg(debug_assertions)]
/// Calls the `func` closure, but forbids any (de)allocations.
///
/// If a call to the allocator is made, the program will abort with an error,
/// print a warning (depending on the `warn_debug` feature flag. Or ignore
/// the situation, when compiled in `--release` mode with the `disable_release`
///feature flag set (which is the default)).
pub fn assert_no_alloc<T, F: FnOnce() -> T>(func: F) -> T {
// RAII guard for managing the forbid counter. This is to ensure correct behaviour
// when catch_unwind is used
struct Guard;
impl Guard {
fn new() -> Guard {
ALLOC_FORBID_COUNT.with(|c| c.set(c.get() + 1));
Guard
}
}
impl Drop for Guard {
fn drop(&mut self) {
ALLOC_FORBID_COUNT.with(|c| c.set(c.get() - 1));
}
}
let guard = Guard::new(); // increment the forbid counter
let ret = func();
std::mem::drop(guard); // decrement the forbid counter
ret
}
#[cfg(debug_assertions)]
/// Calls the `func` closure. Allocations are temporarily allowed, even if this
/// code runs inside of assert_no_alloc.
pub fn permit_alloc<T, F: FnOnce() -> T>(func: F) -> T {
// RAII guard for managing the permit counter
struct Guard;
impl Guard {
fn new() -> Guard {
ALLOC_PERMIT_COUNT.with(|c| c.set(c.get() + 1));
Guard
}
}
impl Drop for Guard {
fn drop(&mut self) {
ALLOC_PERMIT_COUNT.with(|c| c.set(c.get() - 1));
}
}
let guard = Guard::new(); // increment the forbid counter
let ret = func();
std::mem::drop(guard); // decrement the forbid counter
ret
}
/// The custom allocator that handles the checking.
pub struct HelgobossAllocator<I, D> {
sync_deallocator: D,
async_deallocation_machine: OnceLock<AsyncDeallocationMachine<I>>,
}
struct AsyncDeallocationMachine<I> {
sender: SyncSender<AsyncDeallocatorCommand>,
integration: I,
}
#[derive(Debug)]
pub struct AsyncDeallocatorCommandReceiver(Receiver<AsyncDeallocatorCommand>);
#[derive(Debug)]
enum AsyncDeallocatorCommand {
Stop,
Deallocate {
ptr: *mut u8,
layout: Layout,
},
DeallocateForeign {
value: *mut c_void,
deallocate: unsafe extern "C" fn(value: *mut c_void),
},
}
unsafe impl Send for AsyncDeallocatorCommand {}
pub trait AsyncDeallocationIntegration {
/// Should return `true` if deallocation should be offloaded to the dedicated deallocation
/// thread (e.g. when the current thread is a real-time thread).
fn offload_deallocation(&self) -> bool;
}
pub trait Deallocate {
/// The actual code executed on deallocation.
fn deallocate(&self, ptr: *mut u8, layout: Layout);
}
pub fn start_async_deallocation_thread(
deallocator: impl Deallocate + Send + 'static,
receiver: AsyncDeallocatorCommandReceiver,
) -> JoinHandle<AsyncDeallocatorCommandReceiver> {
thread::Builder::new()
.name("Helgobox deallocator".to_string())
.spawn(move || {
while let Ok(cmd) = receiver.0.recv() {
match cmd {
AsyncDeallocatorCommand::Stop => break,
AsyncDeallocatorCommand::Deallocate { ptr, layout } => {
deallocator.deallocate(ptr, layout);
}
AsyncDeallocatorCommand::DeallocateForeign { value, deallocate } => {
unsafe { deallocate(value) };
}
}
}
receiver
})
.unwrap()
}
impl<I, D> HelgobossAllocator<I, D>
where
I: AsyncDeallocationIntegration,
{
/// Initializes the allocator.
///
/// The deallocator that you pass here will only be used for synchronous deallocation.
///
/// This is just the first step! In order to offload deallocations, you need to call
/// [`Self::init`]! It needs to be 2 steps
pub const fn new(deallocator: D) -> Self {
Self {
sync_deallocator: deallocator,
async_deallocation_machine: OnceLock::new(),
}
}
/// This call is necessary to initialize automatic deallocation offloading (asynchronous
/// deallocation).
///
/// The deallocator that you pass here will only be used for asynchronous deallocation.
///
/// As soon as the given capacity is reached, deallocation will be done synchronously until
/// the deallocation thread has capacity again.
pub fn init(&self, capacity: usize, integration: I) -> AsyncDeallocatorCommandReceiver {
let (sender, receiver) = mpsc::sync_channel::<AsyncDeallocatorCommand>(capacity);
let machine = AsyncDeallocationMachine {
sender,
integration,
};
if self.async_deallocation_machine.set(machine).is_err() {
panic!("attempted to initialize async deallocator more than once");
}
AsyncDeallocatorCommandReceiver(receiver)
}
/// Executes the given deallocation function on the given value, possibly asynchronously.
///
/// This makes it possible to defer deallocation even with values that are not managed by Rust,
/// e.g. C values.
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn dealloc_foreign_value(
&self,
deallocate: unsafe extern "C" fn(ptr: *mut c_void),
value: *mut c_void,
) {
self.dealloc_internal(
|| self.check(None),
|| unsafe {
deallocate(value);
},
|| AsyncDeallocatorCommand::DeallocateForeign { value, deallocate },
)
}
/// Sends a stop signal to the receiver of asynchronous deallocation commands (usually a
/// dedicated thread).
pub fn stop_async_deallocation(&self) {
if let Some(machine) = self.async_deallocation_machine.get() {
let _ = machine.sender.try_send(AsyncDeallocatorCommand::Stop);
}
}
fn dealloc_internal(
&self,
check: impl FnOnce(),
dealloc_sync: impl FnOnce(),
create_async_command: impl FnOnce() -> AsyncDeallocatorCommand,
) {
#[cfg(not(debug_assertions))]
let _ = check;
let Some(deallocation_machine) = self.async_deallocation_machine.get() else {
// We are not initialized yet. Attempt normal synchronous deallocation.
#[cfg(debug_assertions)]
check();
dealloc_sync();
return;
};
if deallocation_machine.integration.offload_deallocation() {
// Deallocation shall be offloaded and we are already initialized.
if let Err(e) = deallocation_machine.sender.try_send(create_async_command()) {
match e {
TrySendError::Full(_) => {
// This is possible if we have very many deallocations in a row or if for
// some reason the async deallocator thread is not running. Then do
// deallocation synchronously.
#[cfg(debug_assertions)]
check();
dealloc_sync();
}
TrySendError::Disconnected(_) => {
// Could happen on shutdown
dealloc_sync();
}
}
}
} else {
// Synchronous deallocation is fine. It's still possible that we are in an
// assert_no_alloc block. In that case, we want to report the violation.
#[cfg(debug_assertions)]
check();
dealloc_sync();
}
}
/// For deallocation of foreign structs (e.g. C structs), the layout is not given.
fn check(&self, layout: Option<Layout>) {
#[cfg(debug_assertions)]
{
let forbid_count = ALLOC_FORBID_COUNT.with(|f| f.get());
let permit_count = ALLOC_PERMIT_COUNT.with(|p| p.get());
if forbid_count > 0 && permit_count == 0 {
// Increase our counter (will be displayed in ReaLearn status line)
UNDESIRED_ALLOCATION_COUNTER.fetch_add(1, Ordering::Relaxed);
// Comment out if you don't want to log the violation
// permit_alloc(|| {
// if let Some(layout) = layout {
// eprintln!(
// "Undesired memory (de)allocation of {} bytes from:\n{:?}",
// layout.size(),
// backtrace::Backtrace::new()
// );
// } else {
// eprintln!(
// "Undesired memory (de)allocation of a foreign value from:\n{:?}",
// backtrace::Backtrace::new()
// );
// }
// });
// Comment out if you don't want to abort
// if let Some(layout) = layout {
// std::alloc::handle_alloc_error(layout);
// }
}
}
let _ = layout;
}
}
unsafe impl<I: AsyncDeallocationIntegration, D: Deallocate> GlobalAlloc
for HelgobossAllocator<I, D>
{
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
#[cfg(debug_assertions)]
self.check(Some(layout));
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
self.dealloc_internal(
|| self.check(Some(layout)),
|| self.sync_deallocator.deallocate(ptr, layout),
|| AsyncDeallocatorCommand::Deallocate { ptr, layout },
);
}
}
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "helgobox-api"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[features]
default = []
[dependencies]
# For being able to use the API macro
helgobox-macros.workspace = true
reaper-low.workspace = true
serde.workspace = true
semver.workspace = true
serde_json.workspace = true
playtime-api.workspace = true
derive_more.workspace = true
strum.workspace = true
num_enum.workspace = true
enum-map.workspace = true
enumset = { workspace = true, features = ["serde", "alloc"] }
helgoboss-license-api.workspace = true
serde_with.workspace = true
[dev-dependencies]
# For testing Lua compatibility
mlua.workspace = true
# For generating Luau type definitions from our Rust APIs
syn = { workspace = true, features = ["full", "extra-traits"] }
# For generating Luau type definitions from our Rust APIs
darling.workspace = true
# For generating Luau type definitions from our Rust APIs
heck.workspace = true
# For formatting generated Luau Type definitions
stylua = { workspace = true, features = ["luau"] }
anyhow.workspace = true
[lints.clippy]
enum_glob_use = "deny"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
use crate::bindings::luau::luau_converter::Hook;
use std::fs;
use std::path::PathBuf;
use stylua_lib::OutputVerification;
mod luau_converter;
/// The final code formatting causes error `has overflowed its stack` by default. You need to set
/// `RUST_MIN_STACK` environment variable (e.g. `RUST_MIN_STACK=104857600`) or execute the test in
/// release mode for this to work.
#[test]
pub fn export_luau() {
struct RealearnApiExportHook;
impl Hook for RealearnApiExportHook {
fn translate_crate_name(&self, rust_crate_ident: &str) -> Option<&'static str> {
match rust_crate_ident {
"playtime_api" => Some("playtime"),
_ => None,
}
}
}
export_luau_internal(
"realearn",
"Contains types and helper functions for building ReaLearn presets",
[
"src/persistence/compartment.rs",
"src/persistence/glue.rs",
"src/persistence/group.rs",
"src/persistence/mapping.rs",
"src/persistence/parameter.rs",
"src/persistence/source.rs",
"src/persistence/target.rs",
],
&RealearnApiExportHook,
["playtime"],
["../playtime-api/src/persistence/mod.rs"],
);
struct PlaytimeApiExportHook;
impl Hook for PlaytimeApiExportHook {
fn include_type(&self, simple_ident: &str) -> bool {
!matches!(
simple_ident,
"FlexibleMatrix"
| "PlaytimeApiError"
| "PlaytimePersistenceRoot"
| "RawEvenQuantization"
)
}
}
export_luau_internal(
"playtime",
"Contains types and helper functions for building Playtime presets",
["../playtime-api/src/persistence/mod.rs"],
&PlaytimeApiExportHook,
[],
[],
);
}
fn export_luau_internal<'a>(
name: &str,
description: &str,
src_files: impl IntoIterator<Item = &'a str>,
hook: &impl Hook,
requires: impl AsRef<[&'a str]>,
foreign_files: impl IntoIterator<Item = &'a str>,
) {
let rust_codes: Vec<_> = src_files
.into_iter()
.map(|src_file| {
let code = fs::read_to_string(src_file).unwrap();
let filtered_code: Vec<_> = code
.lines()
.filter(|line| !line.starts_with("//!"))
.collect();
filtered_code.join("\n")
})
.collect();
let merged_rust_code = rust_codes.join("\n\n");
let rust_file = parse_rust_code(&merged_rust_code);
let foreign_rust_files: Vec<_> = foreign_files
.into_iter()
.map(|path| {
let code = fs::read_to_string(path).unwrap();
parse_rust_code(&code)
})
.collect();
let luau_file = luau_converter::LuauFile::new(&rust_file, hook, &foreign_rust_files);
use std::fmt::Write;
let mut luau_code = "--!strict\n\n--- Attention: This file is generated from Rust code! Don't modify it directly!\n\n".to_string();
for req in requires.as_ref() {
writeln!(&mut luau_code, "local {req} = require(\"{req}\")").unwrap();
}
writeln!(&mut luau_code, "\n--- {description}").unwrap();
write!(&mut luau_code, "{luau_file}").unwrap();
let luau_code = stylua_lib::format_code(
&luau_code,
Default::default(),
None,
OutputVerification::Full,
)
.unwrap();
let dest_file = PathBuf::from(format!("../resources/api/luau/{name}.luau"));
fs::write(&dest_file, luau_code).unwrap();
}
fn parse_rust_code(code: &str) -> syn::File {
syn::parse_file(code).expect("unable to parse Rust file")
}
@@ -0,0 +1 @@
mod luau;
+9
View File
@@ -0,0 +1,9 @@
pub mod persistence;
pub mod runtime;
/// Bindings are generated as result of unit tests.
#[cfg(test)]
mod bindings;
mod util;
@@ -0,0 +1,41 @@
use crate::persistence::*;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
/// Complete content of a ReaLearn compartment, including mappings, groups, parameters etc.
#[derive(Default, Serialize, Deserialize)]
pub struct Compartment {
/// Settings of the default group in this compartment.
///
/// Group fields `id` and `name` will be ignored for the default group.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_group: Option<Group>,
/// All parameters in this compartment
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<Parameter>>,
/// All mapping groups in this compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<Group>>,
/// All mappings in this compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub mappings: Option<Vec<Mapping>>,
/// Lua code that will be compiled only once and can then be reused in various Lua scripts within mappings.
///
/// This code should return a value. This value will then be made available to the scripts. How exactly, depends
/// on the particular kind of script. In most cases, you want to return a table that contains functions, variables
/// and other stuff that you want to make available in your scripts.
#[serde(skip_serializing_if = "Option::is_none")]
pub common_lua: Option<String>,
/// Arbitrarily formed data in this compartment.
///
/// The first level is a key-value map where a key represents a sort of namespace. E.g. data that's relevant
/// for the application ReaLearn Companion has key "companion" and data relevant for the application Playtime has
/// the key "playtime". Everything nested below is application-specific.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_data: Option<HashMap<String, serde_json::Value>>,
/// Can contain text notes, e.g. a helpful description of this compartment, instructions etc.
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub unknown_props: Option<BTreeMap<String, serde_json::Value>>,
}
@@ -0,0 +1,154 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct ControllerConfig {
/// All configured controllers.
#[serde(default)]
pub controllers: Vec<Controller>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Controller {
/// ID of the controller.
///
/// Should be unique on a particular machine and ideally globally unique (good for potential
/// merging scenarios).
pub id: String,
/// Descriptive name of the controller.
///
/// If one uses multiple controllers of the same kind, this should make clear which
/// particular controller instance we are talking about.
pub name: String,
/// If not enabled, no auto units will be created for that controller.
#[serde(default)]
pub enabled: bool,
/// Controller color.
///
/// Used e.g. for the control unit rectangle.
#[serde(skip_serializing_if = "Option::is_none")]
pub palette_color: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection: Option<ControllerConnection>,
/// Default controller preset to load whenever an auto unit with this controller is created.
///
/// ReaLearn has mechanisms to automatically identify and load a suitable controller preset
/// depending on which main preset is loaded. If it has to choose between multiple
/// candidates and no default controller preset is set, it will prefer a factory controller
/// preset. If a default controller preset is set and it satisfies the needs of the main preset,
/// it will use this one instead. It will also use the default controller preset if it can't
/// automatically identify the correct one.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_controller_preset: Option<CompartmentPresetId>,
/// Default main preset to load whenever an auto unit with this controller is created.
// TODO-high-playtime-after-release The plan is to introduce an advanced mode where you don't just set a main preset but can define
// a decision table per controller. It's a list of rules. A rule is made from conditions
// (fixed number, typed, every condition optional, AND) and effects (fixed number, typed, optional).
// The default_main_preset would act as fallback, as last line in the list of rules, which doesn't
// define any additional condition.
// Possible conditions:
// - Playtime matrix (if at least one instance is active that has a Playtime matrix)
// - Active pot unit?
// Possible effects:
// - Main preset (optional)
// - Use auto-load in unit
// "Use auto-load in unit" uses the already existing global FX-to-preset links to do auto-load within
// the unit. FX-to-preset links already is very similar to a decision table. It's going to be a
// second global decision table, but a subordinate one, which acts on a single unit (not by adding/removing
// units). The main preset defined in the controller rule should act as fallback if none of the
// FX-to-preset links was effective. This should be implemented by the existing auto-load. Maybe by
// memorizing the preset that was active when "Auto-load depending on instance FX" was active.
// FX-to-preset links are not 100% a decision table already. Because their order doesn't matter.
// But we can turn it into one by sorting the list according to our current automatic ranking
// (a migration step). Making it a decision table would have the nice effect that the user has
// much influence and it's immediately clear why something happens, no implicit ranking. The list
// of conditions can be easily extended. E.g. we could not just react on what unit FX is
// active but also unit track and so on. In any case, we should add a "Controller" condition, so
// that one e.g. can load a different main preset depending on which FX is focused AND which controller
// is connected.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_main_preset: Option<CompartmentPresetId>,
}
/// The way a controller is connected to ReaLearn.
///
/// Protocol-specific.
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ControllerConnection {
Midi(MidiControllerConnection),
Osc(OscControllerConnection),
}
/// A connection via MIDI.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MidiControllerConnection {
/// The expected response to a MIDI device inquiry.
///
/// Example: "F0 7E 00 06 02 00 20 6B 02 00 04 02 0E 02 01 01 F7"
///
/// Can be used by ReaLearn to verify whether the device connected to a port is the correct one.
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_response: Option<String>,
/// The MIDI input port to which this controller is usually connected on this machine.
#[serde(skip_serializing_if = "Option::is_none")]
pub input_port: Option<MidiInputPort>,
/// The MIDI output port to which this controller is usually connected on this machine.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_port: Option<MidiOutputPort>,
}
/// A connection via OSC.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct OscControllerConnection {
#[serde(skip_serializing_if = "Option::is_none")]
pub osc_device_id: Option<OscDeviceId>,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct MidiInputPort(u32);
impl MidiInputPort {
pub fn new(raw: u32) -> Self {
Self(raw)
}
pub fn get(&self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct MidiOutputPort(u32);
impl MidiOutputPort {
pub fn new(raw: u32) -> Self {
Self(raw)
}
pub fn get(&self) -> u32 {
self.0
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct OscDeviceId(String);
impl OscDeviceId {
pub fn get(&self) -> &str {
&self.0
}
}
/// ID of a controller or main preset (which one depends on the context).
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct CompartmentPresetId(String);
impl CompartmentPresetId {
pub fn new(raw: String) -> Self {
Self(raw)
}
pub fn get(&self) -> &str {
&self.0
}
}
@@ -0,0 +1,252 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(PartialEq, Default, Serialize, Deserialize)]
pub struct Glue {
//region Relevant for control and feedback
#[serde(skip_serializing_if = "Option::is_none")]
pub absolute_mode: Option<AbsoluteMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reverse: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub out_of_range_behavior: Option<OutOfRangeBehavior>,
//endregion
//region Relevant for control only (might change in future)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_value_sequence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub round_target_value: Option<bool>,
//endregion
//region Relevant for control only (guaranteed)
#[serde(skip_serializing_if = "Option::is_none")]
pub wrap: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jump_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub takeover_mode: Option<TakeoverMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_transformation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_size_interval: Option<Interval<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_factor_interval: Option<Interval<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub button_filter: Option<ButtonFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoder_filter: Option<EncoderFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relative_mode: Option<RelativeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interaction: Option<Interaction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fire_mode: Option<FireMode>,
//endregion
//region Relevant for feedback only (guaranteed)
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback: Option<Feedback>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_value_table: Option<FeedbackValueTable>,
//endregion
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum FeedbackValueTable {
FromTextToDiscrete(DiscreteFeedbackValueTableContent),
FromTextToContinuous(ContinuousFeedbackValueTableContent),
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct DiscreteFeedbackValueTableContent {
pub value: HashMap<String, u32>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ContinuousFeedbackValueTableContent {
pub value: HashMap<String, f64>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum AbsoluteMode {
#[default]
Normal,
IncrementalButton,
ToggleButton,
MakeRelative,
PerformanceControl,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum RelativeMode {
Normal,
MakeAbsolute,
}
impl Default for RelativeMode {
fn default() -> Self {
Self::Normal
}
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum FireMode {
Normal(NormalFireMode),
AfterTimeout(AfterTimeoutFireMode),
AfterTimeoutKeepFiring(AfterTimeoutKeepFiringFireMode),
OnSinglePress(OnSinglePressFireMode),
OnDoublePress,
}
impl Default for FireMode {
fn default() -> Self {
Self::Normal(Default::default())
}
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct NormalFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub press_duration_interval: Option<Interval<u32>>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct AfterTimeoutFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct AfterTimeoutKeepFiringFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
pub rate: Option<u32>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct OnSinglePressFireMode {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_duration: Option<u32>,
}
#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VirtualColor {
Rgb(RgbColor),
Prop(PropColor),
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RgbColor(pub u8, pub u8, pub u8);
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PropColor {
pub prop: String,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum OutOfRangeBehavior {
MinOrMax,
Min,
Ignore,
}
impl Default for OutOfRangeBehavior {
fn default() -> Self {
Self::MinOrMax
}
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum TakeoverMode {
Off,
PickUpTolerant,
PickUp,
LongTimeNoSee,
Parallel,
CatchUp,
}
impl Default for TakeoverMode {
fn default() -> Self {
Self::Off
}
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum ButtonFilter {
PressOnly,
ReleaseOnly,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum EncoderFilter {
IncrementOnly,
DecrementOnly,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Interaction {
SameControl,
SameTargetValue,
InverseControl,
InverseTargetValue,
InverseTargetValueOnOnly,
InverseTargetValueOffOnly,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct FeedbackCommons {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<VirtualColor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color: Option<VirtualColor>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Feedback {
Numeric(NumericFeedback),
Text(TextFeedback),
Dynamic(DynamicFeedback),
}
impl Default for Feedback {
fn default() -> Self {
Self::Numeric(NumericFeedback::default())
}
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct NumericFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub transformation: Option<String>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct TextFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_expression: Option<String>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct DynamicFeedback {
#[serde(flatten)]
pub commons: FeedbackCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<String>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Interval<T>(pub T, pub T);
@@ -0,0 +1,22 @@
use crate::persistence::*;
use serde::{Deserialize, Serialize};
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Group {
/// An optional ID that you can assign to this group in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all groups in the same compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activation_condition: Option<ActivationCondition>,
}
@@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct InstanceSettings {
pub control: InstanceControlSettings,
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct InstanceControlSettings {
/// Whether auto units will be created for all controllers that have a main preset set.
pub global_control_enabled: bool,
// Local overrides of controller settings.
//
// If global control is enabled, each override will alter the behavior of the corresponding
// controller.
//
// If global control is disabled, each override which has a main preset set will enable
// that specific controller. This way you can selectively enable controllers, either with
// the global default preset or with your own one.
//
// TODO-high-playtime-after-release Controller overrides are not yet implemented because about doubts.
// What if the user deletes the controller? Then all project/instance that have
// an override of that controller will reference a now gone controller. Consequently, the
// overrides will not work anymore. Ideas:
// 1. Memorize the original controller data as part of the override and update it whenever
// the global controller changes. Then we can use that data if the controller is gone.
// => the project will still work but it will be disconnected.
// 2. Don't actually use the global controller anymore once there's an override ... that's like
// disconnecting immediately.
// 3. GOOD SOLUTION FOR NOW Don't provide the possibility for overrides. Force user to create
// a ReaLearn setup that is completely self-containing (the other extreme instead of something
// in-between), including the preset content.
// 4. Is the controller role idea better after all? I don't think so. Yes, it allows a bit
// more instance-specific tuning of global control without depending on particular
// controllers. However, the kind of tuning that it allows is far from exhaustive. Also,
// it's opinionated (clip/daw roles) and has other issues (being harder to grasp and
// awkward when it comes to all-in-one controllers that do both clip/DAW control).
// 5. INTERESTING If all we need is the possibility to disable e.g. global DAW control for a
// specific instance, we could simply let the main preset declare which usage role
// it implements (e.g. the "DAW control" role) and allow the instance to switch on/off
// roles - which will cause the main preset to be loaded or not.
// pub controller_overrides: Vec<ControllerOverride>,
}
// #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
// pub struct ControllerOverride {
// /// ID of the controller which should be overridden.
// pub controller_id: String,
// /// If this is `None`, the controller default main preset will be used.
// pub main_preset: Option<CompartmentPresetId>,
// }
@@ -0,0 +1,190 @@
use super::*;
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Default, Serialize, Deserialize)]
pub struct Mapping {
/// An optional ID that you can assign to this mapping in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all mappings in the compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_in_projection: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activation_condition: Option<ActivationCondition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_activate: Option<LifecycleHook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_deactivate: Option<LifecycleHook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub glue: Option<Glue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<Target>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_audio_feedback: Option<SuccessAudioFeedback>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unprocessed: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct LifecycleHook {
#[serde(skip_serializing_if = "Option::is_none")]
pub send_midi_feedback: Option<Vec<SendMidiFeedbackAction>>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum SendMidiFeedbackAction {
Raw { message: RawMidiMessage },
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RawMidiMessage {
HexString(String),
ByteArray(Vec<u8>),
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum SuccessAudioFeedback {
Simple,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ActivationCondition {
Modifier(ModifierActivationCondition),
Bank(BankActivationCondition),
Eel(EelActivationCondition),
Expression(ExpressionActivationCondition),
TargetValue(TargetValueActivationCondition),
}
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct ModifierActivationCondition {
pub modifiers: Option<Vec<ModifierState>>,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct ModifierState {
pub parameter: ParamRef,
pub on: bool,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct BankActivationCondition {
pub parameter: ParamRef,
pub bank_index: u32,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct EelActivationCondition {
pub condition: String,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct ExpressionActivationCondition {
pub condition: String,
}
#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct TargetValueActivationCondition {
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<String>,
pub condition: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParamRef {
Index(u32),
Key(String),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VirtualControlElementId {
Indexed(u32),
Named(String),
}
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
Debug,
Default,
Serialize,
Deserialize,
Display,
strum::EnumIter,
TryFromPrimitive,
IntoPrimitive,
)]
#[repr(usize)]
pub enum VirtualControlElementCharacter {
/// A control element that can represent more than 2 states.
#[default]
#[serde(alias = "multi")]
Multi,
/// A control element that can represent at a maximum 2 states.
#[serde(alias = "button")]
Button,
}
#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OscArgument {
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "kind")]
pub arg_kind: Option<OscArgKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_range: Option<Interval<f64>>,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum OscArgKind {
Float,
Double,
Bool,
Nil,
Inf,
Int,
String,
Blob,
Time,
Long,
Char,
Color,
Midi,
Array,
}
impl Default for OscArgKind {
fn default() -> Self {
Self::Float
}
}
@@ -0,0 +1,118 @@
mod compartment;
mod controller;
mod glue;
mod group;
mod instance;
mod mapping;
mod parameter;
mod preset;
mod root;
mod session;
mod source;
mod target;
pub use compartment::*;
pub use controller::*;
pub use glue::*;
pub use group::*;
pub use instance::*;
pub use mapping::*;
pub use parameter::*;
pub use preset::*;
pub use root::*;
pub use session::*;
pub use source::*;
pub use target::*;
use semver::Version;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Envelope<T> {
#[serde(default)]
pub version: Option<Version>,
pub value: T,
}
impl<T> Envelope<T> {
pub fn new(version: Option<Version>, value: T) -> Self {
Self { version, value }
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ApiObject {
/// A Playtime matrix.
ClipMatrix(Envelope<Box<Option<playtime_api::persistence::FlexibleMatrix>>>),
/// Main compartment.
MainCompartment(Envelope<Box<Compartment>>),
/// Controller compartment.
ControllerCompartment(Envelope<Box<Compartment>>),
/// A flat list of mappings.
Mappings(Envelope<Vec<Mapping>>),
/// A single mapping.
Mapping(Envelope<Box<Mapping>>),
}
impl ApiObject {
pub fn into_mappings(self) -> Option<Envelope<Vec<Mapping>>> {
match self {
ApiObject::Mappings(Envelope {
value: mappings,
version,
}) => Some(Envelope::new(version, mappings)),
ApiObject::Mapping(Envelope { value: m, version }) => {
Some(Envelope::new(version, vec![*m]))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn example_to_json() {
let mapping = Mapping {
id: Some("volume".to_string()),
name: Some("Volume".to_string()),
tags: Some(vec!["mix".to_string(), "master".to_string()]),
group: Some("faders".to_string()),
visible_in_projection: Some(true),
enabled: Some(true),
control_enabled: Some(true),
feedback_enabled: Some(true),
activation_condition: None,
source: Some(Source::MidiControlChangeValue(
MidiControlChangeValueSource {
feedback_behavior: Some(FeedbackBehavior::Normal),
channel: Some(0),
controller_number: Some(64),
character: Some(SourceCharacter::Button),
fourteen_bit: Some(false),
},
)),
glue: Some(Glue {
source_interval: Some(Interval(0.3, 0.7)),
..Default::default()
}),
target: None,
..Default::default()
};
serde_json::to_string_pretty(&mapping).unwrap();
// std::fs::write("src/schema/test/example.json", json).unwrap();
}
#[test]
fn example_from_lua() {
use mlua::{Lua, LuaSerdeExt};
let lua = Lua::new();
let value = lua.load(include_str!("test/example.lua")).eval().unwrap();
let mapping: Mapping = lua.from_value(value).unwrap();
serde_json::to_string_pretty(&mapping).unwrap();
// std::fs::write("src/schema/test/example_from_lua.json", json).unwrap();
}
}
@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
#[derive(Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Parameter {
pub index: u32,
/// An optional ID that you can assign to this parameter in order to refer
/// to it from somewhere else.
///
/// This ID should be unique within all parameters in the same compartment.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_count: Option<NonZeroU32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_labels: Option<Vec<String>>,
}
@@ -0,0 +1,248 @@
use crate::util::deserialize_null_default;
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use strum::{Display, EnumString};
/// Meta data that is common to both main and controller presets.
///
/// Preset meta data is everything that is loaded right at startup in order to be able to
/// display a list of preset, do certain validations etc. It doesn't include the preset
/// content which is necessary to actually use the preset (e.g. it doesn't include the mappings).
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct CommonPresetMetaData {
/// Display name of the preset.
pub name: String,
/// The ReaLearn version for which this preset was built.
///
/// This can effect the way the preset is loaded, e.g. it can lead to different interpretation
/// or migration of properties. So care should be taken to set this correctly!
///
/// If `None`, it's assumed that it was built for a very old version (< 1.12.0-pre18) that
/// didn't have the versioning concept yet.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
#[serde(alias = "version")]
pub realearn_version: Option<Version>,
/// Author of the preset.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub author: Option<String>,
/// Preset description (prose).
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
/// Preset setup instructions (prose).
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub setup_instructions: Option<String>,
/// Device manufacturer.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub device_manufacturer: Option<String>,
/// Original name of the device.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub device_name: Option<String>,
}
/// Metadata that is specific to controller presets.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct ControllerPresetMetaData {
/// MIDI identity compatibility pattern.
///
/// Will be used for auto-adding controllers and for finding the correct controller preset when calculating auto
/// units.
#[serde(
default,
deserialize_with = "deserialize_null_default",
skip_serializing_if = "Option::is_none"
)]
pub midi_identity_pattern: Option<String>,
/// Possible MIDI identity compatibility patterns.
///
/// Will be used for auto-adding controllers and for finding the correct controller preset when calculating auto
/// units.
///
/// It should only be provided if the device in question doesn't reply to device queries or if it exposes
/// multiple ports which all respond with the same device identity and only one of the ports is the correct one.
/// Example: APC Key 25 mk2, which exposes a "Control" and a "Keys" port.
///
/// It's a list because names often differ between operating systems. ReaLearn will match any in the list.
#[serde(default)]
pub midi_output_port_patterns: Vec<MidiPortPattern>,
/// Provided virtual control schemes.
///
/// Will be used for finding the correct controller preset when calculating auto units.
///
/// The order matters! It directly influences the choice of the best-suited main presets. In particular,
/// schemes that are more specific to this particular controller (e.g. "novation/launchpad-mk3") should come first.
/// Generic schemes (e.g. "grid") should come last. When auto-picking a main preset, matches of more specific
/// schemes will be favored over less specific ones.
#[serde(default)]
pub provided_schemes: Vec<VirtualControlSchemeId>,
}
#[derive(Clone, Eq, PartialEq, Debug, SerializeDisplay, DeserializeFromStr)]
pub struct MidiPortPattern {
pub scope: Option<MidiPortPatternScope>,
pub name_pattern: String,
}
impl MidiPortPattern {
pub fn scope_matches(&self) -> bool {
let Some(scope) = self.scope else {
return true;
};
match scope {
MidiPortPatternScope::Windows => cfg!(windows),
MidiPortPatternScope::MacOs => cfg!(target_os = "macos"),
MidiPortPatternScope::Linux => cfg!(target_os = "linux"),
}
}
}
impl FromStr for MidiPortPattern {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((scope_string, name_pattern)) = s.split_once(':') {
if let Ok(scope) = MidiPortPatternScope::from_str(scope_string) {
// MIDI port pattern with scope restriction
let pattern = Self {
scope: Some(scope),
name_pattern: name_pattern.to_string(),
};
return Ok(pattern);
}
}
// MIDI port pattern without scope restriction
let pattern = Self {
scope: None,
name_pattern: s.to_string(),
};
Ok(pattern)
}
}
impl Display for MidiPortPattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(s) = self.scope {
write!(f, "{s}:")?;
}
self.name_pattern.fmt(f)?;
Ok(())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum MidiPortPatternScope {
Windows,
MacOs,
Linux,
}
/// Metadata that is specific to main presets.
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MainPresetMetaData {
/// Used virtual control schemes.
///
/// Will be used for finding the correct controller preset when calculating auto units.
#[serde(default)]
pub used_schemes: HashSet<VirtualControlSchemeId>,
/// A set of features that a Helgobox instance needs to provide for the preset to make sense.
///
/// See [instance_features].
///
/// Will be used for determining whether an auto unit should be created for a specific instance
/// or not. Example: If the required feature is "playtime" and a controller is configured with
/// this main preset but the instance doesn't contain a Playtime matrix, this instance will
/// not load the main preset.
#[serde(default)]
pub required_features: HashSet<String>,
}
impl MainPresetMetaData {
pub fn requires_playtime(&self) -> bool {
self.required_features.contains(instance_features::PLAYTIME)
}
/// Higher specificity means that the main preset uses a scheme provided by the controller that's more specific to
/// that particular controller (and therefore better suited).
///
/// When picking the "best" main preset for a given controller preset, this is the first criteria taken into
/// account, if there are two competing main preset candidates.
///
/// Given a controller preset that provides the schemes [bla, foo].
/// If main preset A uses schemes [bla] and main preset B [foo], we want main preset A to win because
/// "bla" comes first in the controller preset's list of provided schemes, meaning that "bla" is the
/// more specific scheme.
///
/// An example where this matters in practice:
/// - Controller preset "Launchpad Pro mk3 - Live mode" provides schemes [novation/launchpad-pro-mk3/live, grid]
/// - Main preset "Generic grid controller - Playtime" uses schemes [grid]
/// - Main preset "Launchpad Pro mk3 - Playtime" uses schemes [novation/launchpad-pro-mk3/live]
///
/// Without that rule, it could easily happen that "Generic grid controller - Playtime" will be picked. Bad!
///
/// Returns `None` if no scheme matches.
pub fn calc_scheme_specificity(
&self,
provided_schemes: &[VirtualControlSchemeId],
) -> Option<u8> {
let lowest_matching_index = self
.used_schemes
.iter()
.filter_map(|used_scheme| provided_schemes.iter().position(|s| s == used_scheme))
.min()?;
Some((provided_schemes.len() - lowest_matching_index) as u8)
}
/// Higher coverage means that the main preset uses more schemes provided by the controller.
///
/// When picking the "best" main preset for a given controller preset, this is the second criteria taken into
/// account, if the specificity of two main preset candidates is the same.
pub fn calc_scheme_coverage(&self, provided_schemes: &[VirtualControlSchemeId]) -> u8 {
self.used_schemes
.iter()
.filter(|used_scheme| provided_schemes.contains(used_scheme))
.count() as u8
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
pub struct VirtualControlSchemeId(String);
impl VirtualControlSchemeId {
pub fn get(&self) -> &str {
&self.0
}
}
/// Known instance features.
pub mod instance_features {
/// Instance owns a Playtime matrix.
pub const PLAYTIME: &str = "playtime";
}
@@ -0,0 +1,6 @@
use crate::persistence::session::Session;
/// Only used for JSON schema generation.
pub struct RealearnPersistenceRoot {
_session: Session,
}
@@ -0,0 +1,20 @@
use crate::persistence::*;
/// Only used for JSON schema generation at the moment.
pub struct Session {
_main_compartment: Option<Compartment>,
_clip_matrix: Option<playtime_api::persistence::Matrix>,
_mapping_snapshots: Vec<MappingSnapshot>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct MappingSnapshot {
pub id: String,
pub mappings: Vec<MappingInSnapshot>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct MappingInSnapshot {
pub id: String,
pub target_value: TargetValue,
}
@@ -0,0 +1,394 @@
use crate::persistence::{OscArgument, VirtualControlElementCharacter, VirtualControlElementId};
use derive_more::Display;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::EnumIter;
#[derive(PartialEq, Default, Serialize, Deserialize)]
#[serde(tag = "kind")]
#[allow(clippy::enum_variant_names)]
pub enum Source {
// None
#[default]
None,
// REAPER
MidiDeviceChanges,
RealearnInstanceStart,
RealearnCompartmentLoaded,
Timer(TimerSource),
RealearnParameter(RealearnParameterSource),
Speech,
// MIDI
MidiNoteVelocity(MidiNoteVelocitySource),
MidiNoteKeyNumber(MidiNoteKeyNumberSource),
MidiPolyphonicKeyPressureAmount(MidiPolyphonicKeyPressureAmountSource),
MidiControlChangeValue(MidiControlChangeValueSource),
MidiProgramChangeNumber(MidiProgramChangeNumberSource),
MidiSpecificProgramChange(MidiSpecificProgramChangeSource),
MidiChannelPressureAmount(MidiChannelPressureAmountSource),
MidiPitchBendChangeValue(MidiPitchBendChangeValueSource),
MidiParameterNumberValue(MidiParameterNumberValueSource),
MidiClockTempo,
MidiClockTransport(MidiClockTransportSource),
MidiRaw(MidiRawSource),
MidiScript(MidiScriptSource),
MackieLcd(MackieLcdSource),
XTouchMackieLcd(XTouchMackieLcdSource),
MackieSevenSegmentDisplay(MackieSevenSegmentDisplaySource),
SlKeyboardDisplay(SlKeyboardDisplaySource),
SiniConE24Display(SiniConE24DisplaySource),
LaunchpadProScrollingTextDisplay,
// OSC
Osc(OscSource),
// Keyboard
Key(KeySource),
// StreamDeck
StreamDeck(StreamDeckSource),
// Virtual
Virtual(VirtualSource),
}
// Only makes sense for sources that support both control *and* feedback.
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum FeedbackBehavior {
Normal,
SendFeedbackAfterControl,
PreventEchoFeedback,
}
impl Default for FeedbackBehavior {
fn default() -> Self {
Self::Normal
}
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiNoteVelocitySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiNoteKeyNumberSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiPolyphonicKeyPressureAmountSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiControlChangeValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller_number: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fourteen_bit: Option<bool>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiProgramChangeNumberSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiSpecificProgramChangeSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub program_number: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiChannelPressureAmountSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiPitchBendChangeValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiParameterNumberValueSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fourteen_bit: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiClockTransportSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<MidiClockTransportMessage>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiRawSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<SourceCharacter>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MidiScriptSource {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "kind")]
pub script_kind: Option<MidiScriptKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<String>,
}
/// Kind of a MIDI script
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
)]
#[repr(usize)]
pub enum MidiScriptKind {
#[default]
#[serde(alias = "eel")]
#[display(fmt = "EEL")]
Eel,
#[serde(alias = "lua")]
#[display(fmt = "Lua")]
Lua,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Serialize, Deserialize)]
pub enum SourceCharacter {
#[default]
Range,
Button,
// 127 = decrement; 0 = none; 1 = increment
Relative1,
// 63 = decrement; 64 = none; 65 = increment
Relative2,
// 65 = decrement; 0 = none; 1 = increment
Relative3,
StatefulButton,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum MidiClockTransportMessage {
#[default]
Start,
Continue,
Stop,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MackieLcdSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub extender_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct XTouchMackieLcdSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub extender_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SlKeyboardDisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub section: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u8>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct MackieSevenSegmentDisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<MackieSevenSegmentDisplayScope>,
}
#[derive(Copy, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum MackieSevenSegmentDisplayScope {
All,
#[default]
Assignment,
Tc,
TcHoursBars,
TcMinutesBeats,
TcSecondsSub,
TcFramesTicks,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SiniConE24DisplaySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_index: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub item_index: Option<u8>,
}
#[derive(Default, PartialEq, Serialize, Deserialize)]
pub struct OscSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_behavior: Option<FeedbackBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub argument: Option<OscArgument>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relative: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback_arguments: Option<Vec<String>>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct RealearnParameterSource {
pub parameter_index: u32,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct TimerSource {
pub duration: u64,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct KeySource {
#[serde(skip_serializing_if = "Option::is_none")]
pub keystroke: Option<Keystroke>,
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct StreamDeckSource {
pub button_index: u32,
#[serde(default)]
pub button_design: StreamDeckButtonDesign,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonDesign {
#[serde(default)]
pub background: StreamDeckButtonBackground,
#[serde(default)]
pub foreground: StreamDeckButtonForeground,
#[serde(default)]
pub static_text: String,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonForeground {
#[default]
None,
FadingColor(StreamDeckButtonFadingColorForeground),
FadingImage(StreamDeckButtonFadingImageForeground),
SlidingImage(StreamDeckButtonSlidingImageForeground),
FullBar(StreamDeckButtonFullBarForeground),
Knob(StreamDeckButtonKnobForeground),
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum StreamDeckButtonBackground {
Color(StreamDeckButtonColorBackground),
Image(StreamDeckButtonImageBackground),
}
impl Default for StreamDeckButtonBackground {
fn default() -> Self {
Self::Color(StreamDeckButtonColorBackground::default())
}
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFadingImageForeground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonSlidingImageForeground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFadingColorForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonFullBarForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonKnobForeground {}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonImageBackground {
pub path: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize)]
pub struct StreamDeckButtonColorBackground {}
#[derive(Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Keystroke {
pub modifiers: u8,
pub key: u16,
}
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct VirtualSource {
pub id: VirtualControlElementId,
#[serde(skip_serializing_if = "Option::is_none")]
pub character: Option<VirtualControlElementCharacter>,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
return {
name = "Pedal to Delay",
source = {
kind = "MidiControlChangeValue",
channel = 0,
controller_number = 64,
character = "Button",
fourteen_bit = false,
},
glue = {
target_interval = {0, 0.53},
jump_interval = {0, 0.53},
step_size_interval = {0.01, 0.01},
step_factor_interval = {1, 1},
},
target = {
kind = "FxParameterValue",
parameter = {
address = "ById",
fx = {
address = "ById",
chain = {
address = "Track",
},
id = "22FD4FC0-A4DD-4E6F-BCB3-38F242B557B2",
},
index = 23,
},
},
}
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum GlobalInfoEvent {
Generic(GenericGlobalInfoEvent),
AutoAddedController(AutoAddedControllerEvent),
PlaytimeActivationSucceeded,
PlaytimeActivationFailed,
}
impl GlobalInfoEvent {
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic(GenericGlobalInfoEvent {
message: message.into(),
})
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct GenericGlobalInfoEvent {
pub message: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct AutoAddedControllerEvent {
pub controller_id: String,
}
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum InstanceInfoEvent {
Generic(GenericInstanceInfoEvent),
/// If attempting to MIDI-learn but the track is either not armed or the input monitoring mode
/// is not suitable.
MidiLearnFromFxInputButTrackNotArmed,
MidiLearnFromFxInputButTrackHasAudioInput,
}
impl InstanceInfoEvent {
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic(GenericInstanceInfoEvent {
message: message.into(),
})
}
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub struct GenericInstanceInfoEvent {
pub message: String,
}
@@ -0,0 +1,13 @@
use helgoboss_license_api::persistence::LicenseData;
use serde::Serialize;
#[derive(Clone, Eq, PartialEq, Debug, Serialize)]
pub struct LicenseInfo {
pub licenses: Vec<ValidatedLicense>,
}
#[derive(Clone, Eq, PartialEq, Debug, Serialize)]
pub struct ValidatedLicense {
pub license: LicenseData,
pub valid: bool,
}
@@ -0,0 +1,14 @@
mod preset;
pub use preset::*;
mod global_info_event;
pub use global_info_event::*;
mod instance_info_event;
pub use instance_info_event::*;
mod reaper;
pub use reaper::*;
mod licensing;
pub use licensing::*;
@@ -0,0 +1,16 @@
use crate::persistence::{CommonPresetMetaData, ControllerPresetMetaData, MainPresetMetaData};
use serde::Serialize;
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize)]
pub struct MainPreset {
pub id: String,
pub common: CommonPresetMetaData,
pub specific: MainPresetMetaData,
}
#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize)]
pub struct ControllerPreset {
pub id: String,
pub common: CommonPresetMetaData,
pub specific: ControllerPresetMetaData,
}
@@ -0,0 +1,14 @@
#![allow(non_snake_case)]
use helgobox_macros::reaper_api;
reaper_api![
HelgoboxApi, HelgoboxApiPointers, HelgoboxApiSession, register_helgobox_api
{
/// Finds the first Helgobox instance in the given project.
///
/// If the given project is `null`, it will look in the current project.
///
/// Returns the instance ID or -1 if none exists.
HB_FindFirstHelgoboxInstanceInProject(project: *mut reaper_low::raw::ReaProject) -> std::ffi::c_int;
}
];
+15
View File
@@ -0,0 +1,15 @@
use serde::{Deserialize, Deserializer};
/// Makes sure that JSON `null` is treated the same as omitting a property.
///
/// Use as `#[serde(deserialize_with = "deserialize_null_default")]`.
///
/// See https://github.com/serde-rs/serde/issues/1098#issuecomment-760711617.
pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
@@ -0,0 +1,14 @@
[package]
name = "helgobox-artwork-processor"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
resvg = { workspace = true, features = ["system-fonts"] }
anyhow.workspace = true
[lints.clippy]
enum_glob_use = "deny"
@@ -0,0 +1,130 @@
use anyhow::{Context, Result};
use resvg::tiny_skia::{Pixmap, PremultipliedColorU8, Transform};
use resvg::usvg;
use resvg::usvg::Options;
use std::fs;
use std::path::Path;
use std::sync::Arc;
fn main() -> Result<()> {
render_artwork()?;
println!("Finished rendering artwork");
Ok(())
}
fn render_artwork() -> Result<()> {
let logo_file = "resources/artwork/playtime-logo.svg";
let logo_svg = fs::read_to_string(logo_file)?;
generate_icons("playtime", &logo_svg, "")?;
generate_icons("playtime-custom", &logo_svg, "with-settings-icon")?;
let logo_svg_with_text_2 = logo_svg.replace("TEXT_PLACEHOLDER", "2");
generate_icons("playtime-2", &logo_svg_with_text_2, "with-text-badge")?;
Ok(())
}
fn generate_icons(name_with_dashes: &str, svg: &str, additional_root_classes: &str) -> Result<()> {
let name_with_underscores = name_with_dashes.replace('-', "_");
// Toolbar icons
generate_toolbar_icons(&name_with_underscores, svg, additional_root_classes)?;
// Icons for docs
generate_icon(
svg,
format!("doc/playtime/modules/ROOT/images/screenshots/{name_with_dashes}-toolbar-icon.png"),
(120, 120),
additional_root_classes,
&[ToolbarIconStatus::Normal],
)?;
Ok(())
}
fn generate_toolbar_icons(name: &str, svg: &str, additional_root_classes: &str) -> Result<()> {
use ToolbarIconStatus as S;
let toolbar_statuses = [S::Normal, S::Hovered, S::Selected];
generate_icon(
svg,
format!("resources/artwork/toolbar_icons/toolbar_{name}.png"),
(30, 30),
additional_root_classes,
&toolbar_statuses,
)?;
generate_icon(
svg,
format!("resources/artwork/toolbar_icons/150/toolbar_{name}.png"),
(45, 45),
additional_root_classes,
&toolbar_statuses,
)?;
generate_icon(
svg,
format!("resources/artwork/toolbar_icons/200/toolbar_{name}.png"),
(60, 60),
additional_root_classes,
&toolbar_statuses,
)?;
Ok(())
}
fn generate_icon(
svg: &str,
dst_file: impl AsRef<Path>,
(width, height): (u32, u32),
additional_root_classes: &str,
statuses: &[ToolbarIconStatus],
) -> Result<()> {
let dst_file = dst_file.as_ref();
let pixmap = render_toolbar_icon(svg, (width, height), additional_root_classes, statuses)?;
fs::create_dir_all(dst_file.parent().context("no parent file")?)?;
pixmap.save_png(dst_file)?;
Ok(())
}
fn render_toolbar_icon(
svg: &str,
(width, height): (u32, u32),
additional_root_classes: &str,
statuses: &[ToolbarIconStatus],
) -> Result<Pixmap> {
let sprite_count = statuses.len() as u32;
let mut pixmap = Pixmap::new(width * sprite_count, height).unwrap();
use ToolbarIconStatus as S;
for (i, status) in statuses.iter().enumerate() {
let fg_color = match status {
S::Normal => "#818989",
S::Hovered => "#939a9a",
S::Selected => "#1abc98",
};
let interpolated_svg = svg
.replace(
"ROOT_CLASSES_PLACEHOLDER",
&format!("toolbar-icon {additional_root_classes}"),
)
.replace("var(--fg-color)", fg_color)
.replace("var(--bg-color", "#333333");
let mut options = Options::default();
let mut font_db = usvg::fontdb::Database::new();
font_db.load_fonts_dir("resources/artwork/fonts");
options.fontdb = Arc::new(font_db);
let tree = usvg::Tree::from_str(&interpolated_svg, &options)?;
// Render sprite
let transform = Transform::from_scale(
width as f32 / tree.size().width(),
height as f32 / tree.size().height(),
)
.post_translate(i as f32 * width as f32, 0.0);
resvg::render(&tree, transform, &mut pixmap.as_mut());
}
// Replace "shine-through" color with transparency
let shine_through_color = PremultipliedColorU8::from_rgba(255, 0, 255, 255).unwrap(); // Magenta
for pixel in pixmap.pixels_mut() {
if *pixel == shine_through_color {
*pixel = PremultipliedColorU8::from_rgba(0, 0, 0, 0).unwrap(); // Transparent
}
}
Ok(pixmap)
}
enum ToolbarIconStatus {
Normal,
Hovered,
Selected,
}
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "base"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
# Own
reaper-high.workspace = true
reaper-medium.workspace = true
reaper-low.workspace = true
reaper-rx.workspace = true
helgobox-api.workspace = true
# 3rd-party
serde.workspace = true
serde_json.workspace = true
xxhash-rust.workspace = true
crossbeam-channel.workspace = true
futures-timer.workspace = true
once_cell.workspace = true
tracing.workspace = true
metrics.workspace = true
ascii.workspace = true
enigo.workspace = true
# For getting current mouse state
device_query.workspace = true
derive_more.workspace = true
either.workspace = true
logos.workspace = true
anyhow.workspace = true
thiserror.workspace = true
camino.workspace = true
indexmap.workspace = true
futures.workspace = true
tokio.workspace = true
fragile.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
# For not letting device_query panic when macOS accessibility permissions not granted
macos-accessibility-client.workspace = true
[lints.clippy]
enum_glob_use = "deny"
@@ -0,0 +1,59 @@
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
/// An approximate floating-point type that uses the same epsilon for comparison as the == operator in
/// [EEL2](https://www.cockos.com/EEL2/):
/// Two values are considered equal if the difference is less than 0.00001 (1/100000), 0 if not.
pub type AudioF64 = ApproxF64<100000>;
/// Simple newtype that allows for approximate comparison of 64-bit floating-point numbers.
///
/// The const type parameter `E` ("epsilon") defines how tolerant floating-point comparison is. Two values are considered
/// equal if the difference is less than 1/E.
#[derive(Copy, Clone, Debug, Default)]
pub struct ApproxF64<const E: u32>(pub f64);
impl<const E: u32> ApproxF64<E> {
const EPSILON: f64 = 1.0 / E as f64;
pub fn new(raw: f64) -> Self {
Self(raw)
}
fn difference_is_neglectable(&self, other: &Self) -> bool {
(self.0 - other.0).abs() < Self::EPSILON
}
}
impl<const E: u32> PartialOrd for ApproxF64<E> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.difference_is_neglectable(other) {
return Some(Ordering::Equal);
}
self.0.partial_cmp(&other.0)
}
}
impl<const E: u32> PartialEq for ApproxF64<E> {
fn eq(&self, other: &Self) -> bool {
self.difference_is_neglectable(other)
}
}
impl<const E: u32> Display for ApproxF64<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basics() {
assert_eq!(AudioF64::new(0.75), AudioF64::new(0.75));
assert_ne!(AudioF64::new(0.00001), AudioF64::new(0.00002));
assert_eq!(AudioF64::new(0.000001), AudioF64::new(0.000002));
}
}
@@ -0,0 +1,114 @@
//! We have a raw MIDI pattern in helgoboss-learn already (raw MIDI source), however this is more
//! complicated than this one as it also allows single bits to be variable.
use logos::{Lexer, Logos};
use std::num::ParseIntError;
use std::str::FromStr;
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct BytePattern {
bytes: Vec<PatternByte>,
}
impl BytePattern {
pub const fn new(bytes: Vec<PatternByte>) -> Self {
Self { bytes }
}
pub fn matches(&self, bytes: &[u8]) -> bool {
use PatternByte as B;
let mut byte_iter = bytes.iter();
let mut last_was_multi = false;
for pattern_byte in &self.bytes {
let matches = match pattern_byte {
B::Fixed(expected_byte) => {
if last_was_multi {
// Last pattern byte was multi
last_was_multi = false;
// Greedily consume any follow-up actual bytes until we meet the expected
// byte. If we don't meet it, no match!
byte_iter.any(|b| b == expected_byte)
} else {
// Last pattern byte was single or fixed
byte_iter.next().is_some_and(|b| b == expected_byte)
}
}
B::Single => {
last_was_multi = false;
// We need to have an actual byte but it doesn't matter which one!
byte_iter.next().is_some()
}
B::Multi => {
last_was_multi = true;
// Match even if no actual byte left!
true
}
};
if !matches {
return false;
}
}
byte_iter.next().is_none() || last_was_multi
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Logos)]
#[logos(skip r"[ \t\n\f]+")]
#[logos(error = ParseBytePatternError)]
pub enum PatternByte {
#[regex(r"[0-9a-fA-F][0-9a-fA-F]?", parse_as_byte)]
Fixed(u8),
#[token("?")]
Single,
#[token("*")]
Multi,
}
#[derive(Clone, PartialEq, Debug, Default, thiserror::Error)]
#[error("{msg}")]
pub struct ParseBytePatternError {
msg: &'static str,
}
impl From<ParseIntError> for ParseBytePatternError {
fn from(_: ParseIntError) -> Self {
Self {
msg: "problem parsing fixed byte",
}
}
}
fn parse_as_byte(lex: &mut Lexer<PatternByte>) -> Result<u8, ParseIntError> {
u8::from_str_radix(lex.slice(), 16)
}
impl FromStr for BytePattern {
type Err = ParseBytePatternError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lex: Lexer<PatternByte> = PatternByte::lexer(s);
let entries: Result<Vec<_>, _> = lex.collect();
Ok(BytePattern::new(entries?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basics() {
// Given
let pattern: BytePattern = "F0 7E ? 06 02 * F7".parse().unwrap();
// When
assert!(!pattern.matches(&[]));
assert!(!pattern.matches(&[0xF0]));
assert!(!pattern.matches(&[0xF0, 0x7E]));
assert!(!pattern.matches(&[0xF0, 0x7E, 0x00]));
assert!(!pattern.matches(&[0xF0, 0x7E, 0x00, 0x06]));
assert!(pattern.matches(&[0xF0, 0x7E, 0x00, 0x06, 0x02, 0xF7]));
assert!(pattern.matches(&[0xF0, 0x7E, 0x01, 0x06, 0x02, 0xF7]));
assert!(pattern.matches(&[0xF0, 0x7E, 0xFF, 0x06, 0x02, 0xFF, 0x60, 0xF7]));
assert!(!pattern.matches(&[0xF0, 0x7E, 0xFF, 0x06, 0x02, 0xFF, 0x60, 0xF7, 0xF7]));
}
}
+376
View File
@@ -0,0 +1,376 @@
use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError};
use reaper_high::Reaper;
use std::error::Error;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
pub trait NamedChannelSender {
type Msg;
/// Sends the given message if the channel still has space and the receiver is still
/// connected, otherwise does nothing.
fn send_if_space(&self, msg: Self::Msg);
/// Sends the given message if the channel still has space, otherwise panics.
///
/// If the receiver is disconnected, does nothing.
fn send_complaining(&self, msg: Self::Msg);
}
/// A channel intended to send important messages from a real-time thread to a normal (non-real-time) thread.
///
/// The way this currently works is that it uses 2 senders: One that has an initial capacity and is normally used.
/// And another one that is unbounded (and can therefore allocate) that is only used if the initial one is full.
///
/// TODO-medium Find a channel library that allows pre-allocated unbounded channels (with a high initial capacity).
pub struct ImportantSenderFromRtToNormalThread<T> {
channel_name: &'static str,
bounded_normal_sender: Sender<T>,
unbounded_emergency_sender: Sender<T>,
}
impl<T> ImportantSenderFromRtToNormalThread<T> {
pub fn new(
channel_name: &'static str,
capacity: usize,
) -> (Self, ImportantReceiverFromRtToNormalThread<T>) {
// Main sender should belong to a bounded channel pre-allocated for normal usage.
//
// Emergency sender should belong to an unbounded channel and is only used if sending to the main channel would
// block because it's full. Better allocate than block or discard the event.
let (bounded_normal_sender, bounded_normal_receiver) = crossbeam_channel::bounded(capacity);
let (unbounded_emergency_sender, unbounded_emergency_receiver) =
crossbeam_channel::unbounded();
(
ImportantSenderFromRtToNormalThread {
channel_name,
bounded_normal_sender,
unbounded_emergency_sender,
},
ImportantReceiverFromRtToNormalThread {
channel_name,
bounded_normal_receiver,
unbounded_emergency_receiver,
},
)
}
}
impl<T> ImportantSenderFromRtToNormalThread<T> {
/// Returns `false` if receiver gone.
pub fn send(&self, msg: T) -> bool {
if let Err(e) = self.bounded_normal_sender.try_send(msg) {
match e {
TrySendError::Full(msg) => {
tracing::warn!(
msg = "Main sequence channel was full, using emergency channel (may allocate)!",
%self.channel_name,
);
let _ = self.unbounded_emergency_sender.send(msg);
true
}
TrySendError::Disconnected(_) => false,
}
} else {
true
}
}
}
impl<T> Clone for ImportantSenderFromRtToNormalThread<T> {
fn clone(&self) -> Self {
Self {
channel_name: self.channel_name,
bounded_normal_sender: self.bounded_normal_sender.clone(),
unbounded_emergency_sender: self.unbounded_emergency_sender.clone(),
}
}
}
pub struct ImportantReceiverFromRtToNormalThread<T> {
channel_name: &'static str,
bounded_normal_receiver: Receiver<T>,
unbounded_emergency_receiver: Receiver<T>,
}
impl<T> ImportantReceiverFromRtToNormalThread<T> {
pub fn try_recv(&self) -> Result<T, TryRecvError> {
self.bounded_normal_receiver
.try_recv()
.or_else(|_| self.unbounded_emergency_receiver.try_recv())
}
}
impl<T> Debug for ImportantReceiverFromRtToNormalThread<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ImportantReceiverFromRtToNormalThread")
.field("channel_name", &self.channel_name)
.field("bounded_normal_receiver", &self.bounded_normal_receiver)
.field(
"unbounded_emergency_receiver",
&self.unbounded_emergency_receiver,
)
.finish()
}
}
impl<T> Debug for ImportantSenderFromRtToNormalThread<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ImportantSenderFromRtToNormalThread")
.field("channel_name", &self.channel_name)
.field("bounded_normal_sender", &self.bounded_normal_sender)
.field(
"unbounded_emergency_sender",
&self.unbounded_emergency_sender,
)
.finish()
}
}
/// A channel intended to send messages to a normal (non-real-time) thread.
///
/// - Either unbounded (should only be used if the sender is also a normal thread).
/// - Or bounded (can also be used if the sender is a real-time thread).
///
/// If you need an unbounded one that is okay to use from a real-time thread, look into
/// [`ImportantSenderFromRtToNormalThread`].
pub struct SenderToNormalThread<T> {
channel_name: &'static str,
sender: Sender<T>,
complained_already: AtomicBool,
}
impl<T> Debug for SenderToNormalThread<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("SenderToNormalThread")
.field("channel_name", &self.channel_name)
.field("sender", &self.sender)
.finish()
}
}
impl<T> NamedChannelSender for SenderToNormalThread<T> {
type Msg = T;
fn send_if_space(&self, msg: T) {
let _ = self.send_internal(msg);
}
fn send_complaining(&self, msg: T) {
let result = self.send_internal(msg);
if !receiver_is_disconnected(&result)
&& !self.complained_already.swap(true, Ordering::Relaxed)
{
// Complain
result.unwrap();
}
}
}
fn receiver_is_disconnected<T>(result: &Result<(), NamedChannelTrySendError<T>>) -> bool {
if let Err(e) = &result {
matches!(e.try_send_error, TrySendError::Disconnected(_))
} else {
false
}
}
impl<T> SenderToNormalThread<T> {
/// Creates a bounded channel.
///
/// - **Pro:** Never allocates when sending and is therefore safe to use from real-time threads.
/// - **Con:** We can get "channel full" errors on load spikes if the capacity is not high
/// enough. Choosing an extremely high capacity to avoid this is not a good idea either
/// because it consumes memory that's almost never going to be used.
pub fn new_bounded_channel(name: &'static str, capacity: usize) -> (Self, Receiver<T>) {
let (sender, receiver) = crossbeam_channel::bounded(capacity);
(
Self {
channel_name: name,
sender,
complained_already: AtomicBool::new(false),
},
receiver,
)
}
/// Creates an unbounded channel.
///
/// - **Pro:** We don't get "channel full" errors on load spikes.
/// - **Con:** This can allocate when sending, so don't use this if the sender is used in
/// real-time threads! If you still do so, it will complain in debug mode because we forbid
/// allocation in real-time threads.
///
/// We set a (very high) upper limit even for unbounded channels just to avoid memory exhaustion
/// if the channel grows endlessly because of another error. This limit is not ensured by
/// pre-allocating the channel with a certain capacity but by checking the current number
/// of messages in the channel before sending.
pub fn new_unbounded_channel(name: &'static str) -> (Self, Receiver<T>) {
let (sender, receiver) = crossbeam_channel::unbounded();
(
Self {
channel_name: name,
sender,
complained_already: AtomicBool::new(false),
},
receiver,
)
}
pub fn try_to_send(&self, msg: T) -> bool {
self.sender.try_send(msg).is_ok()
}
pub fn is_bounded(&self) -> bool {
self.sender.capacity().is_some()
}
fn send_internal(&self, msg: T) -> Result<(), NamedChannelTrySendError<T>> {
if !self.is_bounded() {
// The channel is not bounded but we still want to panic if the number of messages
// in the channel is extremely high, to prevent memory exhaustion.
let msg_count = self.sender.len();
if msg_count > 1_000_000 {
panic!(
"Unbounded channel {} is extremely full ({} messages). \
Not accepting new messages in order to prevent memory exhaustion.",
self.channel_name, msg_count
);
}
}
try_send_on_named_channel(&self.sender, self.channel_name, msg)
}
}
impl<T> Clone for SenderToNormalThread<T> {
fn clone(&self) -> Self {
Self {
channel_name: self.channel_name,
sender: self.sender.clone(),
complained_already: AtomicBool::new(false),
}
}
}
/// A channel intended to send messages to real-time threads.
///
/// It has special logic which makes sure the queue doesn't run full when audio is not running.
#[derive(Debug)]
pub struct SenderToRealTimeThread<T> {
channel_name: &'static str,
sender: Sender<T>,
complained_already: AtomicBool,
}
impl<T> Clone for SenderToRealTimeThread<T> {
fn clone(&self) -> Self {
Self {
channel_name: self.channel_name,
sender: self.sender.clone(),
complained_already: AtomicBool::new(false),
}
}
}
impl<T> NamedChannelSender for SenderToRealTimeThread<T> {
type Msg = T;
fn send_if_space(&self, msg: T) {
let _ = self.send_internal(msg);
}
fn send_complaining(&self, msg: T) {
let result = self.send_internal(msg);
if !receiver_is_disconnected(&result)
&& !self.complained_already.swap(true, Ordering::Relaxed)
{
// Complain
result.unwrap();
}
}
}
impl<T> SenderToRealTimeThread<T> {
pub fn new_channel(name: &'static str, capacity: usize) -> (Self, Receiver<T>) {
let (sender, receiver) = crossbeam_channel::bounded(capacity);
(
Self {
channel_name: name,
sender,
complained_already: AtomicBool::new(false),
},
receiver,
)
}
fn send_internal(&self, msg: T) -> Result<(), NamedChannelTrySendError<T>> {
if Reaper::get().audio_is_running() {
// Audio is running so sending should always work. If not, it's an unexpected error and
// we must return it.
try_send_on_named_channel(&self.sender, self.channel_name, msg)
} else {
// Audio is not running. Maybe this is just a very temporary outage or a short initial
// non-running state.
if self.channel_still_has_some_headroom() {
// Channel still has some headroom, so we send the task in order to support a
// temporary outage. This should not fail unless another sender has exhausted the
// channel in the meanwhile. Even then, so what. See "else" branch.
let _ = self.sender.try_send(msg);
Ok(())
} else {
// Channel has already accumulated lots of tasks. Don't send!
// It's not bad if we don't send this task because the real-time processor will
// not be able to process it anyway at the moment (it's not going to be called
// because the audio engine is stopped). Fear not, ReaLearn's audio hook has logic
// that detects a "rebirth" - the moment when the audio cycle starts again. In this
// case it will request a full resync of everything so nothing should get lost
// in theory.
Ok(())
}
}
}
fn channel_still_has_some_headroom(&self) -> bool {
self.sender.len() <= self.sender.capacity().unwrap() / 2
}
}
fn try_send_on_named_channel<T>(
sender: &Sender<T>,
channel_name: &'static str,
msg: T,
) -> Result<(), NamedChannelTrySendError<T>> {
sender.try_send(msg).map_err(|e| NamedChannelTrySendError {
channel_name,
try_send_error: e,
})
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct NamedChannelTrySendError<T> {
channel_name: &'static str,
try_send_error: TrySendError<T>,
}
impl<T> Debug for NamedChannelTrySendError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Channel [{}]: {:?}",
self.channel_name, self.try_send_error
)
}
}
impl<T> Display for NamedChannelTrySendError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Channel [{}]: {}",
self.channel_name, self.try_send_error
)
}
}
impl<T: Send> Error for NamedChannelTrySendError<T> {}
@@ -0,0 +1,38 @@
use serde::{Deserialize, Deserializer};
pub fn is_default<T: Default + PartialEq>(v: &T) -> bool {
v == &T::default()
}
pub fn bool_true() -> bool {
true
}
pub fn is_bool_true(v: &bool) -> bool {
*v
}
/// Should only be used when the deserialization checks the data version number because only that
/// way it can check if `None` represents the old default or the new one! (That is, if there's
/// even a difference between `None` and `Some(default())`, otherwise it doesn't matter).
pub fn is_none_or_some_default<T: Default + PartialEq>(v: &Option<T>) -> bool {
if let Some(i) = v {
i == &T::default()
} else {
true
}
}
/// Makes sure that JSON `null` is treated the same as omitting a property.
///
/// Use as `#[serde(deserialize_with = "deserialize_null_default")]`.
///
/// See https://github.com/serde-rs/serde/issues/1098#issuecomment-760711617.
pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
@@ -0,0 +1,36 @@
use crate::hash_util::PersistentHash;
use std::ffi::OsStr;
pub fn is_hidden(file_name: &OsStr) -> bool {
file_name
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
}
/// Converts a persistent hash number to something like
/// "a9/4a/8fe5ccb19ba61c4c0873d391e987.RfxChain" for the purpose to not get too many
/// files in one directory.
pub fn convert_hash_to_dir_structure(hash: PersistentHash, suffix: &str) -> String {
let hash = hash.get();
let first_byte = hash.rotate_left(8) & 0xff;
let second_byte = hash.rotate_left(16) & 0xff;
// Remaining: 112 bits = 14 bytes = 28 hex chars
let remaining = hash & 0xffffffffffffffffffffffffffff;
format!("{first_byte:02x}/{second_byte:02x}/{remaining:028x}{suffix}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash_util;
#[test]
fn hash_to_dir_structure_simple() {
let hash = hash_util::calculate_persistent_non_crypto_hash_one_shot("test".as_bytes());
assert_eq!(
convert_hash_to_dir_structure(hash, ".RfxChain"),
"6c/78/e0e3bd51d358d01e758642b85fb8.RfxChain".to_string()
);
}
}
@@ -0,0 +1,5 @@
use std::time::Duration;
pub async fn millis(amount: u64) {
futures_timer::Delay::new(Duration::from_millis(amount)).await;
}
+123
View File
@@ -0,0 +1,123 @@
use crossbeam_channel::{Receiver, Sender};
use fragile::Fragile;
use reaper_high::{
FutureMiddleware, FutureSupport, MainTaskMiddleware, MainThreadTask, TaskSupport,
DEFAULT_MAIN_THREAD_TASK_BULK_SIZE,
};
use reaper_rx::{ActionRx, ActionRxProvider, ControlSurfaceRx, MainRx};
use std::sync::LazyLock;
static INSTANCE: LazyLock<Global> = LazyLock::new(Global::default);
/// Spawns the given future in the main thread.
///
/// This only works if the future support is already running (= if the backbone shell is already woken up).
pub fn spawn_in_main_thread(
future: impl std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + 'static,
) {
Global::future_support().spawn_in_main_thread_from_main_thread(future);
}
pub struct Global {
main_rx: Fragile<MainRx>,
task_support: TaskSupport,
future_support: FutureSupport,
task_sender: Sender<MainThreadTask>,
task_receiver: Receiver<MainThreadTask>,
send_future_executor: reaper_high::run_loop_executor::RunLoopExecutor,
non_send_future_executor: reaper_high::local_run_loop_executor::RunLoopExecutor,
}
impl Default for Global {
fn default() -> Self {
// It's important that all of the below channels are unbounded. It's not just that they
// can run full and then panic, it's worse. If sending and receiving happens on the same
// thread (which we use quite often in order to schedule/spawn something on the main
// thread) and the channel is full, we will get a deadlock! It's okay that they allocate
// on sending because `Global` can't be used from a real-time thread.
// See https://github.com/helgoboss/helgobox/issues/875.
let (task_sender, task_receiver) = crossbeam_channel::unbounded();
let (send_future_spawner, send_future_executor) =
reaper_high::run_loop_executor::new_spawner_and_executor(
DEFAULT_MAIN_THREAD_TASK_BULK_SIZE,
);
let (non_send_future_spawner, non_send_future_executor) =
reaper_high::local_run_loop_executor::new_spawner_and_executor(
DEFAULT_MAIN_THREAD_TASK_BULK_SIZE,
);
Self {
main_rx: Default::default(),
task_support: TaskSupport::new(task_sender.clone()),
future_support: FutureSupport::new(send_future_spawner, non_send_future_spawner),
task_sender,
task_receiver,
send_future_executor,
non_send_future_executor,
}
}
}
impl Global {
pub fn get() -> &'static Self {
assert!(
!reaper_high::Reaper::get()
.medium_reaper()
.is_in_real_time_audio(),
"this function must not be called in a real-time thread"
);
&INSTANCE
}
// This is kept static just for allowing easy observable subscription from everywhere. For
// pushing to the subjects, static access is not necessary.
// Don't use from real-time thread!
pub fn control_surface_rx() -> &'static ControlSurfaceRx {
Global::get().main_rx.get().control_surface()
}
// This really needs to be kept static for pushing to the subjects because hook commands can't
// take user data.
//
// Don't use from real-time thread!
pub fn action_rx() -> &'static ActionRx {
Global::get().main_rx.get().action()
}
/// Allows you to schedule tasks for execution on the main thread from anywhere.
///
/// Important: Don't use this to schedule tasks from a real-time thread! This is backed by an
/// unbounded channel now because of https://github.com/helgoboss/helgobox/issues/875, so
/// sending can allocate!
pub fn task_support() -> &'static TaskSupport {
&Global::get().task_support
}
/// Allows you to spawn futures from anywhere.
///
/// Important: Don't use this to spawn futures from a real-time thread! This is backed by an
/// unbounded channel now because of https://github.com/helgoboss/helgobox/issues/875, so
/// sending can allocate!
pub fn future_support() -> &'static FutureSupport {
&Global::get().future_support
}
/// Creates the middleware that drives the task support.
pub fn create_task_support_middleware(&self) -> MainTaskMiddleware {
MainTaskMiddleware::new(self.task_sender.clone(), self.task_receiver.clone())
}
/// Creates the middleware that drives the future support.
pub fn create_future_support_middleware(&self) -> FutureMiddleware {
FutureMiddleware::new(
self.send_future_executor.clone(),
self.non_send_future_executor.clone(),
)
}
}
impl ActionRxProvider for Global {
fn action_rx() -> &'static ActionRx {
Global::action_rx()
}
}
@@ -0,0 +1,30 @@
/// Use only where absolutely necessary because of static-only FFI stuff!
#[macro_export]
macro_rules! make_available_globally_in_main_thread_on_demand {
($instance_struct:path) => {
static INSTANCE: std::sync::OnceLock<fragile::Fragile<$instance_struct>> =
std::sync::OnceLock::new();
impl $instance_struct {
pub fn make_available_globally(create_instance: impl FnOnce() -> $instance_struct) {
if INSTANCE.get().is_some() {
return;
}
let _ = INSTANCE.set(fragile::Fragile::new(create_instance()));
}
/// Whether this instance is (already/still) loaded.
pub fn is_loaded() -> bool {
INSTANCE.get().is_some()
}
/// Panics if not in main thread.
pub fn get() -> &'static $instance_struct {
INSTANCE
.get()
.expect("call `make_available_globally()` before using `get()`")
.get()
}
}
};
}
@@ -0,0 +1,117 @@
use indexmap::{IndexMap, IndexSet};
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash, Hasher};
use xxhash_rust::xxh3::{Xxh3Default, Xxh3DefaultBuilder};
/// The default choice for hashing in Helgobox.
pub type NonCryptoHashBuilder = Xxh3DefaultBuilder;
/// The default choice for hashing in Helgobox.
pub type NonCryptoHasher = Xxh3Default;
/// The default choice for hash maps in Helgobox.
pub type NonCryptoHashMap<K, V> = HashMap<K, V, NonCryptoHashBuilder>;
/// The default choice for hash sets in Helgobox.
pub type NonCryptoHashSet<T> = HashSet<T, NonCryptoHashBuilder>;
/// The default choice for index maps in Helgobox.
pub type NonCryptoIndexMap<K, V> = IndexMap<K, V, NonCryptoHashBuilder>;
/// The default choice for index sets in Helgobox.
pub type NonCryptoIndexSet<T> = IndexSet<T, NonCryptoHashBuilder>;
pub fn clone_to_other_hash_map<
K: Eq + Hash + Clone,
V: Clone,
S1: BuildHasher,
S2: BuildHasher + Default,
>(
non_crypto: &HashMap<K, V, S1>,
) -> HashMap<K, V, S2> {
non_crypto
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn convert_into_other_hash_map<K: Eq + Hash, V, S1: BuildHasher, S2: BuildHasher + Default>(
non_crypto: HashMap<K, V, S1>,
) -> HashMap<K, V, S2> {
non_crypto.into_iter().collect()
}
pub fn convert_into_other_hash_set<K: Eq + Hash, S1: BuildHasher, S2: BuildHasher + Default>(
non_crypto: HashSet<K, S1>,
) -> HashSet<K, S2> {
non_crypto.into_iter().collect()
}
/// Calculates a 64-bit non-crypto hash directly from the given bytes.
///
/// A bit faster than the streaming version.
pub fn calculate_non_crypto_hash_one_shot(payload: &[u8]) -> u64 {
xxhash_rust::xxh3::xxh3_64(payload)
}
/// Calculates a 128-bit non-crypto hash directly from the given bytes suitable for persistence.
///
/// This implementation must not change!
pub fn calculate_persistent_non_crypto_hash_one_shot(payload: &[u8]) -> PersistentHash {
// Don't change the hash function! It's used e.g. for file names.
PersistentHash(xxhash_rust::xxh3::xxh3_128(payload))
}
/// Calculates a 64-bit non-crypto hash from the given hashable type.
///
/// If you already have a slice of bytes, use the one-shot version instead.
pub fn calculate_non_crypto_hash<T: Hash>(t: &T) -> u64 {
let mut hasher = create_non_crypto_hasher();
t.hash(&mut hasher);
hasher.finish()
}
/// Creates a hasher for calculating a 64-bit non-crypto hash.
pub fn create_non_crypto_hasher() -> impl Hasher {
NonCryptoHasher::new()
}
/// Creates a builder for a hasher for calculating a 64-bit non-crypto hash.
pub fn create_non_crypto_hash_builder() -> NonCryptoHashBuilder {
NonCryptoHashBuilder::new()
}
/// This newtype should be used whenever it matters to keep a stable hash function, for example
/// when the hashes are going to be persisted.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct PersistentHash(u128);
impl PersistentHash {
pub fn get(&self) -> u128 {
self.0
}
}
#[derive(Default)]
pub struct PersistentHasher(Xxh3Default);
impl PersistentHasher {
pub fn new() -> Self {
// Don't change the wrapped hasher! It's used e.g. for file names.
Self::default()
}
pub fn digest_128(&self) -> PersistentHash {
PersistentHash(self.0.digest128())
}
}
impl Hasher for PersistentHasher {
fn finish(&self) -> u64 {
self.0.finish()
}
fn write(&mut self, bytes: &[u8]) {
self.0.write(bytes)
}
}
+49
View File
@@ -0,0 +1,49 @@
#[macro_use]
mod regex_util;
#[macro_use]
pub mod tracing_util;
#[macro_use]
mod global_macros;
mod mouse;
pub use mouse::*;
mod global;
pub use global::*;
pub mod default_util;
pub mod hash_util;
mod channels;
pub use channels::*;
mod mutex_util;
pub use mutex_util::*;
pub mod file_util;
pub mod future_util;
pub mod metrics_util;
mod small_ascii_string;
pub use small_ascii_string::*;
mod sound_player;
pub use sound_player::*;
pub mod validation_util;
pub mod peak_util;
pub mod byte_pattern;
pub mod serde_json_util;
mod approx_f64;
pub use approx_f64::*;
pub mod replenishment_channel;
@@ -0,0 +1,131 @@
use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::OnceLock;
use std::thread;
use std::time::{Duration, Instant};
/// This will contain the metrics sender for async metrics recording if metrics are enabled.
static METRICS_SENDER: OnceLock<SyncSender<MetricsRecorderCommand>> = OnceLock::new();
#[derive(Debug)]
pub struct MetricsHook {
sender: SyncSender<MetricsRecorderCommand>,
}
impl Drop for MetricsHook {
fn drop(&mut self) {
// This prevents the metrics recorder thread from lurking around after the library
// is unloaded (which is of importance if "Allow complete unload of VST plug-ins"
// is enabled in REAPER for Windows). Ideally, we would just destroy the sender to achieve
// the same effect. But the sender is in a static variable which already has been
// initialized once and therefore can't be set to `None`. Unloading the library will just
// free the memory without triggering the drop, so that wouldn't work either.
let _ = self.sender.try_send(MetricsRecorderCommand::Finish);
// Joining the thread here somehow leads to a deadlock. Not sure why. It doesn't
// seem to be necessary anyway. The thread will end no matter what.
}
}
impl MetricsHook {
/// Initializes metrics recording if the env variable `HELGOBOX_METRICS` is set.
///
/// This starts a dedicated metrics recording thread, which is responsible for actually
/// recording certain metrics (e.g. durations), which is especially important when measuring
/// stuff from real-time threads. It avoids allocation and doesn't slow down real-time
/// processing (with the exception of measuring the duration itself).
///
/// This should be called only once within the lifetime of the loaded shared library! On Linux
/// and macOS, this means it must only be called once within the lifetime of REAPER because once
/// a shared library is loaded, it's not unloaded anymore. On Windows, it can be called again
/// after REAPER unloaded the library via `FreeLibrary` and reloaded it again.
///
/// The returned metrics hook must be dropped before the library is unloaded, otherwise the
/// metrics thread sticks around and that can't be good.
pub fn init() -> Option<Self> {
std::env::var("HELGOBOX_METRICS").ok()?;
let (sender, receiver) = std::sync::mpsc::sync_channel(5000);
thread::Builder::new()
.name(String::from("Helgobox metrics"))
.spawn(move || {
keep_recording_metrics(receiver);
})
.expect("Helgobox metrics thread couldn't be created");
METRICS_SENDER
.set(sender.clone())
.expect("attempting to initializing metrics hook more than once");
let hook = Self { sender };
Some(hook)
}
}
/// A simple function that doesn't expose anything to the metrics endpoint but warns if a
/// threshold is exceeded. Doesn't do anything in release builds (except executing the function).
pub fn warn_if_takes_too_long<R>(label: &'static str, max: Duration, f: impl FnOnce() -> R) -> R {
#[cfg(debug_assertions)]
{
let before = Instant::now();
let r = f();
let elapsed = before.elapsed();
if elapsed > max {
tracing::warn!(
"Operation took too long: \"{label}\" ({})ms",
elapsed.as_millis()
);
}
r
}
#[cfg(not(debug_assertions))]
{
let _ = (label, max);
f()
}
}
/// Synchronously records the occurrence of the given event.
pub fn record_occurrence(id: &'static str) {
if !metrics_are_enabled() {
return;
}
metrics::counter!(id).increment(1);
}
/// Asynchronously measures and records the time of the given operation and exposes it at the
/// metrics endpoint.
pub fn measure_time<R>(id: &'static str, f: impl FnOnce() -> R) -> R {
if !metrics_are_enabled() {
return f();
}
let start = Instant::now();
let result = f();
record_duration(id, start.elapsed());
result
}
/// Records the given duration into a histogram.
pub fn record_duration(id: &'static str, delta: Duration) {
if let Some(sender) = METRICS_SENDER.get() {
let task = MetricsRecorderCommand::Histogram { id, delta };
if sender.try_send(task).is_err() {
tracing::debug!("Helgobox metrics channel is full");
}
}
}
pub fn metrics_are_enabled() -> bool {
METRICS_SENDER.get().is_some()
}
enum MetricsRecorderCommand {
Finish,
Histogram { id: &'static str, delta: Duration },
}
fn keep_recording_metrics(receiver: Receiver<MetricsRecorderCommand>) {
while let Ok(task) = receiver.recv() {
match task {
MetricsRecorderCommand::Finish => break,
MetricsRecorderCommand::Histogram { id, delta } => {
metrics::histogram!(id).record(delta);
}
}
}
}
@@ -0,0 +1,41 @@
use helgobox_api::persistence::{Axis, MouseButton};
pub trait Mouse {
fn axis_size(&self, axis: Axis) -> u32;
fn cursor_position(&self) -> Result<MouseCursorPosition, &'static str>;
fn set_cursor_position(&mut self, new_pos: MouseCursorPosition) -> Result<(), &'static str>;
/// Moves the mouse cursor relatively to its current position.
///
/// - On the x axis, positive delta scrolls right and negative left.
/// - On the y axis, positive delta scrolls down and negative up (because it's natural for
/// screens to consider the top-left as zero).
fn adjust_cursor_position(&mut self, x_delta: i32, y_delta: i32) -> Result<(), &'static str>;
/// Invokes the scroll wheel.
///
/// - On the x axis, positive delta scrolls right and negative left.
/// - On the y axis, positive delta scrolls up and negative down (because it's natural for
/// knobs and especially faders to increase when scrolling up).
fn scroll(&mut self, axis: Axis, delta: i32) -> Result<(), &'static str>;
fn press(&mut self, button: MouseButton) -> Result<(), &'static str>;
fn release(&mut self, button: MouseButton) -> Result<(), &'static str>;
fn is_pressed(&self, button: MouseButton) -> Result<bool, &'static str>;
}
#[derive(Copy, Clone, Debug)]
pub struct MouseCursorPosition {
pub x: u32,
pub y: u32,
}
impl MouseCursorPosition {
pub fn new(x: u32, y: u32) -> Self {
Self { x, y }
}
}
@@ -0,0 +1,159 @@
use crate::{Mouse, MouseCursorPosition};
use device_query::DeviceState;
use enigo::{Enigo, MouseControllable};
use helgobox_api::persistence::{Axis, MouseButton};
use std::fmt::Debug;
#[derive(Debug)]
pub struct EnigoMouse {
enigo: Enigo,
device_state: Option<DeviceState>,
}
impl Default for EnigoMouse {
fn default() -> Self {
Self::new()
}
}
impl EnigoMouse {
pub fn new() -> Self {
Self {
enigo: Default::default(),
device_state: create_device_state(),
}
}
}
fn create_device_state() -> Option<DeviceState> {
#[cfg(target_os = "macos")]
{
let trusted =
macos_accessibility_client::accessibility::application_is_trusted_with_prompt();
if trusted {
Some(DeviceState::new())
} else {
reaper_high::Reaper::get().show_console_msg("This Helgobox feature only works if Helgobox can access the state of your mouse. For this, it needs macOS accessibility permissions. Please grant REAPER the accessibility permission in the macOS system settings and restart it!\n\n");
None
}
}
#[cfg(not(target_os = "macos"))]
{
Some(DeviceState::new())
}
}
unsafe impl Send for EnigoMouse {}
impl Clone for EnigoMouse {
fn clone(&self) -> Self {
Self::new()
}
}
impl PartialEq for EnigoMouse {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for EnigoMouse {}
impl Mouse for EnigoMouse {
fn axis_size(&self, axis: Axis) -> u32 {
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
let (width, height) = Enigo::main_display_size();
let axis_size = match axis {
Axis::X => width,
Axis::Y => height,
};
axis_size as u32
}
#[cfg(target_os = "linux")]
{
let index = match axis {
Axis::X => reaper_low::raw::SM_CXSCREEN,
Axis::Y => reaper_low::raw::SM_CYSCREEN,
};
reaper_low::Swell::get().GetSystemMetrics(index) as _
}
}
fn cursor_position(&self) -> Result<MouseCursorPosition, &'static str> {
#[cfg(any(target_os = "windows", target_os = "macos"))]
let (x, y) = Enigo::mouse_location();
#[cfg(target_os = "linux")]
let (x, y) = {
let device_state = self
.device_state
.as_ref()
.expect("DeviceState should always work on Linux")
.query_pointer();
(device_state.coords.0, device_state.coords.1)
};
Ok(MouseCursorPosition::new(x.max(0) as u32, y.max(0) as u32))
}
fn set_cursor_position(&mut self, new_pos: MouseCursorPosition) -> Result<(), &'static str> {
self.enigo.mouse_move_to(new_pos.x as _, new_pos.y as _);
Ok(())
}
fn adjust_cursor_position(&mut self, x_delta: i32, y_delta: i32) -> Result<(), &'static str> {
self.enigo.mouse_move_relative(x_delta, y_delta);
Ok(())
}
fn scroll(&mut self, axis: Axis, delta: i32) -> Result<(), &'static str> {
match axis {
Axis::X => self.enigo.mouse_scroll_x(delta),
Axis::Y => {
// Handle https://github.com/enigo-rs/enigo/issues/117
let final_delta = if cfg!(windows) { delta } else { -delta };
self.enigo.mouse_scroll_y(final_delta)
}
}
Ok(())
}
fn press(&mut self, button: MouseButton) -> Result<(), &'static str> {
self.enigo.mouse_down(convert_button_to_enigo(button));
Ok(())
}
fn release(&mut self, button: MouseButton) -> Result<(), &'static str> {
self.enigo.mouse_up(convert_button_to_enigo(button));
Ok(())
}
fn is_pressed(&self, button: MouseButton) -> Result<bool, &'static str> {
let mouse_state = self
.device_state
.as_ref()
.ok_or("macOS accessibility permissions not granted")?
.query_pointer();
let button_index = convert_button_to_device_query(button);
let pressed = mouse_state
.button_pressed
.get(button_index)
.ok_or("couldn't get button")?;
Ok(*pressed)
}
}
fn convert_button_to_device_query(button: MouseButton) -> usize {
match button {
MouseButton::Left => 1,
MouseButton::Middle => 3,
MouseButton::Right => 2,
}
}
fn convert_button_to_enigo(button: MouseButton) -> enigo::MouseButton {
match button {
MouseButton::Left => enigo::MouseButton::Left,
MouseButton::Middle => enigo::MouseButton::Middle,
MouseButton::Right => enigo::MouseButton::Right,
}
}
@@ -0,0 +1,4 @@
mod api;
pub use api::*;
pub mod enigo;
@@ -0,0 +1,90 @@
use crate::metrics_util::warn_if_takes_too_long;
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::time::Duration;
/// Attempts to lock the given mutex.
///
/// Returns the guard even if mutex is poisoned.
///
/// # Panics
///
/// Panics in debug builds if already locked (blocks in release builds).
pub fn non_blocking_lock<'a, T>(
mutex: &'a Mutex<T>,
description: &'static str,
) -> MutexGuard<'a, T> {
// TODO-high-performance This panics when pressing play. Check it out!
// #[cfg(debug_assertions)]
// match mutex.try_lock() {
// Ok(g) => g,
// Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(),
// Err(std::sync::TryLockError::WouldBlock) => {
// panic!("locking mutex would block: {}", description)
// }
// }
// #[cfg(not(debug_assertions))]
blocking_lock(mutex, description)
}
/// Locks the given mutex.
///
/// Returns the guard even if mutex is poisoned.
pub fn blocking_lock_arc<'a, T>(
mutex: &'a Arc<Mutex<T>>,
description: &'static str,
) -> MutexGuard<'a, T> {
blocking_lock(&**mutex, description)
}
/// Locks the given mutex.
///
/// Returns the guard even if mutex is poisoned.
pub fn blocking_lock<'a, T>(mutex: &'a Mutex<T>, description: &'static str) -> MutexGuard<'a, T> {
warn_if_takes_too_long(description, MAX_MUTEX_LOCK_DURATION, || {
match mutex.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
}
})
}
/// Locks the given rw lock.
///
/// Returns the guard even if mutex is poisoned.
pub fn blocking_read_lock<'a, T>(
rw_lock: &'a RwLock<T>,
description: &'static str,
) -> RwLockReadGuard<'a, T> {
warn_if_takes_too_long(description, MAX_MUTEX_LOCK_DURATION, || {
match rw_lock.read() {
Ok(g) => g,
Err(e) => e.into_inner(),
}
})
}
/// Returns `None` if access would block.
pub fn non_blocking_try_read_lock<T>(rw_lock: &RwLock<T>) -> Option<RwLockReadGuard<T>> {
match rw_lock.try_read() {
Ok(g) => Some(g),
Err(std::sync::TryLockError::Poisoned(e)) => Some(e.into_inner()),
Err(std::sync::TryLockError::WouldBlock) => None,
}
}
/// Locks the given rw lock.
///
/// Returns the guard even if mutex is poisoned.
pub fn blocking_write_lock<'a, T>(
rw_lock: &'a RwLock<T>,
description: &'static str,
) -> RwLockWriteGuard<'a, T> {
warn_if_takes_too_long(description, MAX_MUTEX_LOCK_DURATION, || {
match rw_lock.write() {
Ok(g) => g,
Err(e) => e.into_inner(),
}
})
}
const MAX_MUTEX_LOCK_DURATION: Duration = Duration::from_millis(30);
@@ -0,0 +1,36 @@
use either::Either;
use reaper_high::{Reaper, Track};
use reaper_medium::{MediaTrack, ReaperVolumeValue, SoloMode, TrackAttributeKey};
use std::iter;
/// Returns whether the peaks should better be hidden even they are available.
///
/// This is for the case if another track is soloed, reporting the peak would be misleading then.
pub fn peaks_should_be_hidden(track: &Track) -> bool {
let is_master = track.is_master_track();
(is_master && track.is_muted())
|| (!is_master && track.project().any_solo() && track.solo_mode() == SoloMode::Off)
}
/// Returns the track's peaks as iterator.
///
/// This takes VU mode / channel count intricacies into account. It returns peaks even if another
/// track is soloed! See [`peaks_should_be_hidden`].
pub fn get_track_peaks(track: MediaTrack) -> impl ExactSizeIterator<Item = ReaperVolumeValue> {
let reaper = Reaper::get().medium_reaper();
let vu_mode =
unsafe { reaper.get_media_track_info_value(track, TrackAttributeKey::VuMode) as i32 };
let channel_count = if matches!(vu_mode, 2 | 8) {
// These VU modes have multi-channel support.
unsafe { reaper.get_media_track_info_value(track, TrackAttributeKey::Nchan) as i32 }
} else {
// Other VU modes always use stereo.
2
};
if channel_count <= 0 {
return Either::Left(iter::empty());
}
let iter =
(0..channel_count).map(move |ch| unsafe { reaper.track_get_peak_info(track, ch as u32) });
Either::Right(iter)
}
@@ -0,0 +1,7 @@
#[macro_export]
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
@@ -0,0 +1,49 @@
use futures::future::BoxFuture;
use tokio::sync::mpsc::Receiver;
use tracing::debug;
/// An orchestration (task and receiver) to be used to supply the receiver with spare parts that might or might not be
/// necessary.
///
/// This is usually used in scenarios where the consumer lives in a thread that is not allowed to allocate
/// (for example, real-time threads).
pub struct ReplenishmentOrchestration<T, F> {
pub task: F,
pub receiver: ReplenishmentReceiver<T>,
}
/// Creates an orchestration.
///
/// The capacity should be very low, depending on how many spare items you want to create.
pub fn orchestrate_replenishment<T>(
capacity: usize,
mut create_next_item: impl FnMut() -> T + Send + 'static,
) -> ReplenishmentOrchestration<T, BoxFuture<'static, ()>>
where
T: Send + 'static,
{
let (sender, receiver) = tokio::sync::mpsc::channel::<T>(capacity);
let task = async move {
while let Ok(permit) = sender.reserve().await {
debug!("Replenishment channel has capacity. Create next item.");
let item = create_next_item();
permit.send(item);
}
};
ReplenishmentOrchestration {
receiver: ReplenishmentReceiver { receiver },
task: Box::pin(task),
}
}
#[derive(Debug)]
pub struct ReplenishmentReceiver<T> {
receiver: Receiver<T>,
}
impl<T> ReplenishmentReceiver<T> {
/// Returns the next available item if one is available.
pub fn request_item(&mut self) -> Option<T> {
self.receiver.try_recv().ok()
}
}
@@ -0,0 +1,18 @@
use serde_json::Value;
/// https://stackoverflow.com/a/54118457
pub fn merge(a: &mut Value, b: Value) {
if let Value::Object(a) = a {
if let Value::Object(b) = b {
for (k, v) in b {
if v.is_null() {
a.remove(&k);
} else {
merge(a.entry(k).or_insert(Value::Null), v);
}
}
return;
}
}
*a = b;
}
@@ -0,0 +1,84 @@
use ascii::{AsciiChar, AsciiStr, AsciiString, ToAsciiChar};
use core::fmt;
/// String with a maximum of 32 ASCII characters.
///
/// It's useful in the audio thread because it can be cheaply copied and doesn't need allocation.
/// If you are okay with allocation and need cheap cloning, you could just as well use an
/// `Rc<String>`.
pub type SmallAsciiString = LimitedAsciiString<32>;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct LimitedAsciiString<const N: usize> {
length: u8,
content: [u8; N],
}
impl<const N: usize> LimitedAsciiString<N> {
pub const MAX_LENGTH: usize = N;
/// Crops the string if necessary.
pub fn from_ascii_str_cropping(ascii_str: &AsciiStr) -> Self {
let short =
AsciiString::from(&ascii_str.as_slice()[..Self::MAX_LENGTH.min(ascii_str.len())]);
Self::from_ascii_str(&short)
}
/// Returns an error if the given string is not completely ASCII or is too long.
pub fn try_from_str(value: &str) -> Result<Self, &'static str> {
let ascii_string: Result<AsciiString, _> =
value.chars().map(|c| c.to_ascii_char()).collect();
let ascii_string = ascii_string.map_err(|_| "value contains non-ASCII characters")?;
Self::try_from_ascii_str(&ascii_string)
}
/// Returns an error if the given string is too long.
pub fn try_from_ascii_str(ascii_str: &AsciiStr) -> Result<Self, &'static str> {
if ascii_str.len() > Self::MAX_LENGTH {
return Err("ASCII string too large");
}
Ok(Self::from_ascii_str(ascii_str))
}
/// Panics if the given string is too long.
fn from_ascii_str(ascii_str: &AsciiStr) -> Self {
let mut content = [0u8; N];
content[..ascii_str.len()].copy_from_slice(ascii_str.as_bytes());
Self {
content,
length: ascii_str.len() as u8,
}
}
pub fn as_ascii_str(&self) -> &AsciiStr {
AsciiStr::from_ascii(self.as_slice()).unwrap()
}
pub fn as_slice(&self) -> &[u8] {
&self.content[..(self.length as usize)]
}
}
pub fn convert_to_identifier(text: &str) -> Result<SmallAsciiString, &'static str> {
let ascii_string: AsciiString = text
.chars()
// Remove all non-ASCII schars
.filter_map(|c| c.to_ascii_char().ok())
// Allow only letters, digits and underscore
.filter(|ch| ch.is_ascii_alphanumeric() || *ch == AsciiChar::UnderScore)
// Skip leading digits
.skip_while(|ch| ch.is_ascii_digit())
// No uppercase
.map(|ch| ch.to_ascii_lowercase())
.collect();
if ascii_string.is_empty() {
return Err("empty tag");
}
Ok(SmallAsciiString::from_ascii_str_cropping(&ascii_string))
}
impl<const N: usize> fmt::Display for LimitedAsciiString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_ascii_str().fmt(f)
}
}
@@ -0,0 +1,92 @@
use anyhow::{ensure, Context};
use camino::Utf8Path;
use reaper_high::Reaper;
use reaper_low::raw;
use reaper_medium::{
FlexibleOwnedPcmSource, Handle, MeasureAlignment, MidiImportBehavior, OwnedPreviewRegister,
PositionInSeconds, ReaperMutex, ReaperMutexGuard, ReaperVolumeValue,
};
use std::cell::Cell;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct SoundPlayer {
preview_register: Arc<ReaperMutex<OwnedPreviewRegister>>,
play_handle: Cell<Option<Handle<raw::preview_register_t>>>,
}
unsafe impl Send for SoundPlayer {}
impl Default for SoundPlayer {
fn default() -> Self {
Self::new()
}
}
impl SoundPlayer {
pub fn new() -> Self {
let mut register = OwnedPreviewRegister::new();
register.set_volume(ReaperVolumeValue::ZERO_DB);
let preview_register = Arc::new(ReaperMutex::new(register));
Self {
preview_register,
play_handle: Cell::new(None),
}
}
pub fn load_file(&mut self, path_to_file: &Utf8Path) -> anyhow::Result<()> {
ensure!(path_to_file.exists(), "sound file doesn't exist");
let source = Reaper::get()
.medium_reaper()
.pcm_source_create_from_file_ex(path_to_file, MidiImportBehavior::UsePreference)?;
self.load_pcm_source(FlexibleOwnedPcmSource::Reaper(source))
}
pub fn load_pcm_source(&mut self, source: FlexibleOwnedPcmSource) -> anyhow::Result<()> {
let mut preview_register = self.lock_preview_register()?;
preview_register.set_src(Some(source));
Ok(())
}
pub fn volume(&self) -> anyhow::Result<ReaperVolumeValue> {
let preview_register = self.lock_preview_register()?;
Ok(preview_register.volume())
}
pub fn set_volume(&self, volume: ReaperVolumeValue) -> anyhow::Result<()> {
let mut preview_register = self.lock_preview_register()?;
preview_register.set_volume(volume);
Ok(())
}
pub fn play(&self) -> anyhow::Result<()> {
if self.play_handle.get().is_some() {
// Is playing already. Simply rewind.
let mut preview_register = self.lock_preview_register()?;
preview_register.set_cur_pos(PositionInSeconds::ZERO);
} else {
// Is not yet playing. Start playing.
let handle = Reaper::get().medium_session().play_preview_ex(
self.preview_register.clone(),
Default::default(),
MeasureAlignment::PlayImmediately,
)?;
self.play_handle.set(Some(handle));
}
Ok(())
}
pub fn stop(&self) -> anyhow::Result<()> {
let play_handle = self.play_handle.take().context("not playing")?;
Reaper::get().medium_session().stop_preview(play_handle)?;
self.lock_preview_register()?
.set_cur_pos(PositionInSeconds::ZERO);
Ok(())
}
fn lock_preview_register(&self) -> anyhow::Result<ReaperMutexGuard<OwnedPreviewRegister>> {
self.preview_register
.lock()
.context("couldn't acquire preview register lock in sound player")
}
}
@@ -0,0 +1,19 @@
use std::error::Error;
use std::fmt::Display;
pub fn ok_or_log_as_warn<T, E: Display>(result: Result<T, E>) -> Option<T> {
match result {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!("{e}");
None
}
}
}
pub fn log_if_error<T>(result: Result<T, impl AsRef<dyn Error>>) {
if let Err(error) = result {
let error = error.as_ref();
tracing::error!(msg = "Error", error);
}
}
@@ -0,0 +1,42 @@
use crate::hash_util::NonCryptoHashSet;
use std::error::Error;
use std::fmt::Display;
use std::hash::Hash;
#[derive(Debug, derive_more::Display)]
pub struct ValidationError(String);
impl Error for ValidationError {}
#[allow(clippy::unnecessary_filter_map)]
pub fn ensure_no_duplicate<T>(list_label: &str, iter: T) -> Result<(), ValidationError>
where
T: IntoIterator,
T::Item: Eq + Hash + Display,
{
use std::fmt::Write;
let mut uniq = NonCryptoHashSet::default();
let duplicates: NonCryptoHashSet<_> = iter
.into_iter()
.filter_map(|d| {
if uniq.contains(&d) {
Some(d)
} else {
uniq.insert(d);
None
}
})
.collect();
if duplicates.is_empty() {
Ok(())
} else {
let mut s = format!("Found the following duplicate {list_label}: ");
for (i, d) in duplicates.into_iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
let _ = write!(&mut s, "{d}");
}
Err(ValidationError(s))
}
}
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
PROFILE="$1"
set -a
source ".$PROFILE.env"
set +a
# Build commands
cargo build --features "playtime,licensing"
+1
View File
@@ -0,0 +1 @@
large-error-threshold = 200
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "realearn-csi"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
nom.workspace = true
helgoboss-midi.workspace = true
base.workspace = true
helgobox-api.workspace = true
derive_more.workspace = true
[lints.clippy]
enum_glob_use = "deny"
+837
View File
@@ -0,0 +1,837 @@
use base::hash_util::NonCryptoHashSet;
use derive_more::Display;
use helgoboss_midi::{RawShortMessage, ShortMessage, StructuredShortMessage, U14, U7};
use helgobox_api::persistence::{
ApiObject, ButtonFilter, Compartment, Envelope, Glue, Interval, MackieLcdSource,
MackieSevenSegmentDisplayScope, MackieSevenSegmentDisplaySource, Mapping,
MidiChannelPressureAmountSource, MidiControlChangeValueSource, MidiNoteVelocitySource,
MidiPitchBendChangeValueSource, MidiPolyphonicKeyPressureAmountSource,
MidiProgramChangeNumberSource, MidiRawSource, Source, SourceCharacter, Target,
VirtualControlElementCharacter, VirtualControlElementId, VirtualTarget,
};
use std::error::Error;
use std::fmt::{Display, Formatter};
mod parser;
mod schema;
pub use schema::*;
pub enum CsiObject {
Widgets(Vec<Widget>),
}
type CsiResult<T> = Result<T, Box<dyn Error>>;
pub fn deserialize_csi_object_from_csi(text: &str) -> Result<CsiObject, Box<dyn Error>> {
let widgets = parser::mst_file_content(text)?;
Ok(CsiObject::Widgets(widgets))
}
#[derive(Default)]
pub struct Annotator {
context_stack: Vec<String>,
annotations: Vec<Annotation>,
}
impl Annotator {
pub fn new() -> Self {
Self::default()
}
pub fn with_context<R>(&mut self, context: String, f: impl FnOnce(&mut Annotator) -> R) -> R {
self.context_stack.push(context);
let result = f(self);
self.context_stack.pop();
result
}
pub fn info(&mut self, message: impl Into<String>) {
self.annotate(message, AnnotationLevel::Info);
}
pub fn warn(&mut self, message: impl Into<String>) {
self.annotate(message, AnnotationLevel::Warn);
}
fn annotate(&mut self, message: impl Into<String>, level: AnnotationLevel) {
let annotation = Annotation {
context_stack: self.context_stack.clone(),
message: message.into(),
level,
};
self.annotations.push(annotation);
}
pub fn build_result<T>(self, value: T) -> AnnotatedResult<T> {
AnnotatedResult {
value,
annotations: self.annotations,
}
}
}
#[derive(Display)]
enum AnnotationLevel {
#[display(fmt = "INFO")]
Info,
#[display(fmt = "WARN")]
Warn,
}
pub struct Annotation {
context_stack: Vec<String>,
level: AnnotationLevel,
message: String,
}
impl Display for Annotation {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let context_expression = self.context_stack.join(" => ");
write!(f, "{} {}: {}", self.level, context_expression, self.message)
}
}
pub struct AnnotatedResult<T> {
pub value: T,
pub annotations: Vec<Annotation>,
}
impl<T> AnnotatedResult<T> {
pub fn without_annotations(value: T) -> Self {
Self {
value,
annotations: vec![],
}
}
}
impl CsiObject {
pub fn try_into_api_object(self) -> Result<AnnotatedResult<ApiObject>, Box<dyn Error>> {
let mut annotator = Annotator::new();
use CsiObject as O;
let api_object = match self {
O::Widgets(widgets) => {
let results: Vec<_> = widgets
.into_iter()
.filter_map(|w| {
annotator.with_context(format!("Widget \"{}\"", w.name), |annotator| {
match convert_widget(w, annotator) {
Ok(res) => Some(res),
Err(e) => {
annotator.warn(e.to_string());
None
}
}
})
})
.collect();
let has_duplicate_widget_ids = {
let id_set: NonCryptoHashSet<_> =
results.iter().map(|r| r.widget_id.clone()).collect();
results.len() != id_set.len()
};
if has_duplicate_widget_ids {
annotator.warn("Duplicate widget IDs were produced because of truncation. This will most likely lead to problems! Please shorten the affected widget names.")
}
let mappings = results.into_iter().flat_map(|r| r.mappings).collect();
let compartment = Compartment {
mappings: Some(mappings),
..Default::default()
};
ApiObject::ControllerCompartment(Envelope {
version: None,
value: Box::new(compartment),
})
}
};
Ok(annotator.build_result(api_object))
}
}
struct WidgetConvResult {
widget_id: String,
mappings: Vec<Mapping>,
}
fn convert_widget(widget: Widget, annotator: &mut Annotator) -> CsiResult<WidgetConvResult> {
let widget_name = widget.name;
let widget_id = convert_widget_name_to_id(&widget_name, annotator)?;
let mappings = widget
.capabilities
.into_iter()
.flat_map(|c| {
annotator.with_context(format!("Capability \"{c}\""), |annotator| {
convert_capability_to_mappings(&widget_name, &widget_id, c, annotator)
.unwrap_or_else(|e| {
annotator.info(e.to_string());
vec![]
})
})
})
.collect();
let res = WidgetConvResult {
widget_id,
mappings,
};
Ok(res)
}
fn convert_capability_to_mappings(
widget_name: &str,
widget_id: &str,
capability: Capability,
annotator: &mut Annotator,
) -> CsiResult<Vec<Mapping>> {
let base_mapping = Mapping {
id: Some(format!("{widget_id}-{capability}")),
name: Some(format!("{widget_name} - {capability}")),
..Default::default()
};
let target_character = if capability.is_virtual_button() {
VirtualControlElementCharacter::Button
} else {
VirtualControlElementCharacter::Multi
};
let mappings = match capability {
Capability::Press { press, release } => {
let press_res = convert_max_short_msg_to_source(MsgConvInput {
msg: press,
character: SourceCharacter::Button,
press_only: release.is_none(),
fourteen_bit: false,
})?;
// If press-only and we have a value that's neither MAX or MIN, it means we want to a
// message with this particular value ONLY. In this case it's best to create a raw
// MIDI message source.
if let Some(release) = release {
let release_res = convert_max_short_msg_to_source(MsgConvInput {
msg: release,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if release_res.source != press_res.source {
annotator.warn("Press and release messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion not yet. If you really need it, open an issue at GitHub.")
}
}
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(press_res.source),
glue: {
let g = Glue {
button_filter: if release.is_some() {
None
} else {
Some(ButtonFilter::PressOnly)
},
reverse: Some(press_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbTwoState { on, off } => {
let on_res = convert_max_short_msg_to_source(MsgConvInput {
msg: on,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let off_res = convert_max_short_msg_to_source(MsgConvInput {
msg: off,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if off_res.source != on_res.source {
annotator.warn("On and off messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion for this case has not been implemented. If you really need it, open an issue at GitHub.")
}
let mapping = Mapping {
control_enabled: Some(false),
source: Some(on_res.source),
glue: {
let g = Glue {
reverse: Some(on_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::Encoder {
main,
accelerations,
} => {
let acc_conv_res = convert_accelerations(accelerations, annotator)?;
let main_res = convert_max_short_msg_to_source(MsgConvInput {
msg: main,
character: acc_conv_res.character,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(main_res.source),
glue: {
let g = Glue {
step_factor_interval: Some(acc_conv_res.step_factor_interval),
..Default::default()
};
Some(g)
},
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbEncoder { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Relative1,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
control_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::Toggle { on } => {
let on_res = convert_max_short_msg_to_source(MsgConvInput {
msg: on,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(on_res.source),
glue: {
let g = Glue {
reverse: Some(on_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
// TODO-medium Mmh, there's also a separate mapping for that. What's the point of
// "Toggle" then? Maybe it's just a duplicate in the X-Touch mst file. Check!
target: virtual_target(
extended_control_element_id(widget_id, "push")?,
target_character,
),
..base_mapping
};
vec![mapping]
}
Capability::Touch { touch, release } => {
let touch_res = convert_max_short_msg_to_source(MsgConvInput {
msg: touch,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
let release_res = convert_max_short_msg_to_source(MsgConvInput {
msg: release,
character: SourceCharacter::Button,
press_only: false,
fourteen_bit: false,
})?;
if release_res.source != touch_res.source {
annotator.warn("Touch and release messages differ not just in value but also in type or channel. This is very uncommon and might be a mistake or shortcoming of the widget definition. In general, ReaLearn supports such exotic cases but the CSI-to-ReaLearn conversion for this case has not been implemented. If you really need it, open an issue at GitHub.")
}
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(touch_res.source),
glue: {
let g = Glue {
reverse: Some(touch_res.reverse_if_button_like),
..Default::default()
};
Some(g)
},
target: virtual_target(
extended_control_element_id(widget_id, "touch")?,
target_character,
),
..base_mapping
};
vec![mapping]
}
Capability::Fader14Bit { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Range,
press_only: false,
fourteen_bit: true,
})?;
let mapping = Mapping {
feedback_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbFader14Bit { max } => {
let max_res = convert_max_short_msg_to_source(MsgConvInput {
msg: max,
character: SourceCharacter::Range,
press_only: false,
fourteen_bit: true,
})?;
let mapping = Mapping {
control_enabled: Some(false),
source: Some(max_res.source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuVuMeter { index } => {
let source = Source::MidiRaw(MidiRawSource {
feedback_behavior: None,
pattern: Some(format!("D0 [{index:04b} dcba]")),
character: Some(SourceCharacter::Range),
});
let mapping = Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuTimeDisplay => {
let source = Source::MackieSevenSegmentDisplay(MackieSevenSegmentDisplaySource {
scope: Some(MackieSevenSegmentDisplayScope::Tc),
});
let mapping = Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id.to_owned(), target_character),
..base_mapping
};
vec![mapping]
}
Capability::FbMcuDisplayLower { index } => {
let mapping = create_mackie_lcd_mapping(base_mapping, widget_id.to_owned(), index, 1);
vec![mapping]
}
Capability::FbMcuDisplayUpper { index } => {
let mapping = create_mackie_lcd_mapping(base_mapping, widget_id.to_owned(), index, 0);
vec![mapping]
}
Capability::Unknown(_) => {
annotator.warn("Unknown capability. If this is a valid CSI capability, please create a ReaLearn issue at GitHub.");
vec![]
}
};
Ok(mappings)
}
fn extended_control_element_id(base: &str, extension: &str) -> CsiResult<String> {
let res = format!("{base}/{extension}");
if res.len() > MAX_CONTROL_ELEMENT_ID_LENGTH {
return Err(format!("{res} is an invalid control element ID because it's too long. Please shorten the corresponding widget name.").into());
}
Ok(res)
}
fn virtual_target(id: String, character: VirtualControlElementCharacter) -> Option<Target> {
let t = VirtualTarget {
id: VirtualControlElementId::Named(id),
character: Some(character),
learnable: None,
};
Some(Target::Virtual(t))
}
struct AccelerationConvResult {
character: SourceCharacter,
step_factor_interval: Interval<i32>,
}
fn convert_accelerations(
accelerations: Option<Accelerations>,
annotator: &mut Annotator,
) -> CsiResult<AccelerationConvResult> {
let accelerations = if let Some(acc) = accelerations {
acc
} else {
let res = AccelerationConvResult {
character: SourceCharacter::Relative3,
step_factor_interval: Interval(1, 1),
};
return Ok(res);
};
let native_decrements = NativeAcceleration::from_acceleration(accelerations.decrements)
.map_err(|_| "No acceleration values provided for counter-clockwise encoder movement")?;
let native_increments = NativeAcceleration::from_acceleration(accelerations.increments)
.map_err(|_| "No acceleration values provided for clockwise encoder movement")?;
let neutral_accelerations = neutralize_accelerations(native_decrements, native_increments)?;
let res = AccelerationConvResult {
character: neutral_accelerations.character,
step_factor_interval: Interval(1, neutral_accelerations.max_acceleration()),
};
let dec_diff = neutral_accelerations.decrements.diff();
let inc_diff = neutral_accelerations.increments.diff();
if dec_diff.is_non_continuous() || inc_diff.is_non_continuous() {
annotator.warn(
"Non-continuous acceleration profile detected. Encoder acceleration behavior might be slightly different in ReaLearn.",
);
}
if dec_diff != inc_diff {
annotator.warn("Clockwise acceleration profile differs from counter-clockwise acceleration profile. In general supported by ReaLearn but not yet supported by the CSI-to-ReaLearn conversion. That means the acceleration behavior might be slightly different in ReaLearn.");
}
Ok(res)
}
const MAX_CONTROL_ELEMENT_ID_LENGTH: usize = 16;
fn convert_widget_name_to_id(name: &str, annotator: &mut Annotator) -> CsiResult<String> {
let id = name
.chars()
.filter(|ch| ch.is_ascii_alphanumeric() || ch.is_ascii_punctuation())
.take(MAX_CONTROL_ELEMENT_ID_LENGTH)
.collect();
if name.chars().count() > MAX_CONTROL_ELEMENT_ID_LENGTH {
annotator.info(format!("ReaLearn doesn't allow for virtual control element IDs longer than 16 characters, therefore the widget name \"{name}\" was truncated to the ID \"{id}\"."));
}
Ok(id)
}
struct MsgConvOutput {
source: Source,
reverse_if_button_like: bool,
}
struct MsgConvInput {
msg: RawShortMessage,
character: SourceCharacter,
press_only: bool,
fourteen_bit: bool,
}
impl MsgConvInput {
fn should_produce_raw_midi_source_7_bit(&self, value: U7) -> bool {
self.press_only && (1..U7::MAX.get()).contains(&value.get())
}
fn should_produce_raw_midi_source_14_bit(&self, value: U14) -> bool {
self.press_only && (1..U14::MAX.get()).contains(&value.get())
}
}
fn convert_max_short_msg_to_source(input: MsgConvInput) -> CsiResult<MsgConvOutput> {
use StructuredShortMessage as M;
let res = match input.msg.to_structured() {
M::NoteOn {
channel,
key_number,
velocity,
} => {
if input.should_produce_raw_midi_source_7_bit(velocity) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiNoteVelocity(MidiNoteVelocitySource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
}),
reverse_if_button_like: velocity == U7::MIN,
}
}
}
M::NoteOff {
channel,
key_number,
velocity,
} => {
if input.should_produce_raw_midi_source_7_bit(velocity) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiNoteVelocity(MidiNoteVelocitySource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
}),
reverse_if_button_like: true,
}
}
}
M::PolyphonicKeyPressure {
channel,
key_number,
pressure_amount,
} => {
if input.should_produce_raw_midi_source_7_bit(pressure_amount) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiPolyphonicKeyPressureAmount(
MidiPolyphonicKeyPressureAmountSource {
feedback_behavior: None,
channel: Some(channel.get()),
key_number: Some(key_number.get()),
},
),
reverse_if_button_like: pressure_amount == U7::MIN,
}
}
}
M::ControlChange {
channel,
controller_number,
control_value,
} => {
if input.should_produce_raw_midi_source_7_bit(control_value) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiControlChangeValue(MidiControlChangeValueSource {
feedback_behavior: None,
channel: Some(channel.get()),
controller_number: Some(controller_number.get()),
character: Some(input.character),
fourteen_bit: Some(input.fourteen_bit),
}),
reverse_if_button_like: control_value == U7::MIN,
}
}
}
M::ProgramChange {
channel,
program_number,
} => {
if input.should_produce_raw_midi_source_7_bit(program_number) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiProgramChangeNumber(MidiProgramChangeNumberSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: program_number == U7::MIN,
}
}
}
M::ChannelPressure {
channel,
pressure_amount,
} => {
if input.should_produce_raw_midi_source_7_bit(pressure_amount) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiChannelPressureAmount(MidiChannelPressureAmountSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: pressure_amount == U7::MIN,
}
}
}
M::PitchBendChange {
channel,
pitch_bend_value,
} => {
if input.should_produce_raw_midi_source_14_bit(pitch_bend_value) {
convert_short_msg_to_raw_midi_source(input)
} else {
MsgConvOutput {
source: Source::MidiPitchBendChangeValue(MidiPitchBendChangeValueSource {
feedback_behavior: None,
channel: Some(channel.get()),
}),
reverse_if_button_like: pitch_bend_value == U14::MIN,
}
}
}
_ => {
return Err(format!("Message {:?} not handled in source conversion", input.msg).into())
}
};
Ok(res)
}
fn convert_short_msg_to_raw_midi_source(input: MsgConvInput) -> MsgConvOutput {
MsgConvOutput {
source: Source::MidiRaw(MidiRawSource {
feedback_behavior: None,
pattern: Some(convert_to_raw_midi_pattern(input.msg)),
character: Some(input.character),
}),
reverse_if_button_like: false,
}
}
fn convert_to_raw_midi_pattern(msg: RawShortMessage) -> String {
let (status_byte, data_byte_1, data_byte_2) = msg.to_bytes();
format!(
"{:02X} {:02X} {:02X}",
status_byte,
data_byte_1.get(),
data_byte_2.get()
)
}
fn create_mackie_lcd_mapping(
base_mapping: Mapping,
widget_id: String,
index: u8,
line: u8,
) -> Mapping {
let source = Source::MackieLcd(MackieLcdSource {
extender_index: None,
channel: Some(index),
line: Some(line),
});
Mapping {
control_enabled: Some(false),
source: Some(source),
target: virtual_target(widget_id, VirtualControlElementCharacter::Multi),
..base_mapping
}
}
struct NeutralAccelerations {
character: SourceCharacter,
/// This should contain values > 1 where each value contains the decrement amount.
decrements: NeutralAcceleration,
/// This should contain values > 1 where each value contains the increment amount.
increments: NeutralAcceleration,
}
impl NeutralAccelerations {
pub fn max_acceleration(&self) -> i32 {
std::cmp::max(
self.decrements.0.iter().max().copied().unwrap_or(0),
self.increments.0.iter().max().copied().unwrap_or(0),
)
}
}
struct NativeAcceleration(Vec<u8>);
impl NativeAcceleration {
pub fn from_acceleration(acc: Acceleration) -> Result<Self, &'static str> {
let vec = match acc {
Acceleration::Sequence(s) => s,
Acceleration::Range(r) => r.collect(),
};
if vec.is_empty() {
return Err("no acceleration values provided");
}
Ok(Self(vec))
}
pub fn first(&self) -> u8 {
*self.0.first().expect("impossible")
}
pub fn neutralize(self, crementor: i32) -> NeutralAcceleration {
let vec = self
.0
.into_iter()
.map(|b| (b as i32 + crementor).abs())
.collect();
NeutralAcceleration(vec)
}
}
struct NeutralAcceleration(Vec<i32>);
impl NeutralAcceleration {
pub fn diff(&self) -> AccelerationDiff {
let vec = self
.0
.iter()
.copied()
.zip(self.0.iter().copied().skip(1))
.map(|(prev, next)| next - prev)
.collect();
AccelerationDiff(vec)
}
}
#[derive(PartialEq)]
struct AccelerationDiff(Vec<i32>);
impl AccelerationDiff {
pub fn is_non_continuous(&self) -> bool {
self.0.iter().any(|d| *d != 1)
}
}
fn neutralize_accelerations(
decrements: NativeAcceleration,
increments: NativeAcceleration,
) -> CsiResult<NeutralAccelerations> {
let (character, decrementor, incrementor) = match (decrements.first(), increments.first()) {
(121..=127, 1..=7) => (SourceCharacter::Relative1, -128, 0),
(57..=63, 65..=71) => (SourceCharacter::Relative2, -64, -64),
(65..=71, 1..=7) => (SourceCharacter::Relative3, -64, 0),
_ => return Err("Unsupported relative encoder type".into()),
};
let neutralized_acc = NeutralAccelerations {
character,
decrements: decrements.neutralize(decrementor),
increments: increments.neutralize(incrementor),
};
Ok(neutralized_acc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neutralize_accelerations_relative_3() {
// Given
let decrements = NativeAcceleration(vec![0x41, 0x42, 0x43]);
let increments = NativeAcceleration(vec![0x01, 0x02, 0x03]);
// When
let neutralized = neutralize_accelerations(decrements, increments).unwrap();
// Then
assert_eq!(neutralized.character, SourceCharacter::Relative3);
assert_eq!(neutralized.decrements.0, vec![1, 2, 3]);
assert_eq!(neutralized.increments.0, vec![1, 2, 3]);
}
#[test]
fn neutralize_accelerations_relative_1() {
// Given
let decrements = NativeAcceleration(vec![0x7f, 0x7e, 0x7c, 0x7a]);
let increments = NativeAcceleration(vec![0x01, 0x04, 0x07]);
// When
let neutralized = neutralize_accelerations(decrements, increments).unwrap();
// Then
assert_eq!(neutralized.character, SourceCharacter::Relative1);
assert_eq!(neutralized.decrements.0, vec![1, 2, 4, 6]);
assert_eq!(neutralized.increments.0, vec![1, 4, 7]);
}
#[test]
fn neutral_diff() {
// Given
let increments = NeutralAcceleration(vec![0x01, 0x04, 0x07]);
// When
let diff = increments.diff();
// Then
assert_eq!(diff.0, vec![3, 3]);
}
}
+457
View File
@@ -0,0 +1,457 @@
use crate::schema::{Acceleration, Accelerations, Capability, Widget};
use helgoboss_midi::{RawShortMessage, ShortMessageFactory};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while1, take_while_m_n};
use nom::character::complete::{multispace0, not_line_ending, space0, space1};
use nom::combinator::{all_consuming, map, map_res, opt, verify};
use nom::error::ParseError;
use nom::multi::{separated_list0, separated_list1};
use nom::sequence::{preceded, separated_pair};
use nom::{character::complete::char, sequence::delimited, sequence::tuple, Err, IResult, Parser};
use std::convert::TryInto;
type Res<'a, T> = IResult<&'a str, T>;
pub fn mst_file_content(input: &str) -> Result<Vec<Widget>, String> {
let non_comment_lines: Vec<_> = input
.lines()
.filter(|l| !l.trim_start().starts_with('/'))
.collect();
let input_without_comments = non_comment_lines.join("\n");
let (_, widgets) = all_consuming(widgets)(&input_without_comments).map_err(|e| {
let short_err = match e {
Err::Error(e) => Err::Error(nom::error::Error::new(&e.input[0..30], e.code)),
e => e,
};
short_err.to_string()
})?;
Ok(widgets)
}
fn widgets(input: &str) -> Res<Vec<Widget>> {
delimited(
multispace0,
separated_list0(space_with_at_least_one_line_ending, widget),
multispace0,
)(input)
}
fn widget(input: &str) -> Res<Widget> {
map(
tuple((
widget_begin,
space_with_at_least_one_line_ending,
widget_capabilities,
space_with_at_least_one_line_ending,
tag("WidgetEnd"),
)),
|(name, _, capabilities, _, _)| Widget {
name: name.to_owned(),
capabilities,
},
)(input)
}
fn widget_begin(input: &str) -> Res<&str> {
preceded(
tuple((tag("Widget"), space1)),
take_while1(|ch: char| ch.is_alphanumeric() || matches!(ch, '-' | '_')),
)(input)
}
fn widget_capabilities(input: &str) -> Res<Vec<Capability>> {
separated_list0(space_with_at_least_one_line_ending, capability)(input)
}
fn capability(input: &str) -> Res<Capability> {
alt((
capability_press,
capability_fb_two_state,
capability_encoder,
capability_fb_encoder,
capability_toggle,
capability_fader_14_bit,
capability_fb_fader_14_bit,
capability_touch,
capability_fb_mcu_display_upper,
capability_fb_mcu_display_lower,
capability_fb_mcu_vu_meter,
capability_fb_mcu_time_display,
capability_unknown,
))(input)
}
fn capability_press(input: &str) -> Res<Capability> {
map(util::capability_msg_opt_msg("Press"), |(press, release)| {
Capability::Press { press, release }
})(input)
}
fn capability_fb_two_state(input: &str) -> Res<Capability> {
map(util::capability_msg_msg("FB_TwoState"), |(on, off)| {
Capability::FbTwoState { on, off }
})(input)
}
fn capability_fb_encoder(input: &str) -> Res<Capability> {
map(util::capability_msg("FB_Encoder"), |max| {
Capability::FbEncoder { max }
})(input)
}
fn capability_toggle(input: &str) -> Res<Capability> {
map(util::capability_msg("Toggle"), |on| Capability::Toggle {
on,
})(input)
}
fn capability_fader_14_bit(input: &str) -> Res<Capability> {
map(util::capability_msg("Fader14Bit"), |max| {
Capability::Fader14Bit { max }
})(input)
}
fn capability_fb_fader_14_bit(input: &str) -> Res<Capability> {
map(util::capability_msg("FB_Fader14Bit"), |max| {
Capability::FbFader14Bit { max }
})(input)
}
fn capability_touch(input: &str) -> Res<Capability> {
map(util::capability_msg_msg("Touch"), |(on, off)| {
Capability::Touch {
touch: on,
release: off,
}
})(input)
}
fn capability_fb_mcu_display_upper(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUDisplayUpper"), |index| {
Capability::FbMcuDisplayUpper { index }
})(input)
}
fn capability_fb_mcu_display_lower(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUDisplayLower"), |index| {
Capability::FbMcuDisplayLower { index }
})(input)
}
fn capability_fb_mcu_vu_meter(input: &str) -> Res<Capability> {
map(util::capability_index("FB_MCUVUMeter"), |index| {
Capability::FbMcuVuMeter { index }
})(input)
}
fn capability_fb_mcu_time_display(input: &str) -> Res<Capability> {
map(util::capability_empty("FB_MCUTimeDisplay"), |_| {
Capability::FbMcuTimeDisplay
})(input)
}
fn capability_encoder(input: &str) -> Res<Capability> {
map(
tuple((
preceded(tuple((tag("Encoder"), space1)), short_midi_msg),
opt(preceded(space1, accelerations)),
)),
|(main, accelerations)| Capability::Encoder {
main,
accelerations,
},
)(input)
}
fn capability_unknown(input: &str) -> Res<Capability> {
map(
verify(not_line_ending, |s: &str| s != "WidgetEnd"),
|line: &str| Capability::Unknown(line.to_owned()),
)(input)
}
fn short_midi_msg(input: &str) -> Res<RawShortMessage> {
map_res(
tuple((hex_byte, space1, hex_byte, space1, hex_byte)),
|(b1, _, b2, _, b3)| {
RawShortMessage::from_bytes((
b1,
b2.try_into().map_err(|_| "data byte 1 too high")?,
b3.try_into().map_err(|_| "data byte 2 too high")?,
))
.map_err(|_| "invalid short message")
},
)(input)
}
fn accelerations(input: &str) -> Res<Accelerations> {
map(
delimited(
ws(char('[')),
tuple((
parameterized_acceleration('<'),
parameterized_acceleration('>'),
)),
ws(char(']')),
),
|(decrements, increments)| Accelerations {
increments,
decrements,
},
)(input)
}
fn parameterized_acceleration<'a>(
letter: char,
) -> impl FnMut(&'a str) -> IResult<&'a str, Acceleration> {
preceded(ws(char(letter)), acceleration)
}
fn acceleration(input: &str) -> Res<Acceleration> {
alt((acceleration_range, acceleration_sequence))(input)
}
fn acceleration_sequence(input: &str) -> Res<Acceleration> {
map(separated_list1(space1, hex_byte), |values| {
Acceleration::Sequence(values)
})(input)
}
fn acceleration_range(input: &str) -> Res<Acceleration> {
map(
separated_pair(hex_byte, char('-'), hex_byte),
|(min, max)| Acceleration::Range(min..=max),
)(input)
}
fn hex_byte(input: &str) -> Res<u8> {
map_res(take_while_m_n(2, 2, util::is_hex_digit), util::from_hex)(input)
}
fn space_with_at_least_one_line_ending(input: &str) -> Res<&str> {
verify(multispace0, |s: &str| s.contains(&['\r', '\n'][..]))(input)
}
/// Surrounded by optional whitespace (no line endings).
fn ws<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
E: ParseError<&'a str>,
{
delimited(space0, p, space0)
}
mod util {
use super::*;
use nom::character::complete::digit1;
use nom::combinator::value;
pub fn is_hex_digit(c: char) -> bool {
c.is_ascii_hexdigit()
}
pub fn from_hex(input: &str) -> Result<u8, std::num::ParseIntError> {
u8::from_str_radix(input, 16)
}
pub fn capability_msg_opt_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, (RawShortMessage, Option<RawShortMessage>)> {
preceded(
tag(name),
tuple((
preceded(space1, short_midi_msg),
opt(preceded(space1, short_midi_msg)),
)),
)
}
pub fn capability_msg_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, (RawShortMessage, RawShortMessage)> {
preceded(
tag(name),
tuple((
preceded(space1, short_midi_msg),
preceded(space1, short_midi_msg),
)),
)
}
pub fn capability_index<'a>(name: &'static str) -> impl FnMut(&'a str) -> Res<'a, u8> {
map_res(preceded(tuple((tag(name), space1)), digit1), |s: &str| {
s.parse::<u8>()
})
}
pub fn capability_empty<'a>(name: &'static str) -> impl FnMut(&'a str) -> Res<'a, ()> {
value((), tag(name))
}
pub fn capability_msg<'a>(
name: &'static str,
) -> impl FnMut(&'a str) -> Res<'a, RawShortMessage> {
preceded(tag(name), preceded(space1, short_midi_msg))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{Acceleration, Widget};
use helgoboss_midi::test_util::u7;
use helgoboss_midi::ShortMessageFactory;
#[test]
fn parse_widgets() {
let mst_content = include_str!("test_data/test.mst");
let (_, widgets) = widgets(mst_content).unwrap();
assert_eq!(widgets.len(), 146);
for w in widgets {
for c in w.capabilities {
assert!(!c.is_unknown());
}
}
}
#[test]
fn parse_widget() {
assert_eq!(
widget(
"\
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
Weird eu 898 dqwun wd08 . ---
WidgetEnd"
),
Ok((
"",
Widget {
name: "RecordArm1".to_owned(),
capabilities: vec![
Capability::Press {
press: short(0x90, 0x00, 0x7f),
release: Some(short(0x90, 0x00, 0x00)),
},
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
},
Capability::Unknown("Weird eu 898 dqwun wd08 . ---".to_owned())
]
}
))
);
}
#[test]
fn parse_widget_ugly_formatting() {
assert_eq!(
widget(
"\
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
Weird eu 898 dqwun wd08 . ---
WidgetEnd"
),
Ok((
"",
Widget {
name: "RecordArm1".to_owned(),
capabilities: vec![
Capability::Press {
press: short(0x90, 0x00, 0x7f),
release: Some(short(0x90, 0x00, 0x00)),
},
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
},
Capability::Unknown("Weird eu 898 dqwun wd08 . ---".to_owned())
]
}
))
);
}
#[test]
fn parse_press_capability_without_release() {
assert_eq!(
capability("Press 90 28 7f"),
Ok((
"",
Capability::Press {
press: short(0x90, 0x28, 0x7f),
release: None,
}
))
);
}
#[test]
fn parse_press_capability_with_release() {
assert_eq!(
capability("Press 90 28 7f 90 28 00"),
Ok((
"",
Capability::Press {
press: short(0x90, 0x28, 0x7f),
release: Some(short(0x90, 0x28, 0x00)),
}
))
);
}
#[test]
fn parse_fb_two_state_capability() {
assert_eq!(
capability("FB_TwoState 90 00 7f 90 00 00"),
Ok((
"",
Capability::FbTwoState {
on: short(0x90, 0x00, 0x7f),
off: short(0x90, 0x00, 0x00),
}
))
);
}
#[test]
fn parse_encoder_capability_with_range() {
assert_eq!(
capability("Encoder b0 10 7f [ < 41-48 > 01-08 ]"),
Ok((
"",
Capability::Encoder {
main: short(0xb0, 0x10, 0x7f),
accelerations: Some(Accelerations {
decrements: Acceleration::Range(0x41..=0x48),
increments: Acceleration::Range(0x01..=0x08)
})
}
))
);
}
#[test]
fn parse_short_midi_msg() {
assert_eq!(
short_midi_msg("90 28 7f"),
Ok(("", short(0x90, 0x28, 0x7f)))
);
}
#[test]
fn single_hex_byte() {
assert_eq!(hex_byte("90"), Ok(("", 0x90)));
}
fn short(status_byte: u8, data_byte_1: u8, data_byte_2: u8) -> RawShortMessage {
RawShortMessage::from_bytes((status_byte, u7(data_byte_1), u7(data_byte_2))).unwrap()
}
}
+74
View File
@@ -0,0 +1,74 @@
use derive_more::Display;
use helgoboss_midi::RawShortMessage;
use std::ops::RangeInclusive;
#[derive(Eq, PartialEq, Debug)]
pub struct Widget {
pub name: String,
pub capabilities: Vec<Capability>,
}
#[derive(Eq, PartialEq, Debug, Display)]
pub enum Capability {
#[display(fmt = "Press")]
Press {
press: RawShortMessage,
release: Option<RawShortMessage>,
},
#[display(fmt = "FB_TwoState")]
FbTwoState {
on: RawShortMessage,
off: RawShortMessage,
},
#[display(fmt = "Encoder")]
Encoder {
main: RawShortMessage,
accelerations: Option<Accelerations>,
},
#[display(fmt = "FB_Encoder")]
FbEncoder { max: RawShortMessage },
#[display(fmt = "Toggle")]
Toggle { on: RawShortMessage },
#[display(fmt = "Fader14Bit")]
Fader14Bit { max: RawShortMessage },
#[display(fmt = "FB_Fader14Bit")]
FbFader14Bit { max: RawShortMessage },
#[display(fmt = "Touch")]
Touch {
touch: RawShortMessage,
release: RawShortMessage,
},
#[display(fmt = "FB_MCUDisplayLower")]
FbMcuDisplayLower { index: u8 },
#[display(fmt = "FB_MCUDisplayUpper")]
FbMcuDisplayUpper { index: u8 },
#[display(fmt = "FB_MCUTimeDisplay")]
FbMcuTimeDisplay,
#[display(fmt = "FB_MCUVUMeter")]
FbMcuVuMeter { index: u8 },
#[display(fmt = "{_0}")]
Unknown(String),
}
impl Capability {
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown(_))
}
pub fn is_virtual_button(&self) -> bool {
use Capability as C;
matches!(self, C::Press { .. } | C::Toggle { .. } | C::Touch { .. })
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct Accelerations {
pub decrements: Acceleration,
pub increments: Acceleration,
}
#[derive(Eq, PartialEq, Debug)]
pub enum Acceleration {
Sequence(Vec<u8>),
Range(RangeInclusive<u8>),
}
@@ -0,0 +1,711 @@
Widget RecordArm1
Press 90 00 7f 90 00 00
FB_TwoState 90 00 7f 90 00 00
WidgetEnd
Widget RecordArm2
Press 90 01 7f 90 01 00
FB_TwoState 90 01 7f 90 01 00
WidgetEnd
Widget RecordArm3
Press 90 02 7f 90 02 00
FB_TwoState 90 02 7f 90 02 00
WidgetEnd
Widget RecordArm4
Press 90 03 7f 90 03 00
FB_TwoState 90 03 7f 90 03 00
WidgetEnd
Widget RecordArm5
Press 90 04 7f 90 04 00
FB_TwoState 90 04 7f 90 04 00
WidgetEnd
Widget RecordArm6
Press 90 05 7f 90 05 00
FB_TwoState 90 05 7f 90 05 00
WidgetEnd
Widget RecordArm7
Press 90 06 7f 90 06 00
FB_TwoState 90 06 7f 90 06 00
WidgetEnd
Widget RecordArm8
Press 90 07 7f 90 07 00
FB_TwoState 90 07 7f 90 07 00
WidgetEnd
Widget Solo1
Press 90 08 7f 90 08 00
FB_TwoState 90 08 7f 90 08 00
WidgetEnd
Widget Solo2
Press 90 09 7f 90 09 00
FB_TwoState 90 09 7f 90 09 00
WidgetEnd
Widget Solo3
Press 90 0a 7f 90 0a 00
FB_TwoState 90 0a 7f 90 0a 00
WidgetEnd
Widget Solo4
Press 90 0b 7f 90 0b 00
FB_TwoState 90 0b 7f 90 0b 00
WidgetEnd
Widget Solo5
Press 90 0c 7f 90 0c 00
FB_TwoState 90 0c 7f 90 0c 00
WidgetEnd
Widget Solo6
Press 90 0d 7f 90 0d 00
FB_TwoState 90 0d 7f 90 0d 00
WidgetEnd
Widget Solo7
Press 90 0e 7f 90 0e 00
FB_TwoState 90 0e 7f 90 0e 00
WidgetEnd
Widget Solo8
Press 90 0f 7f 90 0f 00
FB_TwoState 90 0f 7f 90 0f 00
WidgetEnd
Widget Mute1
Press 90 10 7f 90 10 00
FB_TwoState 90 10 7f 90 10 00
WidgetEnd
Widget Mute2
Press 90 11 7f 90 11 00
FB_TwoState 90 11 7f 90 11 00
WidgetEnd
Widget Mute3
Press 90 12 7f 90 12 00
FB_TwoState 90 12 7f 90 12 00
WidgetEnd
Widget Mute4
Press 90 13 7f 90 13 00
FB_TwoState 90 13 7f 90 13 00
WidgetEnd
Widget Mute5
Press 90 14 7f 90 14 00
FB_TwoState 90 14 7f 90 14 00
WidgetEnd
Widget Mute6
Press 90 15 7f 90 15 00
FB_TwoState 90 15 7f 90 15 00
WidgetEnd
Widget Mute7
Press 90 16 7f 90 16 00
FB_TwoState 90 16 7f 90 16 00
WidgetEnd
Widget Mute8
Press 90 17 7f 90 17 00
FB_TwoState 90 17 7f 90 17 00
WidgetEnd
Widget Select1
Press 90 18 7f 90 18 00
FB_TwoState 90 18 7f 90 18 00
WidgetEnd
Widget Select2
Press 90 19 7f 90 19 00
FB_TwoState 90 19 7f 90 19 00
WidgetEnd
Widget Select3
Press 90 1a 7f 90 1a 00
FB_TwoState 90 1a 7f 90 1a 00
WidgetEnd
Widget Select4
Press 90 1b 7f 90 1b 00
FB_TwoState 90 1b 7f 90 1b 00
WidgetEnd
Widget Select5
Press 90 1c 7f 90 1c 00
FB_TwoState 90 1c 7f 90 1c 00
WidgetEnd
Widget Select6
Press 90 1d 7f 90 1d 00
FB_TwoState 90 1d 7f 90 1d 00
WidgetEnd
Widget Select7
Press 90 1e 7f 90 1e 00
FB_TwoState 90 1e 7f 90 1e 00
WidgetEnd
Widget Select8
Press 90 1f 7f 90 1f 00
FB_TwoState 90 1f 7f 90 1f 00
WidgetEnd
Widget RotaryPush1
Press 90 20 7f 90 20 00
WidgetEnd
Widget RotaryPush2
Press 90 21 7f 90 21 00
WidgetEnd
Widget RotaryPush3
Press 90 22 7f 90 22 00
WidgetEnd
Widget RotaryPush4
Press 90 23 7f 90 23 00
WidgetEnd
Widget RotaryPush5
Press 90 24 7f 90 24 00
WidgetEnd
Widget RotaryPush6
Press 90 25 7f 90 25 00
WidgetEnd
Widget RotaryPush7
Press 90 26 7f 90 26 00
WidgetEnd
Widget RotaryPush8
Press 90 27 7f 90 27 00
WidgetEnd
Widget Rotary1
Encoder b0 10 7f [ < 41-48 > 01-08 ]
FB_Encoder b0 10 7f
Toggle 90 20 7f
WidgetEnd
Widget Rotary2
Encoder b0 11 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 11 7f
Toggle 90 21 7f
WidgetEnd
Widget Rotary3
Encoder b0 12 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 12 7f
Toggle 90 22 7f
WidgetEnd
Widget Rotary4
Encoder b0 13 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 13 7f
Toggle 90 23 7f
WidgetEnd
Widget Rotary5
Encoder b0 14 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 14 7f
Toggle 90 24 7f
WidgetEnd
Widget Rotary6
Encoder b0 15 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 15 7f
Toggle 90 25 7f
WidgetEnd
Widget Rotary7
Encoder b0 16 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 16 7f
Toggle 90 26 7f
WidgetEnd
Widget Rotary8
Encoder b0 17 7f [ < 41-4a > 01-09 ]
FB_Encoder b0 17 7f
Toggle 90 27 7f
WidgetEnd
Widget Track
Press 90 28 7f 90 28 00
FB_TwoState 90 28 7f 90 28 00
WidgetEnd
Widget Send
Press 90 29 7f 90 29 00
FB_TwoState 90 29 7f 90 29 00
WidgetEnd
Widget Pan
Press 90 2a 7f 90 2a 00
FB_TwoState 90 2a 7f 90 2a 00
WidgetEnd
Widget Plugin
Press 90 2b 7f 90 2b 00
FB_TwoState 90 2b 7f 90 2b 00
WidgetEnd
Widget EQ
Press 90 2c 7f 90 2c 00
FB_TwoState 90 2c 7f 90 2c 00
WidgetEnd
Widget Instrument
Press 90 2d 7f 90 2d 00
FB_TwoState 90 2d 7f 90 2d 00
WidgetEnd
Widget BankLeft
Press 90 2e 7f 90 2e 00
FB_TwoState 90 2e 7f 90 2e 00
WidgetEnd
Widget BankRight
Press 90 2f 7f 90 2f 00
FB_TwoState 90 2f 7f 90 2f 00
WidgetEnd
Widget ChannelLeft
Press 90 30 7f 90 30 00
FB_TwoState 90 30 7f 90 30 00
WidgetEnd
Widget ChannelRight
Press 90 31 7f 90 31 00
FB_TwoState 90 31 7f 90 31 00
WidgetEnd
Widget Flip
Press 90 32 7f 90 32 00
FB_TwoState 90 32 7f 90 32 00
WidgetEnd
Widget GlobalView
Press 90 33 7f 90 33 00
FB_TwoState 90 33 7f 90 33 00
WidgetEnd
Widget BPM-Time
Press 90 35 7f 90 35 00
FB_TwoState 90 35 7f 90 35 00
WidgetEnd
Widget nameValue
Press 90 34 7f
WidgetEnd
Widget F1
Press 90 36 7f 90 36 00
FB_TwoState 90 36 7f 90 36 00
WidgetEnd
Widget F2
Press 90 37 7f 90 37 00
FB_TwoState 90 37 7f 90 37 00
WidgetEnd
Widget F3
Press 90 38 7f 90 38 00
FB_TwoState 90 38 7f 90 38 00
WidgetEnd
Widget F4
Press 90 39 7f 90 39 00
FB_TwoState 90 39 7f 90 39 00
WidgetEnd
Widget F5
Press 90 3a 7f 90 3a 00
FB_TwoState 90 3a 7f 90 3a 00
WidgetEnd
Widget F6
Press 90 3b 7f 90 3b 00
FB_TwoState 90 3b 7f 90 3b 00
WidgetEnd
Widget F7
Press 90 3c 7f 90 3c 00
FB_TwoState 90 3c 7f 90 3c 00
WidgetEnd
Widget F8
Press 90 3d 7f 90 3d 00
FB_TwoState 90 3d 7f 90 3d 00
WidgetEnd
Widget MidiTracks
Press 90 3e 7f 90 3e 00
FB_TwoState 90 3e 7f 90 3e 00
WidgetEnd
Widget Inputs
Press 90 3f 7f 90 3f 00
FB_TwoState 90 3f 7f 90 3f 00
WidgetEnd
Widget AudioTracks
Press 90 40 7f 90 40 00
FB_TwoState 90 40 7f 90 40 00
WidgetEnd
Widget AudioInstrument
Press 90 41 7f 90 41 00
FB_TwoState 90 41 7f 90 41 00
WidgetEnd
Widget Aux
Press 90 42 7f 90 42 00
FB_TwoState 90 42 7f 90 42 00
WidgetEnd
Widget Busses
Press 90 43 7f 90 43 00
FB_TwoState 90 43 7f 90 43 00
WidgetEnd
Widget Outputs
Press 90 44 7f 90 44 00
FB_TwoState 90 44 7f 90 44 00
WidgetEnd
Widget User
Press 90 45 7f 90 45 00
FB_TwoState 90 45 7f 90 45 00
WidgetEnd
Widget Shift
Press 90 46 7f 90 46 00
FB_TwoState 90 46 7f 90 46 00
WidgetEnd
Widget Option
Press 90 47 7f 90 47 00
FB_TwoState 90 47 7f 90 47 00
WidgetEnd
Widget Control
Press 90 48 7f 90 48 00
FB_TwoState 90 48 7f 90 48 00
WidgetEnd
Widget Alt
Press 90 49 7f 90 49 00
FB_TwoState 90 49 7f 90 49 00
WidgetEnd
Widget Read
Press 90 4a 7f 90 4a 00
FB_TwoState 90 4a 7f 90 4a 00
WidgetEnd
Widget Write
Press 90 4b 7f 90 4b 00
FB_TwoState 90 4b 7f 90 4b 00
WidgetEnd
Widget Trim
Press 90 4c 7f 90 4c 00
FB_TwoState 90 4c 7f 90 4c 00
WidgetEnd
Widget Touch
Press 90 4d 7f 90 4d 00
FB_TwoState 90 4d 7f 90 4d 00
WidgetEnd
Widget Latch
Press 90 4e 7f 90 4e 00
FB_TwoState 90 4e 7f 90 4e 00
WidgetEnd
Widget Group
Press 90 4f 7f 90 4f 00
FB_TwoState 90 4f 7f 90 4f 00
WidgetEnd
Widget Save
Press 90 50 7f 90 50 00
FB_TwoState 90 50 7f 90 50 00
WidgetEnd
Widget Undo
Press 90 51 7f 90 51 00
FB_TwoState 90 51 7f 90 51 00
WidgetEnd
Widget Cancel
Press 90 52 7f 90 52 00
FB_TwoState 90 52 7f 90 52 00
WidgetEnd
Widget Enter
Press 90 53 7f 90 53 00
FB_TwoState 90 53 7f 90 53 00
WidgetEnd
Widget Marker
Press 90 54 7f 90 54 00
FB_TwoState 90 54 7f 90 54 00
WidgetEnd
Widget Nudge
Press 90 55 7f 90 55 00
FB_TwoState 90 55 7f 90 55 00
WidgetEnd
Widget Cycle
Press 90 56 7f 90 56 00
FB_TwoState 90 56 7f 90 56 00
WidgetEnd
Widget Drop
Press 90 57 7f 90 57 00
FB_TwoState 90 57 7f 90 57 00
WidgetEnd
Widget Replace
Press 90 58 7f 90 58 00
FB_TwoState 90 58 7f 90 58 00
WidgetEnd
Widget Click
Press 90 59 7f 90 59 00
FB_TwoState 90 59 7f 90 59 00
WidgetEnd
Widget Solo
Press 90 5a 7f 90 5a 00
FB_TwoState 90 5a 7f 90 5a 00
WidgetEnd
Widget Rewind
Press 90 5b 7f 90 5b 00
FB_TwoState 90 5b 7f 90 5b 00
WidgetEnd
Widget FastForward
Press 90 5c 7f 90 5c 00
FB_TwoState 90 5c 7f 90 5c 00
WidgetEnd
Widget Stop
Press 90 5d 7f 90 5d 00
FB_TwoState 90 5d 7f 90 5d 00
WidgetEnd
Widget Play
Press 90 5e 7f 90 5e 00
FB_TwoState 90 5e 7f 90 5e 00
WidgetEnd
Widget Record
Press 90 5f 7f 90 5f 00
FB_TwoState 90 5f 7f 90 5f 00
WidgetEnd
Widget Up
Press 90 60 7f 90 60 00
FB_TwoState 90 60 7f 90 60 00
WidgetEnd
Widget Down
Press 90 61 7f 90 61 00
FB_TwoState 90 61 7f 90 61 00
WidgetEnd
Widget Left
Press 90 62 7f 90 62 00
FB_TwoState 90 62 7f 90 62 00
WidgetEnd
Widget Right
Press 90 63 7f 90 63 00
FB_TwoState 90 63 7f 90 63 00
WidgetEnd
Widget Zoom
Press 90 64 7f 90 64 00
FB_TwoState 90 64 7f 90 64 00
WidgetEnd
Widget Scrub
Press 90 65 7f 90 65 00
FB_TwoState 90 65 7f 90 65 00
WidgetEnd
Widget Fader1
Fader14Bit e0 7f 7f
FB_Fader14Bit e0 7f 7f
Touch 90 68 7f 90 68 00
WidgetEnd
Widget Fader2
Fader14Bit e1 7f 7f
FB_Fader14Bit e1 7f 7f
Touch 90 69 7f 90 69 00
WidgetEnd
Widget Fader3
Fader14Bit e2 7f 7f
FB_Fader14Bit e2 7f 7f
Touch 90 6a 7f 90 6a 00
WidgetEnd
Widget Fader4
Fader14Bit e3 7f 7f
FB_Fader14Bit e3 7f 7f
Touch 90 6b 7f 90 6b 00
WidgetEnd
Widget Fader5
Fader14Bit e4 7f 7f
FB_Fader14Bit e4 7f 7f
Touch 90 6c 7f 90 6c 00
WidgetEnd
Widget Fader6
Fader14Bit e5 7f 7f
FB_Fader14Bit e5 7f 7f
Touch 90 6d 7f 90 6d 00
WidgetEnd
Widget Fader7
Fader14Bit e6 7f 7f
FB_Fader14Bit e6 7f 7f
Touch 90 6e 7f 90 6e 00
WidgetEnd
Widget Fader8
Fader14Bit e7 7f 7f
FB_Fader14Bit e7 7f 7f
Touch 90 6f 7f 90 6f 00
WidgetEnd
Widget MasterFader
Fader14Bit e8 7f 7f
FB_Fader14Bit e8 7f 7f
Touch e8 7f 7f e8 7f 00
WidgetEnd
Widget DisplayLower1
FB_MCUDisplayLower 0
WidgetEnd
Widget DisplayLower2
FB_MCUDisplayLower 1
WidgetEnd
Widget DisplayLower3
FB_MCUDisplayLower 2
WidgetEnd
Widget DisplayLower4
FB_MCUDisplayLower 3
WidgetEnd
Widget DisplayLower5
FB_MCUDisplayLower 4
WidgetEnd
Widget DisplayLower6
FB_MCUDisplayLower 5
WidgetEnd
Widget DisplayLower7
FB_MCUDisplayLower 6
WidgetEnd
Widget DisplayLower8
FB_MCUDisplayLower 7
WidgetEnd
Widget DisplayUpper1
FB_MCUDisplayUpper 0
WidgetEnd
Widget DisplayUpper2
FB_MCUDisplayUpper 1
WidgetEnd
Widget DisplayUpper3
FB_MCUDisplayUpper 2
WidgetEnd
Widget DisplayUpper4
FB_MCUDisplayUpper 3
WidgetEnd
Widget DisplayUpper5
FB_MCUDisplayUpper 4
WidgetEnd
Widget DisplayUpper6
FB_MCUDisplayUpper 5
WidgetEnd
Widget DisplayUpper7
FB_MCUDisplayUpper 6
WidgetEnd
Widget DisplayUpper8
FB_MCUDisplayUpper 7
WidgetEnd
Widget TimeDisplay
FB_MCUTimeDisplay
WidgetEnd
Widget VUMeter1
FB_MCUVUMeter 0
WidgetEnd
Widget VUMeter2
FB_MCUVUMeter 1
WidgetEnd
Widget VUMeter3
FB_MCUVUMeter 2
WidgetEnd
Widget VUMeter4
FB_MCUVUMeter 3
WidgetEnd
Widget VUMeter5
FB_MCUVUMeter 4
WidgetEnd
Widget VUMeter6
FB_MCUVUMeter 5
WidgetEnd
Widget VUMeter7
FB_MCUVUMeter 6
WidgetEnd
Widget VUMeter8
FB_MCUVUMeter 7
WidgetEnd
Widget JogWheelRotaryCW1
Press b0 3c 01
WidgetEnd
Widget JogWheelRotaryCCW1
Press b0 3c 41
WidgetEnd
+246
View File
@@ -0,0 +1,246 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = true
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"BSL-1.0",
"CC0-1.0",
"OFL-1.1",
"Zlib",
"MPL-2.0",
"Unicode-DFS-2016",
"LicenseRef-UFL-1.0",
"OpenSSL",
#"Apache-2.0 WITH LLVM-exception",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.93
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
[[licenses.clarify]]
# The package spec the clarification applies to
crate = "ring"
# The SPDX expression for the license requirements of the crate
expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
{ path = "LICENSE", hash = 0xbd0eed23 }
]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = true
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "helgobox-dialogs"
version = "0.1.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2021"
publish = false
[dependencies]
derive_more.workspace = true
indexmap.workspace = true
[lints.clippy]
# Enum glob use is not really dangerous in this module. Non-critical.
enum_glob_use = "allow"
+759
View File
@@ -0,0 +1,759 @@
#![allow(non_camel_case_types, clippy::upper_case_acronyms)]
use indexmap::IndexMap;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Add;
pub type Caption = &'static str;
pub struct ResourceInfo {
global_scope: Scope,
scopes: IndexMap<String, Scope>,
optional_dialog_ids: HashSet<Id>,
conditional_control_ids: HashSet<Id>,
named_ids: Vec<Id>,
}
/// Formats the info as C header file.
///
/// Useful if you want to preview the dialogs in Visual Studio.
pub struct ResourceInfoAsCHeaderCode<'a>(pub &'a ResourceInfo);
impl Display for ResourceInfoAsCHeaderCode<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for id in &self.0.named_ids {
writeln!(f, "#define {} {}", id.name, id.value)?;
}
Ok(())
}
}
/// Formats the header as Rust code.
///
/// Uses a similar format like bindgen because previously, bindgen was used to translate
/// the C header file to Rust.
pub struct ResourceInfoAsRustCode<'a>(pub &'a ResourceInfo);
impl Display for ResourceInfoAsRustCode<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// Write module opener
f.write_str("pub mod root {\n")?;
// Write scaling information
ScopeAsRustCode::new("GLOBAL", &self.0.global_scope).fmt(f)?;
for (key, scope) in self.0.scopes.iter() {
ScopeAsRustCode::new(key, scope).fmt(f)?;
}
// Write resource IDs
for id in &self.0.named_ids {
if self.0.optional_dialog_ids.contains(id)
|| self.0.conditional_control_ids.contains(id)
{
f.write_str(" #[allow(dead_code)]\n")?;
}
writeln!(f, " pub const {}: u32 = {};", id.name, id.value)?;
}
// Write module closer
f.write_str("}\n")?;
Ok(())
}
}
#[derive(Default)]
pub struct Resource {
pub dialogs: Vec<Dialog>,
}
impl Resource {
pub fn generate_info(&self, context: &Context) -> ResourceInfo {
ResourceInfo {
global_scope: context.global_scope,
scopes: context.scopes.clone(),
optional_dialog_ids: self.optional_dialog_ids().collect(),
conditional_control_ids: self.conditional_control_ids().collect(),
named_ids: self.named_ids().collect(),
}
}
fn named_ids(&self) -> impl Iterator<Item = Id> + '_ {
self.dialogs.iter().flat_map(|dialog| {
fn get_if_named(id: Id) -> Option<Id> {
if id.is_named() {
Some(id)
} else {
None
}
}
let named_dialog_id = get_if_named(dialog.id);
let named_control_ids = dialog
.controls
.iter()
.flat_map(|control| get_if_named(control.id));
named_dialog_id.into_iter().chain(named_control_ids)
})
}
fn optional_dialog_ids(&self) -> impl Iterator<Item = Id> + '_ {
self.dialogs.iter().filter(|d| d.optional).map(|d| d.id)
}
fn conditional_control_ids(&self) -> impl Iterator<Item = Id> + '_ {
self.dialogs.iter().flat_map(|dialog| {
dialog
.controls
.iter()
.filter(|control| !control.conditions.is_empty())
.map(|control| control.id)
})
}
}
impl Display for Resource {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for (i, dialog) in self.dialogs.iter().enumerate() {
dialog.fmt(f)?;
if i < self.dialogs.len() - 1 {
f.write_str("\n\n")?;
}
}
Ok(())
}
}
#[derive(Clone, Default)]
pub struct Dialog {
pub id: Id,
pub optional: bool,
pub rect: Rect,
pub kind: DialogKind,
pub styles: Styles,
pub ex_styles: Styles,
pub caption: Caption,
pub font: Option<Font>,
pub controls: Vec<Control>,
}
#[derive(Clone, Default)]
pub struct Styles(pub Vec<Style>);
impl Display for Styles {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for (i, style) in self.0.iter().enumerate() {
style.fmt(f)?;
if i < self.0.len() - 1 {
f.write_str(" | ")?;
}
}
Ok(())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct Id {
value: u32,
name: &'static str,
}
impl Id {
fn is_named(&self) -> bool {
!self.name.is_empty()
}
}
#[derive(Copy, Clone)]
pub struct DialogScaling {
pub x_scale: f64,
pub y_scale: f64,
pub width_scale: f64,
pub height_scale: f64,
}
struct DialogScalingAsRustCode<'a> {
attr: &'a str,
scope: &'a str,
scaling: &'a DialogScaling,
}
impl<'a> DialogScalingAsRustCode<'a> {
pub fn new(attr: &'a str, scope: &'a str, scaling: &'a DialogScaling) -> Self {
Self {
attr,
scope,
scaling,
}
}
}
impl Display for DialogScalingAsRustCode<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(
f,
" {}\n pub const {}_X_SCALE: f64 = {:.4};",
self.attr, self.scope, self.scaling.x_scale
)?;
writeln!(
f,
" {}\n pub const {}_Y_SCALE: f64 = {:.4};",
self.attr, self.scope, self.scaling.y_scale
)?;
writeln!(
f,
" {}\n pub const {}_WIDTH_SCALE: f64 = {:.4};",
self.attr, self.scope, self.scaling.width_scale
)?;
writeln!(
f,
" {}\n pub const {}_HEIGHT_SCALE: f64 = {:.4};",
self.attr, self.scope, self.scaling.height_scale
)?;
Ok(())
}
}
#[derive(Copy, Clone)]
pub struct ScopedContext<'a> {
pub(crate) context: &'a Context,
scope: Option<Scope>,
}
#[derive(Copy, Clone)]
pub struct Scope {
pub linux: OsSpecificSettings,
pub windows: OsSpecificSettings,
pub macos: OsSpecificSettings,
}
struct ScopeAsRustCode<'a> {
scope_name: &'a str,
scope: &'a Scope,
}
impl<'a> ScopeAsRustCode<'a> {
pub fn new(scope_name: &'a str, scope: &'a Scope) -> Self {
Self { scope_name, scope }
}
}
impl Display for ScopeAsRustCode<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut write_os = |os: &str, scaling: &DialogScaling| -> fmt::Result {
let attr = format!("#[cfg(target_os = {})]", Quoted(os));
DialogScalingAsRustCode::new(&attr, self.scope_name, scaling).fmt(f)?;
Ok(())
};
write_os("linux", &self.scope.linux.scaling)?;
write_os("windows", &self.scope.windows.scaling)?;
write_os("macos", &self.scope.macos.scaling)?;
Ok(())
}
}
impl Scope {
pub const fn settings_for_this_os(&self) -> &OsSpecificSettings {
#[cfg(target_os = "linux")]
{
&self.linux
}
#[cfg(target_os = "windows")]
{
&self.windows
}
#[cfg(target_os = "macos")]
{
&self.macos
}
}
}
#[derive(Copy, Clone)]
pub struct OsSpecificSettings {
pub scaling: DialogScaling,
}
pub fn rect(x: u32, y: u32, width: u32, height: u32) -> Rect {
Rect::new(x, y, width, height)
}
impl ScopedContext<'_> {
pub fn default_dialog(&self) -> Dialog {
self.context.default_dialog()
}
pub fn scale_width(&self, width: u32) -> u32 {
scale(self.scaling().width_scale, width)
}
pub fn scale_height(&self, height: u32) -> u32 {
scale(self.scaling().height_scale, height)
}
pub fn rect(&self, x: u32, y: u32, width: u32, height: u32) -> Rect {
self.rect_flexible(Rect::new(x, y, width, height))
}
pub fn rect_flexible(&self, rect: Rect) -> Rect {
let scaling = self.scaling();
Rect {
x: scale(scaling.x_scale, rect.x),
y: scale(scaling.y_scale, rect.y),
width: scale(scaling.width_scale, rect.width),
height: scale(scaling.height_scale, rect.height),
}
}
fn scaling(&self) -> DialogScaling {
self.scope
.as_ref()
.map(|s| s.settings_for_this_os().scaling)
.unwrap_or(self.context.global_scope.settings_for_this_os().scaling)
}
}
pub struct IdGenerator {
next_id_value: u32,
}
impl IdGenerator {
pub fn new(initial_id_value: u32) -> Self {
Self {
next_id_value: initial_id_value,
}
}
pub fn id(&mut self) -> Id {
Id {
value: self.next_id_value(),
name: "",
}
}
pub fn named_id(&mut self, name: &'static str) -> Id {
Id {
value: self.next_id_value(),
name,
}
}
fn next_id_value(&mut self) -> u32 {
let v = self.next_id_value;
self.next_id_value += 1;
v
}
}
pub struct Context {
pub default_dialog: Dialog,
pub global_scope: Scope,
// IndexMap instead of HashMap because we don't want the order to be always the same when
// writing the bindings file.
pub scopes: IndexMap<String, Scope>,
}
impl Context {
pub fn global(&self) -> ScopedContext {
ScopedContext {
context: self,
scope: None,
}
}
pub fn scoped<'a>(&'a self, scope: &'a str) -> ScopedContext<'a> {
let scope = *self.scopes.get(scope).expect("scope not found");
ScopedContext {
context: self,
scope: Some(scope),
}
}
pub fn default_dialog(&self) -> Dialog {
self.default_dialog.clone()
}
}
fn scale(scale: f64, value: u32) -> u32 {
(scale * value as f64).round() as _
}
impl Display for Id {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.name.is_empty() {
self.value.fmt(f)
} else {
self.name.fmt(f)
}
}
}
#[derive(Copy, Clone, derive_more::Display)]
pub enum DialogKind {
DIALOG,
DIALOGEX,
}
impl Default for DialogKind {
fn default() -> Self {
Self::DIALOG
}
}
#[derive(Clone, Default)]
pub struct Control {
pub id: Id,
/// Unlike in dialog, it's important to distinguish between Some and None because some
/// controls need an empty string.
pub caption: Option<Caption>,
pub kind: ControlKind,
pub sub_kind: Option<SubControlKind>,
pub rect: Rect,
pub styles: Styles,
pub conditions: HashSet<Condition>,
}
impl Add<Style> for Control {
type Output = Control;
fn add(mut self, rhs: Style) -> Self::Output {
self.styles.0.push(rhs);
self
}
}
impl Add<Condition> for Control {
type Output = Control;
fn add(mut self, rhs: Condition) -> Self::Output {
self.conditions.insert(rhs);
self
}
}
struct Quoted<D>(D);
impl<D: Display> Display for Quoted<D> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
struct LineBreaksEscaped<D>(D);
impl<D: Display> Display for LineBreaksEscaped<D> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0
.to_string()
.replace("\r\n", "\\r\\n")
.replace('\n', "\\r\\n")
.fmt(f)
}
}
fn opt<T: Display>(v: &Option<T>) -> Option<String> {
let v = v.as_ref()?;
Some(v.to_string())
}
fn req<T: Display>(v: T) -> Option<String> {
Some(v.to_string())
}
impl Display for Dialog {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(f, "{} {} {}", self.id, self.kind, self.rect)?;
if !self.styles.0.is_empty() {
writeln!(f, "STYLE {}", self.styles)?;
}
if !self.ex_styles.0.is_empty() {
writeln!(f, "EXSTYLE {}", self.ex_styles)?;
}
if !self.caption.is_empty() {
writeln!(f, "CAPTION {}", Quoted(self.caption))?;
}
if let Some(font) = self.font.as_ref() {
writeln!(f, "FONT {font}")?;
}
f.write_str("BEGIN\n")?;
if !self.controls.is_empty() {
for control in &self.controls {
#[cfg(target_os = "macos")]
if control.conditions.contains(&Condition::SkipOnMacOs) {
continue;
}
writeln!(f, " {control}")?;
}
}
f.write_str("END")?;
Ok(())
}
}
impl Display for Control {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let caption = opt(&self.caption.map(LineBreaksEscaped).map(Quoted));
let id = req(self.id);
let rect = req(self.rect);
let styles = if self.styles.0.is_empty() {
None
} else {
Some(self.styles.to_string())
};
let args = if self.kind == ControlKind::CONTROL {
vec![
caption,
id,
req(Quoted(self.sub_kind.unwrap())),
styles,
rect,
]
} else {
vec![caption, id, rect, styles]
};
let args: Vec<_> = args.into_iter().flatten().collect();
write!(f, "{} {}", self.kind, args.join(","))
}
}
#[derive(Copy, Clone, Eq, PartialEq, derive_more::Display)]
pub enum ControlKind {
LTEXT,
RTEXT,
COMBOBOX,
PUSHBUTTON,
CONTROL,
EDITTEXT,
GROUPBOX,
DEFPUSHBUTTON,
CTEXT,
}
impl Default for ControlKind {
fn default() -> Self {
Self::CTEXT
}
}
#[derive(Copy, Clone, derive_more::Display)]
pub enum SubControlKind {
Button,
Static,
msctls_trackbar32,
}
#[derive(Clone, Copy)]
pub struct Font {
pub name: &'static str,
pub size: u32,
}
impl Display for Font {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}", self.size, Quoted(self.name))
}
}
#[derive(Copy, Clone, Default)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Display for Rect {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}, {}, {}, {}", self.x, self.y, self.width, self.height)
}
}
impl Rect {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
pub fn pushbutton(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::PUSHBUTTON,
rect,
..Default::default()
}
}
pub fn groupbox(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::GROUPBOX,
rect,
..Default::default()
}
}
pub fn defpushbutton(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::DEFPUSHBUTTON,
rect,
..Default::default()
}
}
pub fn ltext(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::LTEXT,
rect: fix_text_rect(rect),
..Default::default()
}
}
pub fn rtext(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::RTEXT,
rect: fix_text_rect(rect),
..Default::default()
}
}
pub fn ctext(caption: Caption, id: Id, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::CTEXT,
rect: fix_text_rect(rect),
..Default::default()
}
}
pub fn combobox(id: Id, rect: Rect) -> Control {
Control {
id,
kind: ControlKind::COMBOBOX,
rect,
..Default::default()
}
}
pub fn edittext(id: Id, rect: Rect) -> Control {
Control {
id,
kind: ControlKind::EDITTEXT,
rect,
..Default::default()
}
}
pub fn control(caption: Caption, id: Id, sub_kind: SubControlKind, rect: Rect) -> Control {
Control {
id,
caption: Some(caption),
kind: ControlKind::CONTROL,
sub_kind: Some(sub_kind),
rect,
..Default::default()
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Condition {
/// Doesn't output the control in the RC file generated on macOS.
///
/// Still assigns an ID because it's better to keep the bindings file the same on every
/// platform.
SkipOnMacOs,
}
#[derive(Copy, Clone, derive_more::Display)]
pub enum Style {
DS_SETFONT,
DS_MODALFRAME,
DS_3DLOOK,
DS_FIXEDSYS,
DS_CENTER,
WS_POPUP,
WS_VISIBLE,
WS_CAPTION,
WS_SYSMENU,
WS_THICKFRAME,
WS_MAXIMIZEBOX,
DS_CONTROL,
WS_CHILD,
CBS_DROPDOWNLIST,
CBS_HASSTRINGS,
ES_MULTILINE,
ES_READONLY,
ES_WANTRETURN,
WS_VSCROLL,
WS_TABSTOP,
WS_GROUP,
WS_DISABLED,
BS_AUTOCHECKBOX,
BS_AUTORADIOBUTTON,
TBS_BOTH,
TBS_NOTICKS,
SS_ETCHEDHORZ,
SS_LEFTNOWORDWRAP,
ES_AUTOHSCROLL,
SS_CENTERIMAGE,
SS_WORDELLIPSIS,
// With negation
#[display(fmt = "NOT WS_TABSTOP")]
NOT_WS_TABSTOP,
#[display(fmt = "NOT WS_GROUP")]
NOT_WS_GROUP,
// Ex styles
WS_EX_TOPMOST,
WS_EX_WINDOWEDGE,
}
/// Makes sure the effective (already scaled) height of a text is not too low.
pub fn fix_text_rect(rect: Rect) -> Rect {
Rect {
height: rect.height.max(MIN_EFFECTIVE_TEXT_HEIGHT),
..rect
}
}
pub struct Adder(pub u32);
impl Adder {
pub fn space(&mut self, units: u32) -> u32 {
self.0 += units;
self.0
}
pub fn span(&mut self, units: u32) -> u32 {
self.0 += units;
units
}
pub fn get(&self) -> u32 {
self.0
}
}
impl From<Adder> for u32 {
fn from(v: Adder) -> Self {
v.0
}
}
// If lower than this, text will be cut off, especially the part below the baseline.
#[cfg(target_os = "windows")]
const MIN_EFFECTIVE_TEXT_HEIGHT: u32 = 8;
// If lower than this, radio buttons will be cut off.
#[cfg(target_os = "macos")]
const MIN_EFFECTIVE_TEXT_HEIGHT: u32 = 10;
#[cfg(target_os = "linux")]
const MIN_EFFECTIVE_TEXT_HEIGHT: u32 = 13;
@@ -0,0 +1,11 @@
use crate::base::*;
pub fn create(context: ScopedContext, ids: &mut IdGenerator) -> Dialog {
use Style::*;
Dialog {
id: ids.named_id("ID_COLOR_PANEL"),
rect: context.rect(0, 0, 250, 250),
styles: Styles(vec![DS_SETFONT, DS_CONTROL, WS_CHILD, WS_VISIBLE]),
..context.default_dialog()
}
}
@@ -0,0 +1,12 @@
// Attention: We can't calculate a constant main panel height at this point because different
// scaling factors will be applied to the header panel, depending on the operating system.
pub const MAIN_PANEL_WIDTH: u32 = 470;
pub const HEADER_PANEL_HEIGHT: u32 = 124;
pub const HEADER_PANEL_WIDTH: u32 = MAIN_PANEL_WIDTH;
// Need to leave some space for the scrollbar.
pub const MAPPING_ROW_PANEL_WIDTH: u32 = MAIN_PANEL_WIDTH - 10;
pub const MAPPING_ROW_PANEL_HEIGHT: u32 = 48;
pub const FOOTER_PANEL_HEIGHT: u32 = 43;
pub const MAPPING_ROW_COUNT: u32 = 5;
pub const MAPPING_ROWS_PANEL_WIDTH: u32 = MAIN_PANEL_WIDTH;
pub const MAPPING_ROWS_PANEL_HEIGHT: u32 = MAPPING_ROW_PANEL_HEIGHT * MAPPING_ROW_COUNT;
@@ -0,0 +1,22 @@
use crate::base::*;
pub fn create(context: ScopedContext, ids: &mut IdGenerator) -> Dialog {
use Style::*;
Dialog {
id: ids.named_id("ID_EMPTY_PANEL"),
optional: true,
caption: "Editor",
rect: context.rect(0, 0, 600, 250),
styles: Styles(vec![
// Places the window into the center by default
DS_CENTER,
// Displays a close button
WS_SYSMENU,
// Displays a maximize button
WS_MAXIMIZEBOX,
// Allows user to change size of window
WS_THICKFRAME,
]),
..context.default_dialog()
}
}
+43
View File
@@ -0,0 +1,43 @@
use crate::base::*;
impl ScopedContext<'_> {
pub fn checkbox(&self, caption: Caption, id: Id, rect: Rect) -> Control {
use Style::*;
// We want to completely ignore the given checkbox height, but we want it to scale.
let fixed_rect = self.rect_flexible(Rect { height: 8, ..rect });
control(
caption,
id,
SubControlKind::Button,
fix_text_rect(fixed_rect),
) + BS_AUTOCHECKBOX
}
}
pub fn ok_button(id: Id, rect: Rect) -> Control {
defpushbutton("OK", id, rect)
}
pub fn dropdown(id: Id, rect: Rect) -> Control {
use Style::*;
combobox(id, rect) + CBS_DROPDOWNLIST + CBS_HASSTRINGS
}
pub fn slider(id: Id, rect: Rect) -> Control {
use Style::*;
control("", id, SubControlKind::msctls_trackbar32, rect) + TBS_BOTH + TBS_NOTICKS
}
pub fn radio_button(caption: Caption, id: Id, rect: Rect) -> Control {
use Style::*;
control(caption, id, SubControlKind::Button, fix_text_rect(rect)) + BS_AUTORADIOBUTTON
}
pub fn divider(id: Id, rect: Rect) -> Control {
use Style::*;
control("", id, SubControlKind::Static, rect) + SS_ETCHEDHORZ
}
pub fn static_text(caption: Caption, id: Id, rect: Rect) -> Control {
control(caption, id, SubControlKind::Static, fix_text_rect(rect))
}
@@ -0,0 +1,27 @@
use crate::base::*;
use crate::ext::*;
pub fn create(context: ScopedContext, ids: &mut IdGenerator) -> Dialog {
use Style::*;
Dialog {
id: ids.named_id("ID_GROUP_PANEL"),
caption: "Edit group",
rect: context.rect(0, 0, 444, 74),
styles: Styles(vec![
DS_SETFONT,
DS_MODALFRAME,
DS_3DLOOK,
DS_FIXEDSYS,
DS_CENTER,
WS_POPUP,
WS_VISIBLE,
WS_CAPTION,
WS_SYSMENU,
]),
controls: vec![ok_button(
ids.named_id("ID_GROUP_PANEL_OK"),
context.rect(197, 53, 50, 14),
)],
..context.default_dialog()
}
}

Some files were not shown because too many files have changed in this diff Show More