Reorganize top-level directories with clearer naming convention
app -> app-desktop-macos, presets -> app-presets, server -> remote-server, daw-config-reaper -> osc-config-daw, plugin-reaper-realearn -> plugin-reaper-relearn. Updated run.py and presets.py path references accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"]
|
||||
@@ -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=
|
||||
@@ -0,0 +1,4 @@
|
||||
* text=auto
|
||||
|
||||
# REAPER projects always seem to use CRLF
|
||||
*.RPP text eol=crlf
|
||||
+3
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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'
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
target
|
||||
main/lib/WDL
|
||||
*.json
|
||||
*.lua
|
||||
+50
@@ -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}",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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!
|
||||
Let’s 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]
|
||||
@@ -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 `<category name="Extensions">` 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!
|
||||
Generated
+8962
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
@@ -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"]
|
||||
@@ -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>.
|
||||
@@ -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].
|
||||
@@ -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
@@ -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"
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
];
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
large-error-threshold = 200
|
||||
@@ -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"
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
use crate::base::*;
|
||||
use crate::constants::{HEADER_PANEL_HEIGHT, HEADER_PANEL_WIDTH};
|
||||
use crate::ext::*;
|
||||
|
||||
pub fn create(context: ScopedContext, ids: &mut IdGenerator) -> Dialog {
|
||||
use Style::*;
|
||||
let text_height = 9;
|
||||
let left_label_x = 7;
|
||||
let io_label_width = 24;
|
||||
let space = 3;
|
||||
let (io_combo_box_width, io_combo_box_height) = (190, 14);
|
||||
let mut row1 = Adder(left_label_x);
|
||||
let mut row2 = Adder(left_label_x);
|
||||
let upper_part_controls = [
|
||||
// Input/output
|
||||
ltext(
|
||||
"Input",
|
||||
ids.id(),
|
||||
context.rect(row1.get(), 5, row1.span(io_label_width), text_height),
|
||||
),
|
||||
pushbutton(
|
||||
"MIDI: <FX input>",
|
||||
ids.named_id("ID_CONTROL_INPUT_BUTTON"),
|
||||
context.rect(
|
||||
row1.space(space),
|
||||
3,
|
||||
row1.span(io_combo_box_width),
|
||||
io_combo_box_height,
|
||||
),
|
||||
) + NOT_WS_TABSTOP,
|
||||
ltext(
|
||||
"Output",
|
||||
ids.id(),
|
||||
context.rect(row2.get(), 25, row2.span(io_label_width), text_height),
|
||||
),
|
||||
pushbutton(
|
||||
"<None>",
|
||||
ids.named_id("ID_FEEDBACK_OUTPUT_BUTTON"),
|
||||
context.rect(
|
||||
row2.space(space),
|
||||
23,
|
||||
row2.span(io_combo_box_width),
|
||||
io_combo_box_height,
|
||||
),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Quick actions
|
||||
pushbutton(
|
||||
"Menu",
|
||||
ids.named_id("ID_MENU_BUTTON"),
|
||||
context.rect(row1.space(space), 3, row1.span(27), 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"Import from clipboard",
|
||||
ids.named_id("ID_IMPORT_BUTTON"),
|
||||
context.rect(row1.space(space), 3, row1.span(73), 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"Export to clipboard",
|
||||
ids.named_id("ID_EXPORT_BUTTON"),
|
||||
context.rect(row1.space(space), 3, row1.span(67), 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
pushbutton(
|
||||
"Projection",
|
||||
ids.named_id("ID_PROJECTION_BUTTON"),
|
||||
context.rect(row1.space(space), 3, row1.span(42), 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
pushbutton(
|
||||
"?",
|
||||
ids.named_id("ID_MAIN_HELP_BUTTON"),
|
||||
context.rect(row1.space(space), 3, row1.span(14), 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Event filter
|
||||
ltext(
|
||||
"Let MIDI through:",
|
||||
ids.named_id("ID_LET_THROUGH_LABEL_TEXT"),
|
||||
context.rect(257, 25, 55, 9),
|
||||
),
|
||||
context.checkbox(
|
||||
"Matched events",
|
||||
ids.named_id("ID_LET_MATCHED_EVENTS_THROUGH_CHECK_BOX"),
|
||||
rect(319, 25, 67, 8),
|
||||
) + WS_TABSTOP,
|
||||
context.checkbox(
|
||||
"Unmatched events",
|
||||
ids.named_id("ID_LET_UNMATCHED_EVENTS_THROUGH_CHECK_BOX"),
|
||||
rect(392, 25, 76, 8),
|
||||
) + WS_TABSTOP,
|
||||
];
|
||||
let show_controls = [
|
||||
ltext(
|
||||
"Show",
|
||||
ids.named_id("ID_HEADER_PANEL_SHOW_LABEL_TEXT"),
|
||||
context.rect(7, 47, 24, 9),
|
||||
),
|
||||
radio_button(
|
||||
"Controller compartment (for describing your device, optional)",
|
||||
ids.named_id("ID_CONTROLLER_COMPARTMENT_RADIO_BUTTON"),
|
||||
context.rect(50, 47, 200, 8),
|
||||
) + WS_TABSTOP,
|
||||
radio_button(
|
||||
"Main compartment (for defining what it should do)",
|
||||
ids.named_id("ID_MAIN_COMPARTMENT_RADIO_BUTTON"),
|
||||
context.rect(269, 47, 200, 8),
|
||||
) + WS_TABSTOP,
|
||||
];
|
||||
let lower_part_controls = [
|
||||
// Preset
|
||||
ltext(
|
||||
"Controller preset",
|
||||
ids.named_id("ID_PRESET_LABEL_TEXT"),
|
||||
context.rect(7, 69, 57, 9),
|
||||
),
|
||||
pushbutton(
|
||||
"<None>",
|
||||
ids.named_id("ID_PRESET_BROWSE_BUTTON"),
|
||||
context.rect(68, 66, 135, 14),
|
||||
) + WS_GROUP,
|
||||
// Preset actions
|
||||
pushbutton(
|
||||
"Save",
|
||||
ids.named_id("ID_PRESET_SAVE_BUTTON"),
|
||||
context.rect(207, 66, 26, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
pushbutton(
|
||||
"Save as...",
|
||||
ids.named_id("ID_PRESET_SAVE_AS_BUTTON"),
|
||||
context.rect(235, 66, 42, 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"Delete",
|
||||
ids.named_id("ID_PRESET_DELETE_BUTTON"),
|
||||
context.rect(279, 66, 28, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Auto-load
|
||||
ltext(
|
||||
"Auto-load",
|
||||
ids.named_id("ID_AUTO_LOAD_LABEL_TEXT"),
|
||||
context.rect(319, 69, 33, 9),
|
||||
) + NOT_WS_GROUP,
|
||||
dropdown(
|
||||
ids.named_id("ID_AUTO_LOAD_COMBO_BOX"),
|
||||
context.rect(356, 67, 107, 16),
|
||||
) + WS_VSCROLL
|
||||
+ WS_GROUP
|
||||
+ WS_TABSTOP,
|
||||
// Mapping group
|
||||
ltext("Mapping group", ids.id(), context.rect(7, 89, 55, 9)) + NOT_WS_GROUP,
|
||||
dropdown(
|
||||
ids.named_id("ID_GROUP_COMBO_BOX"),
|
||||
context.rect(68, 87, 135, 16),
|
||||
) + WS_VSCROLL
|
||||
+ WS_TABSTOP,
|
||||
// Mapping group actions
|
||||
pushbutton(
|
||||
"Add",
|
||||
ids.named_id("ID_GROUP_ADD_BUTTON"),
|
||||
context.rect(207, 86, 26, 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"Remove",
|
||||
ids.named_id("ID_GROUP_DELETE_BUTTON"),
|
||||
context.rect(235, 86, 42, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
pushbutton(
|
||||
"Edit",
|
||||
ids.named_id("ID_GROUP_EDIT_BUTTON"),
|
||||
context.rect(279, 86, 28, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
pushbutton(
|
||||
"Notes",
|
||||
ids.named_id("ID_NOTES_BUTTON"),
|
||||
context.rect(426, 86, 35, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Mapping list actions
|
||||
ltext("Mappings", ids.id(), context.rect(7, 109, 33, 9)) + NOT_WS_GROUP,
|
||||
pushbutton(
|
||||
"Add one",
|
||||
ids.named_id("ID_ADD_MAPPING_BUTTON"),
|
||||
context.rect(42, 106, 41, 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"Learn many",
|
||||
ids.named_id("ID_LEARN_MANY_MAPPINGS_BUTTON"),
|
||||
context.rect(86, 106, 47, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Search
|
||||
ltext("Search", ids.id(), context.rect(139, 109, 25, 9)) + NOT_WS_GROUP,
|
||||
edittext(
|
||||
ids.named_id("ID_HEADER_SEARCH_EDIT_CONTROL"),
|
||||
context.rect(165, 106, 157, 14),
|
||||
) + ES_AUTOHSCROLL,
|
||||
pushbutton(
|
||||
"X",
|
||||
ids.named_id("ID_CLEAR_SEARCH_BUTTON"),
|
||||
context.rect(323, 106, 11, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Source filter
|
||||
pushbutton(
|
||||
"Filter source",
|
||||
ids.named_id("ID_FILTER_BY_SOURCE_BUTTON"),
|
||||
context.rect(340, 106, 48, 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"X",
|
||||
ids.named_id("ID_CLEAR_SOURCE_FILTER_BUTTON"),
|
||||
context.rect(389, 106, 11, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
// Target filter
|
||||
pushbutton(
|
||||
"Filter target",
|
||||
ids.named_id("ID_FILTER_BY_TARGET_BUTTON"),
|
||||
context.rect(404, 106, 45, 14),
|
||||
) + WS_GROUP,
|
||||
pushbutton(
|
||||
"X",
|
||||
ids.named_id("ID_CLEAR_TARGET_FILTER_BUTTON"),
|
||||
context.rect(450, 106, 11, 14),
|
||||
) + NOT_WS_TABSTOP,
|
||||
];
|
||||
let divider_controls = [
|
||||
divider(ids.id(), context.rect(0, 41, HEADER_PANEL_WIDTH, 1)),
|
||||
divider(ids.id(), context.rect(0, 62, HEADER_PANEL_WIDTH, 1)),
|
||||
// divider(ids.id(), context.rect(0, 123, HEADER_PANEL_WIDTH, 1)),
|
||||
];
|
||||
Dialog {
|
||||
id: ids.named_id("ID_HEADER_PANEL"),
|
||||
kind: DialogKind::DIALOGEX,
|
||||
rect: context.rect(0, 0, HEADER_PANEL_WIDTH, HEADER_PANEL_HEIGHT),
|
||||
styles: Styles(vec![DS_SETFONT, DS_CONTROL, WS_CHILD, WS_VISIBLE]),
|
||||
controls: upper_part_controls
|
||||
.into_iter()
|
||||
.chain(show_controls)
|
||||
.chain(lower_part_controls)
|
||||
.chain(divider_controls)
|
||||
.collect(),
|
||||
..context.default_dialog()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user