diff --git a/.github/workflows/ci-debug.yml b/.github/workflows/ci-debug.yml
new file mode 100644
index 00000000..12e4f318
--- /dev/null
+++ b/.github/workflows/ci-debug.yml
@@ -0,0 +1,126 @@
+#===========================================================================================
+# This workflow will build the project for Windows, macOS, and Linux.
+# It will also build an Arch Linux and Fedora package using Docker containers.
+# The artifacts will be uploaded to the release page.
+#
+# TODO:
+# - Add a step after release to update the AUR repository
+#===========================================================================================
+
+name: Build Debug
+on:
+ workflow_dispatch:
+defaults:
+ run:
+ shell: bash
+
+#===========================================================================================
+# Environment Variables
+env:
+ PROJECT_NAME: Disflux
+ BUNDLE_NAME: disflux
+ BUNDLE_ID: com.dimethoxy.disflux
+ BUILD_DIR: build
+ DISPLAY: :0 # Linux pluginval needs this
+ HOMEBREW_NO_INSTALL_CLEANUP: 1
+ IPP_DIR: C:\Program Files (x86)\Intel\oneAPI\ipp\latest\lib\cmake\ipp
+
+#===========================================================================================
+# Build Jobs
+#===========================================================================================
+jobs:
+ #=========================================================================================
+ # Windows Build
+ #=========================================================================================
+ build-windows:
+ runs-on: windows-latest
+ steps:
+ - name: Export Variables
+ run: |
+ echo "BUILD_ARTIFACTS_DIR=$BUILD_DIR/src/${{env.PROJECT_NAME}}Plugin_artefacts/Debug" >> $GITHUB_ENV
+ echo "WINDOWS-PACKAGE=${{env.BUNDLE_NAME}}-windows-debug.exe" >> $GITHUB_ENV
+
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
+ - name: Setup MSVC
+ uses: TheMrMilchmann/setup-msvc-dev@v3
+ with:
+ arch: x64
+ spectre: true
+
+ - name: Set Up Windows
+ run: |
+ choco install ninja
+ choco install innosetup
+
+ - name: Cache IPP (Windows)
+ id: cache-ipp
+ uses: actions/cache@v4
+ with:
+ key: ipp-2026-0-0-193
+ path: C:\Program Files (x86)\Intel
+
+ - name: Install IPP (Windows)
+ if: steps.cache-ipp.outputs.cache-hit != 'true'
+ run: |
+ curl --output intel-oneapi-toolkit-2026.0.0.193_offline.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/bae85ab1-cfcd-4251-8d42-a0c27949ea33/intel-oneapi-toolkit-2026.0.0.193_offline.exe
+ ./intel-oneapi-toolkit-2026.0.0.193_offline.exe -s -a --silent --eula accept -p=NEED_VS2022_INTEGRATION=1
+
+ - name: Save IPP cache
+ if: steps.cache-ipp.outputs.cache-hit != 'true'
+ uses: actions/cache/save@v4
+ with:
+ path: C:\Program Files (x86)\Intel
+ key: ipp-2026-0-0-193
+
+ - name: Select CMake Preset
+ run: |
+ cmake --preset "Windows Debug"
+
+ - name: Build
+ run: cmake --build build --config "Debug"
+
+ - name: Copy Build Artifacts
+ run: |
+ mkdir -p artifacts
+ cp -r ${{env.BUILD_ARTIFACTS_DIR}}/VST3/${{env.PROJECT_NAME}}.vst3 \
+ artifacts/
+ cp -r ${{env.BUILD_ARTIFACTS_DIR}}/CLAP/${{env.PROJECT_NAME}}.clap \
+ artifacts/
+ shell: bash
+
+ - name: Package Debug Symbols (Windows)
+ run: |
+ mkdir -p artifacts/debug-symbols
+ find ${{env.BUILD_ARTIFACTS_DIR}} -name "*.pdb" -type f | while read pdb; do
+ cp "$pdb" artifacts/debug-symbols/
+ done
+ if [ -f artifacts/debug-symbols/*.pdb ]; then
+ cd artifacts
+ zip -r ${{env.BUNDLE_NAME}}-windows-debug-symbols.zip debug-symbols/
+ fi
+ shell: bash
+
+ - name: Create Installer
+ run: |
+ # Create the packaging directory
+ mkdir -p packaging
+
+ # Create the Inno Setup
+ envsubst < pkg/windows/Setup.iss > packaging/Setup.iss
+ iscc packaging/Setup.iss
+
+ # Move the installer to the artifacts directory
+ mv packaging/Output/"${{env.PROJECT_NAME}}_Setup.exe" artifacts/${{env.WINDOWS-PACKAGE}}
+ shell: bash
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts-windows-debug
+ path: artifacts
diff --git a/.github/workflows/ci-development.yml b/.github/workflows/ci-development.yml
deleted file mode 100644
index 030a9ce8..00000000
--- a/.github/workflows/ci-development.yml
+++ /dev/null
@@ -1,423 +0,0 @@
-#===========================================================================================
-# This workflow will build the project for Windows, macOS, and Linux.
-# It will also build an Arch Linux and Fedora package using Docker containers.
-# The artifacts will be uploaded to the release page.
-#
-# TODO:
-# - Add a step after release to update the AUR repository
-#===========================================================================================
-
-name: Build Development
-on:
- workflow_dispatch:
-defaults:
- run:
- shell: bash
-
-#===========================================================================================
-# Environment Variables
-env:
- PROJECT_NAME: Disflux
- BUNDLE_NAME: disflux
- BUNDLE_ID: com.dimethoxy.disflux
- VERSION: 1.1.2 # Disflux-Version
- BUILD_DIR: build
- DISPLAY: :0 # Linux pluginval needs this
- HOMEBREW_NO_INSTALL_CLEANUP: 1
- IPP_DIR: C:\Program Files (x86)\Intel\oneAPI\ipp\latest\lib\cmake\ipp
-
-#===========================================================================================
-# Build Jobs
-#===========================================================================================
-jobs:
- #=========================================================================================
- # Windows Build
- #=========================================================================================
- build-windows:
- runs-on: windows-latest
- steps:
- - name: Export Variables
- run: |
- echo "BUILD_ARTIFACTS_DIR=$BUILD_DIR/src/${{env.PROJECT_NAME}}Plugin_artefacts/Release" >> $GITHUB_ENV
- echo "WINDOWS-PACKAGE=${{env.BUNDLE_NAME}}-windows.exe" >> $GITHUB_ENV
-
- - name: Checkout Code
- uses: actions/checkout@v4
- with:
- ref: development
-
- - name: Setup MSVC
- uses: TheMrMilchmann/setup-msvc-dev@v3
- with:
- arch: x64
- spectre: true
-
- - name: Set Up Windows
- run: |
- choco install ninja
- choco install innosetup
-
- - name: Cache IPP (Windows)
- id: cache-ipp
- uses: actions/cache@v4
- with:
- key: ipp-v6
- path: C:\Program Files (x86)\Intel
-
- - name: Install IPP (Windows)
- if: steps.cache-ipp.outputs.cache-hit != 'true'
- run: |
- curl --output oneapi.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/2e89fab4-e1c7-4f14-a1ef-6cddba8c5fa7/intel-ipp-2022.0.0.796_offline.exe
- ./oneapi.exe -s -x -f oneapi
- ./oneapi/bootstrapper.exe -s -c --action install --components=intel.oneapi.win.ipp.devel --eula=accept -p=NEED_VS2022_INTEGRATION=1 --log-dir=.
-
- - name: Save IPP cache
- if: steps.cache-ipp.outputs.cache-hit != 'true'
- uses: actions/cache/save@v4
- with:
- path: C:\Program Files (x86)\Intel
- key: ipp-v6
-
- - name: Select CMake Preset
- run: |
- cmake --preset "Windows Release"
-
- - name: Build
- run: cmake --build build --config "Release"
-
- - name: Copy Build Artifacts
- run: |
- mkdir -p artifacts
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/VST3/${{env.PROJECT_NAME}}.vst3 \
- artifacts/
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/CLAP/${{env.PROJECT_NAME}}.clap \
- artifacts/
- shell: bash
-
- - name: Create Installer
- run: |
- # Create the packaging directory
- mkdir -p packaging
-
- # Create the Inno Setup
- envsubst < pkg/windows/Setup.iss > packaging/Setup.iss
- iscc packaging/Setup.iss
-
- # Move the installer to the artifacts directory
- mv packaging/Output/"${{env.PROJECT_NAME}}_Setup.exe" artifacts/${{env.WINDOWS-PACKAGE}}
- shell: bash
-
- - name: Upload Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: artifacts-windows
- path: artifacts
-
- #=========================================================================================
- # macOS Build
- #=========================================================================================
- build-macos:
- runs-on: macos-latest
- steps:
- - name: Export Variables
- run: |
- echo "BUILD_ARTIFACTS_DIR=$BUILD_DIR/src/${{env.PROJECT_NAME}}Plugin_artefacts/Release" >> $GITHUB_ENV
- echo "MAC_PACKAGE=${{env.BUNDLE_NAME}}-macos.pkg" >> $GITHUB_ENV
-
- - name: Checkout Code
- uses: actions/checkout@v4
- with:
- ref: development
-
- - name: Set Up Mac
- run: brew install ninja osxutils
-
- - name: Select CMake Preset
- run: |
- cmake --preset "Mac Release" \
- -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
-
- - name: Build
- run: cmake --build build --config "Release"
-
- - name: Copy Build Artifacts
- run: |
- mkdir -p artifacts
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/VST3/${{env.PROJECT_NAME}}.vst3 \
- artifacts/
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/CLAP/${{env.PROJECT_NAME}}.clap \
- artifacts/
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/AU/${{env.PROJECT_NAME}}.component \
- artifacts/
- shell: bash
-
- - name: Import Certificates
- uses: sudara/basic-macos-keychain-action@v1
- id: keychain
- with:
- dev-id-app-cert: ${{ secrets.DEV_ID_APP_CERT }}
- dev-id-app-password: ${{ secrets.DEV_ID_PASSWORD }}
- dev-id-installer-cert: ${{ secrets.DEV_ID_INSTALL_CERT }}
- dev-id-installer-password: ${{ secrets.DEV_ID_PASSWORD }}
-
- - name: Codesign (macOS)
- run: |
- # Set variables
- VST3_BIN="artifacts/${{env.PROJECT_NAME}}.vst3/Contents/MacOS/${{env.PROJECT_NAME}}"
- CLAP_BIN="artifacts/${{env.PROJECT_NAME}}.clap/Contents/MacOS/${{env.PROJECT_NAME}}"
- AU_BIN="artifacts/${{env.PROJECT_NAME}}.component/Contents/MacOS/${{env.PROJECT_NAME}}"
-
- # Codesign the binaries
- codesign \
- --force -s "${{secrets.DEVELOPER_ID_APPLICATION}}" \
- -v "$VST3_BIN" \
- --deep --strict --options=runtime --timestamp
- codesign \
- --force -s "${{secrets.DEVELOPER_ID_APPLICATION}}" \
- -v "$CLAP_BIN" \
- --deep --strict --options=runtime --timestamp
- codesign \
- --force -s "${{secrets.DEVELOPER_ID_APPLICATION}}" \
- -v "$AU_BIN" \
- --deep --strict --options=runtime --timestamp
- shell: bash
-
- - name: Create Installer
- run: |
- # Set variables
- VST3_PATH="artifacts/${{env.PROJECT_NAME}}.vst3"
- CLAP_PATH="artifacts/${{env.PROJECT_NAME}}.clap"
- AU_PATH="artifacts/${{env.PROJECT_NAME}}.component"
- PACKAGE_NAME="${{env.PROJECT_NAME}}-macos.pkg"
-
- # Create the packaging directory
- mkdir -p packaging
-
- # Create the distribution file
- envsubst < pkg/mac/distribution.xml > packaging/distribution.xml
-
- # Create the VST subpackage
- pkgbuild \
- --identifier "${{env.BUNDLE_ID}}.vst3.pkg" \
- --version "$VERSION" \
- --component "$VST3_PATH" \
- --install-location "/Library/Audio/Plug-Ins/VST3" \
- "packaging/${{env.PROJECT_NAME}}.vst3.pkg"
-
- # Create the CLAP subpackage
- pkgbuild \
- --identifier "${{env.BUNDLE_ID}}.clap.pkg" \
- --version "$VERSION" \
- --component "$CLAP_PATH" \
- --install-location "/Library/Audio/Plug-Ins/CLAP" \
- "packaging/${{env.PROJECT_NAME}}.clap.pkg"
-
- # Create the AU subpackage
- pkgbuild \
- --identifier "${{env.BUNDLE_ID}}.au.pkg" \
- --version "$VERSION" \
- --component "$AU_PATH" \
- --install-location "/Library/Audio/Plug-Ins/Components" \
- "packaging/${{env.PROJECT_NAME}}.au.pkg"
-
- # Create the main package
- cd packaging
- productbuild \
- --resources ./resources \
- --distribution distribution.xml \
- --sign "${{secrets.DEVELOPER_ID_INSTALLER}}" \
- --timestamp "${{env.PROJECT_NAME}}.pkg"
-
- # Notarize the package
- xcrun notarytool submit "${{env.PROJECT_NAME}}.pkg" \
- --apple-id ${{secrets.NOTARIZATION_USERNAME}} \
- --password ${{secrets.NOTARIZATION_PASSWORD}} \
- --team-id ${{secrets.TEAM_ID}} \
- --wait
-
- # Staple the package
- xcrun stapler staple "${{env.PROJECT_NAME}}.pkg"
-
- # Move the package to the artifacts directory
- mv ${{env.PROJECT_NAME}}.pkg ../artifacts/${{env.MAC_PACKAGE}}
- shell: bash
-
- - name: Upload Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: artifacts-macos
- path: artifacts
-
- #=========================================================================================
- # Ubuntu Linux Build
- #=========================================================================================
- build-ubuntu:
- runs-on: ubuntu-latest
- steps:
- - name: Export Variables
- run: |
- echo "BUILD_ARTIFACTS_DIR=$BUILD_DIR/src/${{env.PROJECT_NAME}}Plugin_artefacts/Release" >> $GITHUB_ENV
- echo "UBUNTU_PACKAGE=${{env.BUNDLE_NAME}}-linux-ubuntu.deb" >> $GITHUB_ENV
- echo "LINUX_PACKAGE=${{env.BUNDLE_NAME}}-linux-vanilla.zip" >> $GITHUB_ENV
-
- - name: Checkout Code
- uses: actions/checkout@v4
- with:
- ref: development
-
- - name: Set Up Linux
- run: |
- sudo apt-get update
- sudo apt-get install ninja-build
- sudo apt install libasound2-dev \
- libjack-jackd2-dev \
- ladspa-sdk \
- libcurl4-openssl-dev \
- libfreetype-dev \
- libfontconfig1-dev \
- libx11-dev \
- libxcomposite-dev \
- libxcursor-dev \
- libxext-dev \
- libxinerama-dev \
- libxrandr-dev \
- libxrender-dev \
- libwebkit2gtk-4.1-dev \
- libglu1-mesa-dev mesa-common-dev
- sudo apt install curl
- sudo apt-get install -y dpkg-dev devscripts
- sudo /usr/bin/Xvfb $DISPLAY &
- shell: bash
-
- - name: Select CMake Preset
- run: |
- cmake --preset "Linux Release"
-
- - name: Build
- run: cmake --build build --config "Release"
-
- - name: Copy Artifacts
- run: |
- mkdir -p artifacts
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/VST3/${{env.PROJECT_NAME}}.vst3 \
- artifacts/
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/CLAP/${{env.PROJECT_NAME}}.clap \
- artifacts/
- cp -r ${{env.BUILD_ARTIFACTS_DIR}}/LV2/${{env.PROJECT_NAME}}.lv2 \
- artifacts/
- shell: bash
-
- - name: Package Linux Artifacts
- run: |
- # Set variables
- UBUNTU_PACKAGE_DIR="${{env.BUNDLE_NAME}}-linux-ubuntu"
- UBUNTU_CONTROL_FILE="pkg/ubuntu/control"
- VST3_INSTALL_DIR="/usr/lib/vst3/${{env.PROJECT_NAME}}"
- CLAP_INSTALL_DIR="/usr/lib/clap/${{env.PROJECT_NAME}}"
- LV2_INSTALL_DIR="/usr/lib/lv2/${{env.PROJECT_NAME}}"
-
- # Create Vanilla package directory
- mkdir -p vanilla
- cp -r artifacts/${{env.PROJECT_NAME}}.vst3 vanilla/
- cp -r artifacts/${{env.PROJECT_NAME}}.clap vanilla/
- cp -r artifacts/${{env.PROJECT_NAME}}.lv2 vanilla/
-
- # Zip and move the package to the artifacts directory
- zip -r ${{env.LINUX_PACKAGE}} vanilla/*
- mv ${{env.LINUX_PACKAGE}} artifacts/$LINUX_PACKAGE
-
- # Create Debian package directory
- mkdir -p $UBUNTU_PACKAGE_DIR/DEBIAN
- mkdir -p $UBUNTU_PACKAGE_DIR/$VST3_INSTALL_DIR
- mkdir -p $UBUNTU_PACKAGE_DIR/$CLAP_INSTALL_DIR
- mkdir -p $UBUNTU_PACKAGE_DIR/$LV2_INSTALL_DIR
-
- # Copy files to Debian package directory
- cp -r artifacts/${{env.PROJECT_NAME}}.vst3 \
- $UBUNTU_PACKAGE_DIR/$VST3_INSTALL_DIR/
- cp -r artifacts/${{env.PROJECT_NAME}}.clap \
- $UBUNTU_PACKAGE_DIR/$CLAP_INSTALL_DIR/
- cp -r artifacts/${{env.PROJECT_NAME}}.lv2 \
- $UBUNTU_PACKAGE_DIR/$LV2_INSTALL_DIR/
-
- # Set PACKAGE_NAME for envsubst
- export PACKAGE_NAME=${{env.BUNDLE_NAME}}
- export PACKAGE_VERSION=${{env.VERSION}}
-
- # Replace PACKAGE_NAME in control file and copy to Debian package directory
- envsubst < $UBUNTU_CONTROL_FILE > $UBUNTU_PACKAGE_DIR/DEBIAN/control
-
- # Build Debian package
- dpkg-deb --build $UBUNTU_PACKAGE_DIR
-
- # Move the package to the artifacts directory
- cp -r *.deb artifacts/${{env.UBUNTU_PACKAGE}}
- shell: bash
-
- - name: Upload Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: artifacts-ubuntu
- path: artifacts
-
- #=========================================================================================
- # Arch Linux Build
- #=========================================================================================
- build-archlinux:
- runs-on: ubuntu-latest
- container:
- image: archlinux:latest
- steps:
- - name: Export Variables
- run: |
- echo "BUILD_ARTIFACTS_DIR=$BUILD_DIR/src/${{env.PROJECT_NAME}}Plugin_artefacts/Release" >> $GITHUB_ENV
- echo "PACKAGE_NAME=${{env.BUNDLE_NAME}}-linux-arch.pkg.tar.zst" >> $GITHUB_ENV
-
- - name: Install Base Packages
- run: |
- pacman -Sy --noconfirm base-devel git
-
- - name: Checkout Repository
- uses: actions/checkout@v4
- with:
- ref: development
-
- - name: Create user for build process
- run: |
- useradd -m dimethoxy
- sudo su - dimethoxy
-
- - name: Set permissions for the build directory
- run: |
- sudo chown -R dimethoxy:dimethoxy /__w/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}/pkg/arch/release
- sudo chmod -R u+rw /__w/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}/pkg/arch/release
-
- - name: Disable sudo password prompt
- run: |
- echo 'dimethoxy ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /etc/sudoers
-
- - name: Build Package
- run: |
- sudo -u dimethoxy bash -c 'cd pkg/arch/release && makepkg -s --noconfirm'
-
- - name: Package Artifacts
- run: |
- # Remove the default package
- rm pkg/arch/release/*debug*.pkg.tar.zst
-
- # Move the package to the root directory
- mv pkg/arch/release/*.pkg.tar.zst .
-
- # Rename the package
- mv *.pkg.tar.zst ${{env.PACKAGE_NAME}}
-
- # Move the package to the artifacts directory
- mkdir -p artifacts
- mv ${{env.PACKAGE_NAME}} artifacts/
- shell: bash
-
- - name: Upload Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: artifacts-archlinux
- path: artifacts
diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml
index 767aa31e..6595af9f 100644
--- a/.github/workflows/ci-release.yml
+++ b/.github/workflows/ci-release.yml
@@ -20,7 +20,6 @@ env:
PROJECT_NAME: Disflux
BUNDLE_NAME: disflux
BUNDLE_ID: com.dimethoxy.disflux
- VERSION: 1.1.2 # Disflux-Version
BUILD_DIR: build
DISPLAY: :0 # Linux pluginval needs this
HOMEBREW_NO_INSTALL_CLEANUP: 1
@@ -44,6 +43,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Setup MSVC
uses: TheMrMilchmann/setup-msvc-dev@v3
with:
@@ -59,22 +62,21 @@ jobs:
id: cache-ipp
uses: actions/cache@v4
with:
- key: ipp-v6
+ key: ipp-2026-0-0-193
path: C:\Program Files (x86)\Intel
- name: Install IPP (Windows)
if: steps.cache-ipp.outputs.cache-hit != 'true'
run: |
- curl --output oneapi.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/2e89fab4-e1c7-4f14-a1ef-6cddba8c5fa7/intel-ipp-2022.0.0.796_offline.exe
- ./oneapi.exe -s -x -f oneapi
- ./oneapi/bootstrapper.exe -s -c --action install --components=intel.oneapi.win.ipp.devel --eula=accept -p=NEED_VS2022_INTEGRATION=1 --log-dir=.
+ curl --output intel-oneapi-toolkit-2026.0.0.193_offline.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/bae85ab1-cfcd-4251-8d42-a0c27949ea33/intel-oneapi-toolkit-2026.0.0.193_offline.exe
+ ./intel-oneapi-toolkit-2026.0.0.193_offline.exe -s -a --silent --eula accept -p=NEED_VS2022_INTEGRATION=1
- name: Save IPP cache
if: steps.cache-ipp.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: C:\Program Files (x86)\Intel
- key: ipp-v6
+ key: ipp-2026-0-0-193
- name: Select CMake Preset
run: |
@@ -125,6 +127,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Set Up Mac
run: brew install ninja osxutils
@@ -259,6 +265,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Set Up Linux
run: |
sudo apt-get update
@@ -336,7 +346,7 @@ jobs:
# Set PACKAGE_NAME for envsubst
export PACKAGE_NAME=${{env.BUNDLE_NAME}}
- export PACKAGE_VERSION=${{env.VERSION}}
+ export PACKAGE_VERSION=$VERSION
# Replace PACKAGE_NAME in control file and copy to Debian package directory
envsubst < $UBUNTU_CONTROL_FILE > $UBUNTU_PACKAGE_DIR/DEBIAN/control
@@ -374,6 +384,10 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Create user for build process
run: |
useradd -m dimethoxy
diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml
index e5626a4c..a9151386 100644
--- a/.github/workflows/ci-snapshot.yml
+++ b/.github/workflows/ci-snapshot.yml
@@ -11,7 +11,11 @@ name: Build Snapshot
on:
workflow_dispatch:
pull_request:
+ branches:
+ - main
push:
+ branches:
+ - main
defaults:
run:
shell: bash
@@ -22,7 +26,6 @@ env:
PROJECT_NAME: Disflux
BUNDLE_NAME: disflux
BUNDLE_ID: com.dimethoxy.disflux
- VERSION: 1.1.2 # Disflux-Version
BUILD_DIR: build
DISPLAY: :0 # Linux pluginval needs this
HOMEBREW_NO_INSTALL_CLEANUP: 1
@@ -46,6 +49,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Setup MSVC
uses: TheMrMilchmann/setup-msvc-dev@v3
with:
@@ -61,22 +68,21 @@ jobs:
id: cache-ipp
uses: actions/cache@v4
with:
- key: ipp-v6
+ key: ipp-2026-0-0-193
path: C:\Program Files (x86)\Intel
- name: Install IPP (Windows)
if: steps.cache-ipp.outputs.cache-hit != 'true'
run: |
- curl --output oneapi.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/2e89fab4-e1c7-4f14-a1ef-6cddba8c5fa7/intel-ipp-2022.0.0.796_offline.exe
- ./oneapi.exe -s -x -f oneapi
- ./oneapi/bootstrapper.exe -s -c --action install --components=intel.oneapi.win.ipp.devel --eula=accept -p=NEED_VS2022_INTEGRATION=1 --log-dir=.
+ curl --output intel-oneapi-toolkit-2026.0.0.193_offline.exe https://registrationcenter-download.intel.com/akdlm/IRC_NAS/bae85ab1-cfcd-4251-8d42-a0c27949ea33/intel-oneapi-toolkit-2026.0.0.193_offline.exe
+ ./intel-oneapi-toolkit-2026.0.0.193_offline.exe -s -a --silent --eula accept -p=NEED_VS2022_INTEGRATION=1
- name: Save IPP cache
if: steps.cache-ipp.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: C:\Program Files (x86)\Intel
- key: ipp-v6
+ key: ipp-2026-0-0-193
- name: Select CMake Preset
run: |
@@ -127,6 +133,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Set Up Mac
run: brew install ninja osxutils
@@ -261,6 +271,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Set Up Linux
run: |
sudo apt-get update
@@ -338,7 +352,7 @@ jobs:
# Set PACKAGE_NAME for envsubst
export PACKAGE_NAME=${{env.BUNDLE_NAME}}-snapshot
- export PACKAGE_VERSION=${{env.VERSION}}
+ export PACKAGE_VERSION=$VERSION
# Replace PACKAGE_NAME in control file and copy to Debian package directory
envsubst < $UBUNTU_CONTROL_FILE > $UBUNTU_PACKAGE_DIR/DEBIAN/control
@@ -376,6 +390,10 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4
+ - name: Read Version
+ run: |
+ echo "VERSION=$(tr -d '[:space:]' < VERSION.md)" >> $GITHUB_ENV
+
- name: Create user for build process
run: |
useradd -m dimethoxy
diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json
index 4d49ce61..76689a51 100644
--- a/.vscode/c_cpp_properties.json
+++ b/.vscode/c_cpp_properties.json
@@ -2,9 +2,10 @@
"configurations": [
{
"name": "Linux",
+ "compileCommands": "${workspaceFolder}/build/compile_commands.json",
"includePath": [
- "${workspaceFolder}/external/**",
- "${workspaceFolder}/src/**"
+ "${workspaceFolder}/src/**",
+ "${workspaceFolder}/external/**"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
diff --git a/.vscode/launch.json b/.vscode/launch.json
index a015b0cb..bf286a9c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -47,12 +47,13 @@
"name": "Launch FL Studio (Windows)",
"type": "cppvsdbg",
"request": "launch",
- "program": "C:\\Program Files\\Image-Line\\FL Studio 2024\\FL64.exe",
+ "program": "C:\\Program Files\\Image-Line\\FL Studio 2025\\FL64.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
- "console": "internalConsole"
+ "console": "internalConsole",
+ "symbolSearchPath": "C:\\Program Files\\Common Files\\VST3\\Dimethoxy\\Disflux"
},
{
"name": "Launch Bitwig Studio (Linux)",
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9a818828..df5fdb5b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,25 +1,26 @@
#==============================================================================
-# Main CMakeLists.txt file for the Oscilloscope project
+# Main CMakeLists.txt file for the Disflux project
#==============================================================================
cmake_minimum_required(VERSION 3.30)
-project(Disflux VERSION 1.1.2) # Disflux-Version
+file(READ "${CMAKE_CURRENT_LIST_DIR}/VERSION.md" FILE_VERSION)
+string(STRIP "${FILE_VERSION}" FILE_VERSION)
+project(Disflux VERSION ${FILE_VERSION})
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS TRUE)
set(EXT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external)
+# Sanitizer helpers are opt-in through presets (DMT_ENABLE_ASAN / DMT_ENABLE_UBSAN)
+include(${CMAKE_CURRENT_LIST_DIR}/cmake/Sanitizers.cmake)
+
# Set macOS deployment target for older macOS compatibility
-if(APPLE)
- # macOS 12.0 (Monterey) is the minimum for reliable C++23 support
- set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum macOS deployment target")
-endif()
include(cmake/get_cpm.cmake)
# Add external dependencies
CPMAddPackage(
NAME JUCE
GITHUB_REPOSITORY juce-framework/JUCE
- GIT_TAG master
+ GIT_TAG 8.0.12
SOURCE_DIR ${EXT_DIR}/juce
)
CPMAddPackage(
diff --git a/CMakePresets.json b/CMakePresets.json
index c73292db..afadad1e 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -48,6 +48,27 @@
"rhs": "Windows"
}
},
+ {
+ "name": "Windows Release With Debug Info",
+ "hidden": false,
+ "description": "Release configuration with debug symbols",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+ "CMAKE_C_COMPILER": "cl",
+ "CMAKE_CXX_COMPILER": "cl"
+ },
+ "environment": {
+ "CC": "cl",
+ "CXX": "cl"
+ },
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
{
"name": "Windows Perfetto",
"hidden": false,
@@ -70,6 +91,28 @@
"rhs": "Windows"
}
},
+ {
+ "name": "Windows ASan",
+ "hidden": false,
+ "description": "Debug + AddressSanitizer (MSVC cl)",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "CMAKE_C_COMPILER": "cl",
+ "CMAKE_CXX_COMPILER": "cl",
+ "DMT_ENABLE_ASAN": "ON"
+ },
+ "environment": {
+ "CC": "cl",
+ "CXX": "cl"
+ },
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
{
"name": "Mac Debug",
"hidden": false,
@@ -92,6 +135,29 @@
"rhs": "Darwin"
}
},
+ {
+ "name": "Mac Debug Perfetto",
+ "hidden": false,
+ "description": "Debug configuration for macOS",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "CMAKE_C_COMPILER": "/usr/bin/clang",
+ "CMAKE_CXX_COMPILER": "/usr/bin/clang++",
+ "CMAKE_OSX_DEPLOYMENT_TARGET": "10.15",
+ "CMAKE_CXX_FLAGS": "/DPERFETTO=1"
+ },
+ "environment": {
+ "CC": "/usr/bin/clang",
+ "CXX": "/usr/bin/clang++"
+ },
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Darwin"
+ }
+ },
{
"name": "Mac Release",
"hidden": false,
diff --git a/README.md b/README.md
index 0c8c3da4..9677fd24 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,6 @@ If it feels familiar, that's no accident. Disflux is a lovingly crafted take on
-
## π₯ Features
- **Free & Open-Source** β No paywalls, no restrictions.
@@ -27,10 +26,8 @@ If it feels familiar, that's no accident. Disflux is a lovingly crafted take on
## π§ Coming Soon
Here are some of the exciting things you can expect in future updates:
-
-- **Bug Fixes** β We are actively working on fixing bugs and improving stability.
+
- **Preset Menu** β Add functionality to select and save presets.
-- **Performance** β While performance is already solid, weβre working to optimize it even further.
- **Oversampling** β Implementing oversampling to minimize aliasing and improve the quality of high-frequency content.
- **Themes** - We already allow heavy theming, but we want to make it easier to export and share themes with the community.
- **Mobile Support** β We are planning to release Disflux for iOS and Android in the future.
@@ -54,21 +51,25 @@ If you want to compile Plasma from source yourself, follow these steps:
### 1. Prerequisites
**Ubuntu**
+
- Install build tools: `sudo apt-get install build-essential cmake ninja-build`
- Install dependencies: `sudo apt install libasound2-dev libjack-jackd2-dev ladspa-sdk libcurl4-openssl-dev libfreetype-dev libfontconfig1-dev libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libglu1-mesa-dev mesa-common-dev curl`
-**MacOS**
+**MacOS**
+
1. Install [Homebrew](https://brew.sh/)
2. Install build tools: `brew install ninja osxutils`
**Windows**
+
1. Install [Git](https://git-scm.com/downloads)
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
- - During installation, make sure to select package "Desktop development with C++"
-4. Install [Chocolatey](https://chocolatey.org/install) (open **PowerShell** as Admin and follow instructions on their site)
-5. Use **Chocolatey** to install required tools: `choco install cmake ninja llvm`
-
+ - During installation, make sure to select package "Desktop development with C++"
+3. Install [Chocolatey](https://chocolatey.org/install) (open **PowerShell** as Admin and follow instructions on their site)
+4. Use **Chocolatey** to install required tools: `choco install cmake ninja llvm`
+
### 2. Clone the Repository
+
```bash
git clone https://github.com/yourusername/Disflux.git
cd Disflux
@@ -79,16 +80,19 @@ cd Disflux
Run CMake to configure the project. Use the appropriate preset for your platform:
**Ubuntu**
+
```bash
cmake --preset "Linux Release"
```
**MacOS**
+
```bash
cmake --preset "Mac Release" -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
```
**Windows**
+
```bash
cmake --preset "Windows Release"
```
@@ -96,6 +100,7 @@ cmake --preset "Windows Release"
### 4. Build the project
After configuring with CMake, build the project using the following command:
+
```bash
cmake --build build --config "Release"
```
@@ -103,6 +108,7 @@ cmake --build build --config "Release"
### 5. Locate the Build Artifacts
After the build process completes, you can find the compiled artifacts in the build directory under the following paths:
+
- **VST3:** `build/src/DisfluxPlugin_artefacts/Release/VST3/Disflux.vst3`
- **CLAP:** `build/src/DisfluxPlugin_artefacts/Release/CLAP/Disflux.clap`
- **LV2:** `build/src/DisfluxPlugin_artefacts/Release/LV2/Disflux.lv2`
@@ -110,7 +116,6 @@ After the build process completes, you can find the compiled artifacts in the bu
You can move these to your plugin folder.
-
## π Privacy
**Disflux** is built with privacy in mind. It **does not collect any personal data** or send any telemetry. We are committed to **never sharing or selling your data**. It makes us sad that in today's day and age, we consider this to be a standout point, but here we are.
diff --git a/VERSION b/VERSION
deleted file mode 100644
index e69de29b..00000000
diff --git a/VERSION.md b/VERSION.md
new file mode 100644
index 00000000..26aaba0e
--- /dev/null
+++ b/VERSION.md
@@ -0,0 +1 @@
+1.2.0
diff --git a/cmake/Sanitizers.cmake b/cmake/Sanitizers.cmake
new file mode 100644
index 00000000..7b5318f8
--- /dev/null
+++ b/cmake/Sanitizers.cmake
@@ -0,0 +1,43 @@
+#==============================================================================
+# Opt-in sanitizer and debug-symbol configuration.
+# Supports both target-scoped (for selective sanitization) and global (for consistency).
+#==============================================================================
+
+option(DMT_ENABLE_ASAN "Enable AddressSanitizer for all targets" OFF)
+option(DMT_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer for all targets" OFF)
+
+# Apply sanitizer flags globally when enabled to ensure consistency across all compilation units
+# Windows MSVC only - no sanitizers on Mac or Linux
+if(DMT_ENABLE_ASAN OR DMT_ENABLE_UBSAN)
+ if(MSVC)
+ if(DMT_ENABLE_UBSAN)
+ message(WARNING "DMT_ENABLE_UBSAN is not supported with MSVC cl; ignoring UBSan")
+ endif()
+
+ if(DMT_ENABLE_ASAN)
+ add_compile_options(/fsanitize=address)
+ add_link_options(/fsanitize=address)
+ # Disable incremental linking with ASAN
+ add_link_options(/INCREMENTAL:NO)
+ endif()
+
+ # Symbol and stack quality for debugger + sanitizer crash reports.
+ add_compile_options(/Zi /Oy-)
+ add_link_options(/DEBUG)
+ else()
+ message(WARNING "Sanitizers are only supported on Windows with MSVC; ignoring DMT_ENABLE_ASAN and DMT_ENABLE_UBSAN")
+ endif()
+endif()
+
+add_compile_definitions($,DMT_SANITIZERS_ENABLED=1,>)
+
+function(dmt_target_enable_sanitizers target_name)
+ if(NOT TARGET ${target_name})
+ message(FATAL_ERROR "dmt_target_enable_sanitizers: unknown target '${target_name}'")
+ endif()
+
+ # Sanitizers are now applied globally, so this function is a no-op for backwards compatibility
+ if(DMT_ENABLE_ASAN)
+ target_compile_definitions(${target_name} PRIVATE DMT_SANITIZERS_ENABLED=1)
+ endif()
+endfunction()
diff --git a/external/clap b/external/clap
index e1f67893..e8de9e85 160000
--- a/external/clap
+++ b/external/clap
@@ -1 +1 @@
-Subproject commit e1f67893cc409a40c1154fa2e78c97046da24ce0
+Subproject commit e8de9e8571626633b8541a54c2406fccc4272767
diff --git a/external/juce b/external/juce
index 501c0767..29396c22 160000
--- a/external/juce
+++ b/external/juce
@@ -1 +1 @@
-Subproject commit 501c07674e1ad693085a7e7c398f205c2677f5da
+Subproject commit 29396c22c93392d6738e021b83196283d6e4d850
diff --git a/external/melatonin_blur b/external/melatonin_blur
index f5764e3a..989a6e1f 160000
--- a/external/melatonin_blur
+++ b/external/melatonin_blur
@@ -1 +1 @@
-Subproject commit f5764e3a5c98f7b2849fe4c9c2205a854a658fa3
+Subproject commit 989a6e1f8d79b7183c9daa6271cb71f7f8800229
diff --git a/pkg/arch/git/PKGBUILD b/pkg/arch/git/PKGBUILD
index 9d8c2a9d..fba0c887 100644
--- a/pkg/arch/git/PKGBUILD
+++ b/pkg/arch/git/PKGBUILD
@@ -18,7 +18,7 @@ sha256sums=('SKIP')
# Build and runtime dependencies β adjust as necessary for your project.
makedepends=('gcc' 'git' 'cmake' 'ninja' 'pkg-config' 'alsa-lib' 'jack' 'ladspa'
'curl' 'freetype2' 'fontconfig' 'libx11' 'libxcomposite' 'libxcursor'
- 'libxext' 'libxinerama' 'libxrandr' 'libxrender' 'webkit2gtk' 'glu' 'mesa')
+ 'libxext' 'libxinerama' 'libxrandr' 'libxrender' 'webkit2gtk-4.1' 'glu' 'mesa')
depends=('curl')
# Generate a proper pkgver from the git commit (using git describe)
diff --git a/pkg/arch/release/PKGBUILD b/pkg/arch/release/PKGBUILD
index 8af6b231..9288457d 100644
--- a/pkg/arch/release/PKGBUILD
+++ b/pkg/arch/release/PKGBUILD
@@ -18,7 +18,7 @@ sha256sums=('SKIP')
# Build and runtime dependencies β adjust as necessary for your project.
makedepends=('gcc' 'git' 'cmake' 'ninja' 'pkg-config' 'alsa-lib' 'jack' 'ladspa'
'curl' 'freetype2' 'fontconfig' 'libx11' 'libxcomposite' 'libxcursor'
- 'libxext' 'libxinerama' 'libxrandr' 'libxrender' 'webkit2gtk' 'glu' 'mesa' 'libxml2')
+ 'libxext' 'libxinerama' 'libxrandr' 'libxrender' 'webkit2gtk-4.1' 'glu' 'mesa' 'libxml2')
depends=('curl')
build() {
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 92828bcd..5a038198 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,16 +1,20 @@
#==============================================================================
-# Oscilloscope Source Folder CMakeLists.txt file
+# Disflux Source Folder CMakeLists.txt file
#==============================================================================
cmake_minimum_required(VERSION 3.22)
-project(DisfluxPlugin VERSION 1.1.2) # Disflux-Version
+if(NOT DEFINED FILE_VERSION)
+ file(READ "${CMAKE_CURRENT_LIST_DIR}/../VERSION.md" FILE_VERSION)
+ string(STRIP "${FILE_VERSION}" FILE_VERSION)
+endif()
+project(DisfluxPlugin VERSION ${FILE_VERSION})
# If we are on MacOS, we need to build for arm64 and x86_64
if (APPLE)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
# Apparently macOS 12.0 is the minimum for reliable C++23 support
# But we yolo it and just hope for the best on older macOS versions
- set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
endif()
# Option to disable update notification (default OFF)
@@ -18,19 +22,19 @@ option(DMT_DISABLE_UPDATE_NOTIFICATION "Disable update notification in the GUI"
# JUCE setup
if(WIN32)
- set(DISFLUX_PLUGIN_FORMATS "VST3;CLAP;Standalone")
+ set(OS_PLUGIN_FORMATS "VST3;CLAP;Standalone")
elseif(APPLE)
- set(DISFLUX_PLUGIN_FORMATS "VST3;CLAP;AU;Standalone")
+ set(OS_PLUGIN_FORMATS "VST3;CLAP;AU;Standalone")
elseif(UNIX)
- set(DISFLUX_PLUGIN_FORMATS "VST3;CLAP;LV2;Standalone")
+ set(OS_PLUGIN_FORMATS "VST3;CLAP;LV2;Standalone")
else()
- set(DISFLUX_PLUGIN_FORMATS "VST3;CLAP;Standalone")
+ set(OS_PLUGIN_FORMATS "VST3;CLAP;Standalone")
endif()
juce_add_plugin(${PROJECT_NAME}
PRODUCT_NAME "Disflux"
COMPANY_NAME "Dimethoxy"
- FORMATS ${DISFLUX_PLUGIN_FORMATS}
+ FORMATS ${OS_PLUGIN_FORMATS}
VST3_CATEGORIES "Fx" "Analyzer"
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
@@ -118,6 +122,9 @@ target_link_libraries(${PROJECT_NAME}
juce::juce_recommended_warning_flags
)
+ # Apply opt-in sanitizers and debug-symbol settings for crash diagnostics.
+ dmt_target_enable_sanitizers(${PROJECT_NAME})
+
# Conditionally link curl only on Linux
if(UNIX AND NOT APPLE)
target_link_libraries(${PROJECT_NAME}
diff --git a/src/app/PluginEditor.cpp b/src/app/PluginEditor.cpp
index 113a8f1d..624be54f 100644
--- a/src/app/PluginEditor.cpp
+++ b/src/app/PluginEditor.cpp
@@ -1,245 +1,19 @@
#include "PluginEditor.h"
#include "PluginProcessor.h"
-namespace {
-// Filter out notification-level GL debug messages
-static void KHRONOS_APIENTRY
-juceFilteredGLDebugCallback(GLenum source,
- GLenum type,
- GLuint id,
- GLenum severity,
- GLsizei length,
- const GLchar* message,
- const void* userParam)
-{
- // Ignore low-priority notifications
- if (severity == juce::gl::GL_DEBUG_SEVERITY_NOTIFICATION)
- return;
-
- // Log other messages so we don't lose important info
- juce::String msg =
- (message != nullptr) ? juce::String(message) : juce::String();
- DBG("OpenGL DBG message: " << msg);
-
- // Keep JUCE's behaviour for serious errors
- if (type == juce::gl::GL_DEBUG_TYPE_ERROR &&
- severity == juce::gl::GL_DEBUG_SEVERITY_HIGH)
- jassertfalse;
-}
-
-} // anonymous namespace
-
//==============================================================================
PluginEditor::PluginEditor(PluginProcessor& p)
- : AudioProcessorEditor(&p)
- , p(p)
- , sizeFactor(p.sizeFactor)
- , mainLayout({}, {})
- , compositor("DisFlux", mainLayout, p.apvts, p.properties, sizeFactor)
- , compositorAttached(true)
+ : dmt::app::AbstractPluginEditor(
+ p,
+ "DisFlux",
+ 500,
+ 270,
+ [&p](dmt::gui::window::Layout& layout) {
+ layout.addPanel>(
+ 0, 0, 1, 1, p.apvts, p.oscilloscopeBuffer);
+ })
{
- mainLayout.addPanel>(
- 0, 0, 1, 1, p.apvts, p.oscilloscopeBuffer);
-
- if (OS_IS_WINDOWS) {
- setResizable(false, true);
- }
-
- if (OS_IS_DARWIN) {
- setResizable(false, true);
- }
-
- if (OS_IS_LINUX) {
- openGLContext.setComponentPaintingEnabled(true);
- openGLContext.setContinuousRepainting(false);
- openGLContext.attachTo(*getTopLevelComponent());
- std::thread([this]() {
- for (int i = 0; i < 200; ++i) {
- if (openGLContext.isAttached() &&
- openGLContext.getRawContext() != nullptr)
- break;
- std::this_thread::sleep_for(std::chrono::milliseconds(25));
- }
-
- if (!openGLContext.isAttached() ||
- openGLContext.getRawContext() == nullptr)
- return;
-
- openGLContext.executeOnGLThread(
- [](juce::OpenGLContext&) {
- if (juce::gl::glDebugMessageControl) {
- juce::gl::glDebugMessageControl(
- juce::gl::GL_DEBUG_SOURCE_API,
- juce::gl::GL_DEBUG_TYPE_OTHER,
- juce::gl::GL_DEBUG_SEVERITY_NOTIFICATION,
- 0,
- nullptr,
- juce::gl::GL_FALSE);
- }
-
- if (juce::gl::glDebugMessageCallback)
- juce::gl::glDebugMessageCallback(juceFilteredGLDebugCallback,
- nullptr);
- },
- true);
- }).detach();
- }
-
- setConstraints(baseWidth, baseHeight + headerHeight);
- addAndMakeVisible(compositor);
- setResizable(false, true);
-
- const auto startWidth = baseWidth * sizeFactor;
- const auto startHeight = (baseHeight + headerHeight) * sizeFactor;
- setSize(startWidth, startHeight);
-
- // Set the callback for header visibility changes
- compositor.setHeaderVisibilityCallback([this](bool isHeaderVisible) {
- handleHeaderVisibilityChange(isHeaderVisible);
- });
}
-void
-PluginEditor::handleHeaderVisibilityChange(bool isHeaderVisible)
-{
- const int adjustedHeight =
- isHeaderVisible ? baseHeight + headerHeight : baseHeight;
- setConstraints(baseWidth, adjustedHeight);
- setSize(baseWidth * sizeFactor, adjustedHeight * sizeFactor);
-}
//==============================================================================
PluginEditor::~PluginEditor() {}
-
-//==============================================================================
-void
-PluginEditor::paint(juce::Graphics& g)
-{
- TRACER("PluginEditor::paint");
-
- // Just painting the background
- g.fillAll(dmt::Settings::Window::backgroundColour);
-
- if (!compositorAttached && compositorSnapshot.isValid()) {
- // Draw the last compositor snapshot, scaled to fit
- auto bounds = getLocalBounds().toFloat();
- g.drawImage(
- compositorSnapshot, bounds, juce::RectanglePlacement::stretchToFit);
- return;
- }
-}
-
-//==============================================================================
-void
-PluginEditor::setConstraints(int width, int height)
-{
- if (auto* constrainer = this->getConstrainer()) {
- const auto aspectRatio = (double)width / (double)height;
- constrainer->setFixedAspectRatio(aspectRatio);
- const auto minWidth = width / 2;
- const auto minHeight = height / 2;
- const auto maxWidth = width * 2;
- const auto maxHeight = height * 2;
- constrainer->setSizeLimits(minWidth, minHeight, maxWidth, maxHeight);
- } else {
- jassertfalse; // Constrainer not set
- }
-}
-
-//==============================================================================
-void
-PluginEditor::resized()
-{
- TRACER("PluginEditor::resized");
-
- // Set the global size
- const int currentHeight = getHeight();
- const float newSize =
- (float)currentHeight /
- (compositor.isHeaderVisible() ? baseHeight + headerHeight : baseHeight);
-
- // Make sure the size makes sense
- if (newSize <= 0.0f || std::isinf(newSize)) {
- jassertfalse;
- }
-
- // Update the processor's scale factor
- sizeFactor = newSize;
- p.setSizeFactor(newSize);
-
- // Debounced resizing logic
- if (firstDraw) {
- // On first draw, skip debounce and just layout normally
- compositor.setBounds(getLocalBounds());
- firstDraw = false;
- return;
- }
- detachCompositorForResize();
-}
-
-void
-PluginEditor::detachCompositorForResize()
-{
- if (compositorAttached) {
- // Take a snapshot before detaching
- updateCompositorSnapshot();
-
- // Remove compositor from view
- removeChildComponent(&compositor);
- compositorAttached = false;
- }
-
- // Restart debounce timer (100ms)
- stopTimer();
- startTimer(100);
-}
-
-void
-PluginEditor::attachCompositorAfterResize()
-{
- if (!compositorAttached) {
- // Snap to the correct aspect ratio, considering header visibility
- auto bounds = getLocalBounds();
- bool headerVisible = compositor.isHeaderVisible();
- int aspectHeight = headerVisible ? (baseHeight + headerHeight) : baseHeight;
- const double aspect = (double)baseWidth / (double)aspectHeight;
- int w = bounds.getWidth();
- int h = bounds.getHeight();
- double currentAspect = (double)w / (double)h;
-
- if (currentAspect > aspect) {
- // Too wide, adjust width
- w = static_cast(h * aspect);
- } else if (currentAspect < aspect) {
- // Too tall, adjust height
- h = static_cast(w / aspect);
- }
- setSize(w, h);
-
- // Set compositor bounds to fill the editor
- addAndMakeVisible(compositor);
- compositor.setBounds(getLocalBounds());
- compositorAttached = true;
- repaint();
- }
-}
-
-void
-PluginEditor::updateCompositorSnapshot()
-{
- // Render compositor to an image at its current size
- if (getWidth() > 0 && getHeight() > 0) {
- compositorSnapshot =
- juce::Image(juce::Image::ARGB, getWidth(), getHeight(), true);
- juce::Graphics g(compositorSnapshot);
- compositor.paintEntireComponent(g, true);
- }
-}
-
-void
-PluginEditor::timerCallback()
-{
- // Timer expired: reattach compositor and repaint
- stopTimer();
- attachCompositorAfterResize();
- repaint();
-}
diff --git a/src/app/PluginEditor.h b/src/app/PluginEditor.h
index 5b50e89f..07ff6886 100644
--- a/src/app/PluginEditor.h
+++ b/src/app/PluginEditor.h
@@ -4,58 +4,14 @@
#include
//==============================================================================
-class PluginEditor
- : public juce::AudioProcessorEditor
- , private juce::Timer
+class PluginEditor : public dmt::app::AbstractPluginEditor
{
- using Image = juce::Image;
- using ImageComponent = juce::ImageComponent;
- using PixelFormat = juce::Image::PixelFormat;
- using OpenGLContext = juce::OpenGLContext;
-
- // Window size
- const int baseWidth = 500;
- const int baseHeight = 270;
-
- // Window header
- const int& headerHeight = dmt::Settings::Header::height;
-
public:
explicit PluginEditor(PluginProcessor&);
~PluginEditor() override;
- //==============================================================================
- void paint(juce::Graphics&) override;
- void resized() override;
- void setConstraints(int width, int height);
- void handleHeaderVisibilityChange(bool isHeaderVisible);
-
- // Debounced resizing
- void timerCallback() override;
- void detachCompositorForResize();
- void attachCompositorAfterResize();
- void updateCompositorSnapshot();
-
private:
//==============================================================================
- PluginProcessor& p;
- OpenGLContext openGLContext;
- //==============================================================================
- int lastWidth = baseWidth;
- int lastHeight = baseHeight;
- double ratio = baseWidth / baseHeight;
- float& sizeFactor = p.sizeFactor;
- bool firstDraw = true;
- //==============================================================================
- Image image;
- bool isResizing = false;
- //==============================================================================
- dmt::gui::window::Layout mainLayout;
- dmt::gui::window::Compositor compositor;
- //==============================================================================
- juce::Image compositorSnapshot;
- bool compositorAttached = true;
- //==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginEditor)
};
diff --git a/src/app/PluginProcessor.cpp b/src/app/PluginProcessor.cpp
index db64a27a..d882ef6c 100644
--- a/src/app/PluginProcessor.cpp
+++ b/src/app/PluginProcessor.cpp
@@ -3,16 +3,7 @@
#include "PluginEditor.h"
//==============================================================================
PluginProcessor::PluginProcessor()
- : AudioProcessor(
- BusesProperties()
-#if !JucePlugin_IsMidiEffect
-#if !JucePlugin_IsSynth
- .withInput("Input", juce::AudioChannelSet::stereo(), true)
-#endif
- .withOutput("Output", juce::AudioChannelSet::stereo(), true)
-#endif
- )
- , apvts(*this, nullptr, ProjectInfo::projectName, createParameterLayout())
+ : dmt::app::AbstractPluginProcessor(createParameterLayout)
, oscilloscopeBuffer(2, 4096)
, disfluxProcessor(apvts,
dmt::Settings::Audio::frequencySmoothness,
@@ -22,18 +13,9 @@ PluginProcessor::PluginProcessor()
dmt::Settings::Audio::outputHighpassFrequency,
dmt::Settings::Audio::smoothingInterval)
{
-#if PERFETTO
- MelatoninPerfetto::get().beginSession();
-#endif
- properties.initialize();
}
-PluginProcessor::~PluginProcessor()
-{
-#if PERFETTO
- MelatoninPerfetto::get().endSession();
-#endif
-}
+PluginProcessor::~PluginProcessor() = default;
//==============================================================================
const juce::String
@@ -42,75 +24,6 @@ PluginProcessor::getName() const
return "Disflux";
}
-bool
-PluginProcessor::acceptsMidi() const
-{
-#if JucePlugin_WantsMidiInput
- return true;
-#else
- return false;
-#endif
-}
-
-bool
-PluginProcessor::producesMidi() const
-{
-#if JucePlugin_ProducesMidiOutput
- return true;
-#else
- return false;
-#endif
-}
-
-bool
-PluginProcessor::isMidiEffect() const
-{
-#if JucePlugin_IsMidiEffect
- return true;
-#else
- return false;
-#endif
-}
-
-double
-PluginProcessor::getTailLengthSeconds() const
-{
- return 0.0;
-}
-
-int
-PluginProcessor::getNumPrograms()
-{
- return 1; // NB: some hosts don't cope very well if you tell them there are 0
- // programs, so this should be at least 1, even if you're not really
- // implementing programs.
-}
-
-int
-PluginProcessor::getCurrentProgram()
-{
- return 0;
-}
-
-void
-PluginProcessor::setCurrentProgram(int index)
-{
- juce::ignoreUnused(index);
-}
-
-const juce::String
-PluginProcessor::getProgramName(int index)
-{
- juce::ignoreUnused(index);
- return {};
-}
-
-void
-PluginProcessor::changeProgramName(int index, const juce::String& newName)
-{
- juce::ignoreUnused(index, newName);
-}
-
//==============================================================================
void
PluginProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
@@ -120,6 +33,7 @@ PluginProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
disfluxProcessor.prepare(sampleRate);
}
+//==============================================================================
void
PluginProcessor::releaseResources()
{
@@ -127,51 +41,22 @@ PluginProcessor::releaseResources()
// spare memory, etc.
}
-bool
-PluginProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
-{
-#if JucePlugin_IsMidiEffect
- juce::ignoreUnused(layouts);
- return true;
-#else
- // This is the place where you check if the layout is supported.
- // In this template code we only support mono or stereo.
- // Some plugin hosts, such as certain GarageBand versions, will only
- // load plugins that support stereo bus layouts.
- if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() &&
- layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
- return false;
-
- // This checks if the input layout matches the output layout
-#if !JucePlugin_IsSynth
- if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
- return false;
-#endif
-
- return true;
-#endif
-}
-
+//==============================================================================
void
PluginProcessor::processBlock(juce::AudioBuffer& buffer,
juce::MidiBuffer& midiMessages)
{
+ // Boilerplate
juce::ignoreUnused(midiMessages);
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
- // In case we have more outputs than inputs, this code clears any output
- // channels that didn't contain input data, (because these aren't
- // guaranteed to be empty - they may contain garbage).
- // This is here to avoid people getting screaming feedback
- // when they first compile a plugin, but obviously you don't need to keep
- // this code if your algorithm always overwrites all the output channels.
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
- //============================================================================
+ // Start actual processing
TRACE_DSP();
const auto* bypassParam = apvts.getRawParameterValue("GlobalBypass");
bool isBypassed = bypassParam->load() > 0.5f;
@@ -183,37 +68,13 @@ PluginProcessor::processBlock(juce::AudioBuffer& buffer,
}
//==============================================================================
-bool
-PluginProcessor::hasEditor() const
-{
- return true; // (change this to false if you choose to not supply an editor)
-}
-
+// This creates new instances of the plugin..
juce::AudioProcessorEditor*
PluginProcessor::createEditor()
{
return new PluginEditor(*this);
}
-//==============================================================================
-void
-PluginProcessor::getStateInformation(juce::MemoryBlock& destData)
-{
- juce::MemoryOutputStream mos(destData, true);
- apvts.state.writeToStream(mos);
-}
-
-void
-PluginProcessor::setStateInformation(const void* data, int sizeInBytes)
-{
- auto tree = juce::ValueTree::readFromData(data, sizeInBytes);
- if (tree.isValid()) {
- apvts.replaceState(tree);
- }
-}
-
-//==============================================================================
-// This creates new instances of the plugin..
juce::AudioProcessor* JUCE_CALLTYPE
createPluginFilter()
{
diff --git a/src/app/PluginProcessor.h b/src/app/PluginProcessor.h
index 8c668042..e1ec9ee5 100644
--- a/src/app/PluginProcessor.h
+++ b/src/app/PluginProcessor.h
@@ -5,7 +5,7 @@
#include
//==============================================================================
-class PluginProcessor final : public juce::AudioProcessor
+class PluginProcessor final : public dmt::app::AbstractPluginProcessor
{
public:
//==============================================================================
@@ -13,58 +13,19 @@ class PluginProcessor final : public juce::AudioProcessor
~PluginProcessor() override;
//==============================================================================
+ const juce::String getName() const override;
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
-
- bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
-
void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override;
- using AudioProcessor::processBlock;
//==============================================================================
juce::AudioProcessorEditor* createEditor() override;
- bool hasEditor() const override;
-
- //==============================================================================
- const juce::String getName() const override;
-
- bool acceptsMidi() const override;
- bool producesMidi() const override;
- bool isMidiEffect() const override;
- double getTailLengthSeconds() const override;
-
- //==============================================================================
- int getNumPrograms() override;
- int getCurrentProgram() override;
- void setCurrentProgram(int index) override;
- const juce::String getProgramName(int index) override;
- void changeProgramName(int index, const juce::String& newName) override;
-
- //==============================================================================
- void getStateInformation(juce::MemoryBlock& destData) override;
- void setStateInformation(const void* data, int sizeInBytes) override;
-
- //==============================================================================
- juce::AudioProcessorValueTreeState apvts;
-
- //==============================================================================
- dmt::configuration::Properties properties;
- dmt::version::Manager versionManager;
//==============================================================================
dmt::dsp::data::FifoAudioBuffer oscilloscopeBuffer;
dmt::dsp::effect::DisfluxProcessor disfluxProcessor;
- //==============================================================================
- // Store scale factor for editor window
- float sizeFactor = 1.0f;
- float getSizeFactor() const { return sizeFactor; }
- void setSizeFactor(float newSize) { sizeFactor = newSize; }
-
private:
-#if PERFETTO
- std::unique_ptr tracingSession;
-#endif
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginProcessor)
};
diff --git a/src/dmt/DmtHeader.h b/src/dmt/DmtHeader.h
index b3390cb6..52f895ff 100644
--- a/src/dmt/DmtHeader.h
+++ b/src/dmt/DmtHeader.h
@@ -17,6 +17,7 @@
//==============================================================================
#include "../melatonin_perfetto/melatonin_perfetto/melatonin_perfetto.h"
//==============================================================================
+#include "./app/App.h"
#include "./configuration/Configuration.h"
#include "./dsp/Dsp.h"
#include "./gui/Gui.h"
diff --git a/src/dmt/README.md b/src/dmt/README.md
index 87743ea1..ce41b775 100644
--- a/src/dmt/README.md
+++ b/src/dmt/README.md
@@ -1,23 +1,30 @@
# Dimethoxy Library
-> [!WARNING]
-> This library is under active development and primarily intended for use within Dimethoxy projects \
-> External use is unsupported.
+> [!CAUTION]
+> **Do NOT use this library in your own projects if you expect support.**
+> We will **not fix your issues, bugs, or integration problems** if you use it externally.
+>
+> This library is under active development and is built **exclusively for Dimethoxy projects**.
+> External use is entirely at your own risk.
-The Dimethoxy Library is the core engine behind all Dimethoxy plugins β a modular C++ codebase built for high-performance digital signal processing and plugin development.
-From custom DSP algorithms to GUI systems and utility wrappers, this library powers nearly every aspect of our audio tools.
+The Dimethoxy Library is the core engine behind all Dimethoxy plugins.
+While technically usable outside the ecosystem, it is **not designed, tested, or maintained** for third-party use.
-## Features
-- π High-performance DSP tailored for aggressive electronic music (Hardstyle, Hardcore, Uptempo, etc.)
-- ποΈ Modular components for efficient plugin development
-- π§± Shared utilities, math, parameter handling, and UI helpers
-- π§ͺ Designed for maintainability and extensibility across multiple projects
+No guarantees are made regarding:
+
+* Stability
+* Compatibility
+* API consistency
+* Build success on your setup
+
+If it breaks, you own the pieces.
## Used In
-- [Disflux](https://github.com/Dimethoxy/Disflux) β Transient Smearing Audio Plugin for Windows, MacOS and Linux
-- [Oscilloscope](https://github.com/Dimethoxy/Oscilloscope) β Work in Progress Oscilloscope Audio Plugin for Windows, MacOS and Linux
+* [Disflux](https://github.com/Dimethoxy/Disflux): Transient Smearing Audio Plugin for Windows, macOS, and Linux
+* [Oscilloscope](https://github.com/Dimethoxy/Oscilloscope): Work-in-progress Oscilloscope Audio Plugin for Windows, macOS, and Linux
## License
-This project is licensed under AGPLv3. \
+
+This project is licensed under AGPLv3.
Any project using **any** part of this library **must** also be licensed under AGPLv3 or a compatible open-source license.
diff --git a/src/dmt/app/AbstractPluginEditor.h b/src/dmt/app/AbstractPluginEditor.h
index e743115c..0a8214a9 100644
--- a/src/dmt/app/AbstractPluginEditor.h
+++ b/src/dmt/app/AbstractPluginEditor.h
@@ -1,3 +1,355 @@
#pragma once
-#include
\ No newline at end of file
+//==============================================================================
+// Preprocessor flags for renderer control
+#define DMT_SUPPRESS_GL_DEBUG_MESSAGES 0
+
+//==============================================================================
+
+#include "app/AbstractPluginProcessor.h"
+#include "gui/window/Compositor.h"
+#include
+#include
+
+namespace dmt {
+namespace app {
+class AbstractPluginEditor
+ : public juce::AudioProcessorEditor
+ , protected juce::Timer
+{
+ using Image = juce::Image;
+ using ImageComponent = juce::ImageComponent;
+ using PixelFormat = juce::Image::PixelFormat;
+ using OpenGLContext = juce::OpenGLContext;
+
+public:
+ // Strategy function type for layout initialization
+ using LayoutInitializer = std::function;
+
+ AbstractPluginEditor(dmt::app::AbstractPluginProcessor& _p,
+ juce::String _name,
+ int _baseWidth,
+ int _baseHeight,
+ LayoutInitializer&& _layoutInit)
+ : juce::AudioProcessorEditor(&_p)
+ , p(_p)
+ , baseWidth(_baseWidth)
+ , baseHeight(_baseHeight)
+ , sizeFactor(p.sizeFactor)
+ , mainLayout({}, {})
+ , compositor(_name, mainLayout, p.apvts, p.properties, sizeFactor)
+ , compositorAttached(true)
+ {
+ // Initialize the layout via strategy function
+ _layoutInit(mainLayout);
+
+ // Now that layout is fully configured, attach the compositor
+ addAndMakeVisible(compositor);
+
+#if OS_IS_DARWIN || OS_IS_LINUX
+ // Determine if hardware acceleration should be used
+ bool shouldUseOpenGL = dmt::Settings::useOpenGL;
+ DBG("[AbstractPluginEditor] OpenGL renderer: "
+ << (shouldUseOpenGL ? "ENABLED" : "DISABLED"));
+
+ if (OS_IS_DARWIN) {
+ // macOS: Use OpenGL for hardware acceleration if enabled
+ if (shouldUseOpenGL) {
+ DBG("[AbstractPluginEditor] Using macOS OpenGL renderer");
+ openGLContext.setComponentPaintingEnabled(true);
+ openGLContext.setContinuousRepainting(false);
+ openGLContext.attachTo(*getTopLevelComponent());
+ setupOpenGLContext();
+ } else {
+ DBG("[AbstractPluginEditor] Using macOS software renderer");
+ }
+ setResizable(false, true);
+ }
+
+ if (OS_IS_LINUX) {
+ // Linux: Use OpenGL for hardware acceleration if enabled
+ if (shouldUseOpenGL) {
+ DBG("[AbstractPluginEditor] Using Linux OpenGL renderer");
+ openGLContext.setComponentPaintingEnabled(true);
+ openGLContext.setContinuousRepainting(false);
+ openGLContext.attachTo(*getTopLevelComponent());
+ setupOpenGLContext();
+ } else {
+ DBG("[AbstractPluginEditor] Using Linux software renderer");
+ }
+ }
+
+#endif
+
+ setConstraints(baseWidth, baseHeight + headerHeight);
+ setResizable(false, true);
+
+ const auto startWidth = baseWidth * sizeFactor;
+ const auto startHeight = (baseHeight + headerHeight) * sizeFactor;
+ setSize(startWidth, startHeight);
+
+ // Set the callback for header visibility changes
+ compositor.setHeaderVisibilityCallback([this](bool isHeaderVisible) {
+ handleHeaderVisibilityChange(isHeaderVisible);
+ });
+ }
+
+ ~AbstractPluginEditor()
+ {
+ // Ensure OpenGL context is detached before destruction
+ if (openGLContext.isAttached()) {
+ openGLContext.detach();
+ }
+
+ // Stop the debounce timer if it's running
+ stopTimer();
+ }
+
+ //==============================================================================
+ // JUCE overrides
+
+ void paint(juce::Graphics& g)
+ {
+ TRACER("PluginEditor::paint");
+
+ // Just painting the background
+ g.fillAll(dmt::Settings::Window::backgroundColour);
+
+ if (!compositorAttached && compositorSnapshot.isValid()) {
+ // Draw the last compositor snapshot, scaled to fit
+ auto bounds = getLocalBounds().toFloat();
+ g.drawImage(
+ compositorSnapshot, bounds, juce::RectanglePlacement::stretchToFit);
+ return;
+ }
+ }
+
+ void resized()
+ {
+ TRACER("PluginEditor::resized");
+
+ // Set the global size
+ const int currentHeight = getHeight();
+ const float newSize =
+ (float)currentHeight /
+ (compositor.isHeaderVisible() ? baseHeight + headerHeight : baseHeight);
+
+ // Make sure the size makes sense
+ if (newSize <= 0.0f || std::isinf(newSize)) {
+ jassertfalse;
+ }
+
+ // Update the processor's scale factor
+ sizeFactor = newSize;
+ p.setSizeFactor(newSize);
+
+ // Debounced resizing logic
+ if (firstDraw) {
+ // On first draw, skip debounce and just layout normally
+ compositor.setBounds(getLocalBounds());
+ firstDraw = false;
+ return;
+ }
+ detachCompositorForResize();
+ }
+
+ //==============================================================================
+ // Handle peer creation for Windows Direct2D setup
+
+ void parentHierarchyChanged() override {}
+
+ //==============================================================================
+ // JUCE overrides
+
+ void setConstraints(int width, int height)
+ {
+ if (auto* constrainer = this->getConstrainer()) {
+ const auto aspectRatio = (double)width / (double)height;
+ constrainer->setFixedAspectRatio(aspectRatio);
+ const auto minWidth = width / 2;
+ const auto minHeight = height / 2;
+ const auto maxWidth = width * 2;
+ const auto maxHeight = height * 2;
+ constrainer->setSizeLimits(minWidth, minHeight, maxWidth, maxHeight);
+ } else {
+ jassertfalse; // Constrainer not set
+ }
+ }
+
+ void handleHeaderVisibilityChange(bool isHeaderVisible)
+ {
+ const int adjustedHeight =
+ isHeaderVisible ? baseHeight + headerHeight : baseHeight;
+ setConstraints(baseWidth, adjustedHeight);
+ setSize(baseWidth * sizeFactor, adjustedHeight * sizeFactor);
+ }
+
+ dmt::gui::window::Layout& getMainLayout() { return mainLayout; }
+
+ //==============================================================================
+ // Debounced resizing
+
+ // Debounce timer callback: reattach compositor and repaint
+ void timerCallback() override
+ {
+ stopTimer();
+ attachCompositorAfterResize();
+ repaint(); // TODO: Redundant call, maybe remove this?
+ }
+
+ // Detach compositor to improve resize performance
+ void detachCompositorForResize()
+ {
+ if (compositorAttached) {
+ // Take a snapshot before detaching
+ updateCompositorSnapshot();
+
+ // Remove compositor from view
+ removeChildComponent(&compositor);
+ compositorAttached = false;
+ }
+
+ // Restart debounce timer (100ms)
+ stopTimer();
+ startTimer(100);
+ }
+
+ // Reattach compositor and repaint after resizing
+ void attachCompositorAfterResize()
+ {
+ if (!compositorAttached) {
+ // Snap to the correct aspect ratio, considering header visibility
+ auto bounds = getLocalBounds();
+ bool headerVisible = compositor.isHeaderVisible();
+ int aspectHeight =
+ headerVisible ? (baseHeight + headerHeight) : baseHeight;
+ const double aspect = (double)baseWidth / (double)aspectHeight;
+ int w = bounds.getWidth();
+ int h = bounds.getHeight();
+ double currentAspect = (double)w / (double)h;
+
+ if (currentAspect > aspect) {
+ // Too wide, adjust width
+ w = static_cast(h * aspect);
+ } else if (currentAspect < aspect) {
+ // Too tall, adjust height
+ h = static_cast(w / aspect);
+ }
+ setSize(w, h);
+
+ // Set compositor bounds to fill the editor
+ addAndMakeVisible(compositor);
+ compositor.setBounds(getLocalBounds());
+ compositorAttached = true;
+ repaint();
+ }
+ }
+
+ // Capture the current compositor state into an image for smooth resizing
+ void updateCompositorSnapshot()
+ {
+ // Render compositor to an image at its current size
+ if (getWidth() > 0 && getHeight() > 0) {
+ compositorSnapshot =
+ juce::Image(juce::Image::ARGB, getWidth(), getHeight(), true);
+ juce::Graphics g(compositorSnapshot);
+ compositor.paintEntireComponent(g, true);
+ }
+ }
+
+ //==============================================================================
+ // OpenGL initialization
+
+ void setupOpenGLContext()
+ {
+ std::thread([this]() {
+ for (int i = 0; i < 200; ++i) {
+ if (openGLContext.isAttached() &&
+ openGLContext.getRawContext() != nullptr)
+ break;
+ std::this_thread::sleep_for(std::chrono::milliseconds(25));
+ }
+
+ if (!openGLContext.isAttached() ||
+ openGLContext.getRawContext() == nullptr)
+ return;
+
+ openGLContext.executeOnGLThread(
+ [](juce::OpenGLContext&) {
+#if DMT_SUPPRESS_GL_DEBUG_MESSAGES
+ DBG("[AbstractPluginEditor] GL debug message suppression: ENABLED");
+ // Suppress low-priority GL debug messages
+ if (juce::gl::glDebugMessageControl) {
+ juce::gl::glDebugMessageControl(
+ juce::gl::GL_DEBUG_SOURCE_API,
+ juce::gl::GL_DEBUG_TYPE_OTHER,
+ juce::gl::GL_DEBUG_SEVERITY_NOTIFICATION,
+ 0,
+ nullptr,
+ juce::gl::GL_FALSE);
+ }
+#else
+ DBG("[AbstractPluginEditor] GL debug message suppression: DISABLED");
+#endif
+ // Set up callback for GL debug messages
+ if (juce::gl::glDebugMessageCallback)
+ juce::gl::glDebugMessageCallback(juceFilteredGLDebugCallback,
+ nullptr);
+ },
+ true);
+ }).detach();
+ }
+
+ //==============================================================================
+ // OpenGL debug overwrite
+
+ static void KHRONOS_APIENTRY
+ juceFilteredGLDebugCallback(GLenum source,
+ GLenum type,
+ GLuint id,
+ GLenum severity,
+ GLsizei length,
+ const GLchar* message,
+ const void* userParam)
+ {
+ // Ignore low-priority notifications
+ if (severity == juce::gl::GL_DEBUG_SEVERITY_NOTIFICATION)
+ return;
+
+ // Log other messages so we don't lose important info
+ juce::String msg =
+ (message != nullptr) ? juce::String(message) : juce::String();
+ DBG("OpenGL DBG message: " << msg);
+
+ // Keep JUCE's behaviour for serious errors
+ if (type == juce::gl::GL_DEBUG_TYPE_ERROR &&
+ severity == juce::gl::GL_DEBUG_SEVERITY_HIGH)
+ jassertfalse;
+ }
+
+protected:
+ dmt::app::AbstractPluginProcessor& p;
+
+ const int& headerHeight = dmt::Settings::Header::height;
+ const int baseWidth;
+ const int baseHeight;
+ int lastWidth = baseWidth;
+ int lastHeight = baseHeight;
+ double ratio = baseWidth / baseHeight;
+ float& sizeFactor;
+ bool firstDraw = true;
+
+ juce::Image compositorSnapshot;
+ bool compositorAttached = true;
+
+ Image image;
+ bool isResizing = false;
+
+ dmt::gui::window::Layout mainLayout;
+ dmt::gui::window::Compositor compositor;
+
+ OpenGLContext openGLContext;
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AbstractPluginEditor)
+};
+} // namespace app
+} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/app/AbstractPluginProcessor.h b/src/dmt/app/AbstractPluginProcessor.h
index e743115c..ad653104 100644
--- a/src/dmt/app/AbstractPluginProcessor.h
+++ b/src/dmt/app/AbstractPluginProcessor.h
@@ -1,3 +1,169 @@
#pragma once
-#include
\ No newline at end of file
+#include "configuration/Properties.h"
+#include "version/Manager.h"
+#include
+
+namespace dmt {
+namespace app {
+class AbstractPluginProcessor : public juce::AudioProcessor
+{
+public:
+ //==============================================================================
+ AbstractPluginProcessor(
+ std::function
+ createParameterLayout)
+ : AudioProcessor(
+ BusesProperties()
+#if !JucePlugin_IsMidiEffect
+#if !JucePlugin_IsSynth
+ .withInput("Input", juce::AudioChannelSet::stereo(), true)
+#endif
+ .withOutput("Output", juce::AudioChannelSet::stereo(), true)
+#endif
+ )
+ , apvts(*this, nullptr, ProjectInfo::projectName, createParameterLayout())
+ {
+#if PERFETTO
+ MelatoninPerfetto::get().beginSession();
+#endif
+ properties.initialize();
+ }
+
+ //==============================================================================
+ ~AbstractPluginProcessor() override
+ {
+#if PERFETTO
+ MelatoninPerfetto::get().endSession();
+#endif
+ }
+
+ //==============================================================================
+ // Program Management
+
+ double getTailLengthSeconds() const { return 0.0; }
+
+ int getNumPrograms()
+ {
+ return 1; // NB: some hosts don't cope very well if you tell them there are
+ // 0 programs, so this should be at least 1, even if you're not
+ // really implementing programs.
+ }
+
+ int getCurrentProgram() { return 0; }
+
+ void setCurrentProgram(int index) { juce::ignoreUnused(index); }
+
+ const juce::String getProgramName(int index)
+ {
+ juce::ignoreUnused(index);
+ return {};
+ }
+
+ void changeProgramName(int index, const juce::String& newName)
+ {
+ juce::ignoreUnused(index, newName);
+ }
+
+ //==============================================================================
+ // Midi
+
+ bool acceptsMidi() const override
+ {
+#if JucePlugin_WantsMidiInput
+ return true;
+#else
+ return false;
+#endif
+ }
+ bool producesMidi() const override
+ {
+#if JucePlugin_ProducesMidiOutput
+ return true;
+#else
+ return false;
+#endif
+ }
+
+ bool isMidiEffect() const override
+ {
+#if JucePlugin_IsMidiEffect
+ return true;
+#else
+ return false;
+#endif
+ }
+
+ //==============================================================================
+ // Preset Save/Load
+
+ void getStateInformation(juce::MemoryBlock& destData) override
+ {
+ juce::MemoryOutputStream mos(destData, true);
+ apvts.state.writeToStream(mos);
+ }
+
+ void setStateInformation(const void* data, int sizeInBytes) override
+ {
+ auto tree = juce::ValueTree::readFromData(data, sizeInBytes);
+ if (tree.isValid()) {
+ apvts.replaceState(tree);
+ }
+ }
+
+ //==============================================================================
+ //
+
+ bool isBusesLayoutSupported(const BusesLayout& layouts) const
+ {
+#if JucePlugin_IsMidiEffect
+ juce::ignoreUnused(layouts);
+ return true;
+#else
+ // This is the place where you check if the layout is supported.
+ // In this template code we only support mono or stereo.
+ // Some plugin hosts, such as certain GarageBand versions, will only
+ // load plugins that support stereo bus layouts.
+ if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() &&
+ layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
+ return false;
+
+ // This checks if the input layout matches the output layout
+#if !JucePlugin_IsSynth
+ if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
+ return false;
+#endif
+
+ return true;
+#endif
+ }
+
+ bool hasEditor() const
+ {
+ return true; // (change this to false if you choose to not supply an editor)
+ }
+
+public:
+ //==============================================================================
+ juce::AudioProcessorValueTreeState apvts;
+
+ //==============================================================================
+ dmt::configuration::Properties properties;
+ dmt::version::Manager versionManager;
+
+ //==============================================================================
+ float sizeFactor = 1.0f;
+ float getSizeFactor() const { return sizeFactor; }
+ void setSizeFactor(float newSize) { sizeFactor = newSize; }
+
+ //==============================================================================
+private:
+#if PERFETTO
+ std::unique_ptr tracingSession;
+#endif
+
+ //==============================================================================
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AbstractPluginProcessor)
+};
+}
+}
\ No newline at end of file
diff --git a/src/dmt/configuration/Options.h b/src/dmt/configuration/Options.h
index 909d3c98..ca53bef3 100644
--- a/src/dmt/configuration/Options.h
+++ b/src/dmt/configuration/Options.h
@@ -55,12 +55,15 @@ getOptions() noexcept
options.filenameSuffix = ".config";
options.storageFormat = juce::PropertiesFile::storeAsXML;
- if constexpr (OS_IS_WINDOWS) {
+ if (OS_IS_WINDOWS) {
options.folderName = juce::String("Dimethoxy/") + name;
- } else if constexpr (OS_IS_DARWIN) {
+ } else if (OS_IS_DARWIN) {
options.folderName = juce::String("Dimethoxy/") + name;
- } else if constexpr (OS_IS_LINUX) {
+ } else if (OS_IS_LINUX) {
options.folderName = juce::String(".config/Dimethoxy/") + name;
+ } else {
+ // What the hell is the OS?
+ options.folderName = juce::String("Dimethoxy/") + name;
}
options.osxLibrarySubFolder = "Application Support";
diff --git a/src/dmt/configuration/Properties.h b/src/dmt/configuration/Properties.h
index 45f37550..d8ae4bb7 100644
--- a/src/dmt/configuration/Properties.h
+++ b/src/dmt/configuration/Properties.h
@@ -56,6 +56,12 @@ class Properties
using String = juce::String;
public:
+ //============================================================================
+ /**
+ * @brief Construct a new Properties object
+ */
+ Properties() = default;
+
//============================================================================
/**
* @brief Initialize the properties with options and settings.
@@ -203,6 +209,8 @@ class Properties
private:
juce::ApplicationProperties file;
juce::PropertySet fallbackPropertySet;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Properties)
};
} // namespace configuration
} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/dsp/effect/DisfluxProcessor.h b/src/dmt/dsp/effect/DisfluxProcessor.h
index 33ab1884..fd27ff41 100644
--- a/src/dmt/dsp/effect/DisfluxProcessor.h
+++ b/src/dmt/dsp/effect/DisfluxProcessor.h
@@ -317,6 +317,8 @@ class alignas(64) DisfluxProcessor
juce::IIRFilter outputHighpassLeft;
juce::IIRFilter outputHighpassRight;
float lastHighpassFrequency = -1.0f;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DisfluxProcessor)
};
//==============================================================================
diff --git a/src/dmt/dsp/effect/Distortion.h b/src/dmt/dsp/effect/Distortion.h
index 93c16c03..a1fdfdbb 100644
--- a/src/dmt/dsp/effect/Distortion.h
+++ b/src/dmt/dsp/effect/Distortion.h
@@ -354,6 +354,8 @@ struct alignas(64) Distortion
}
}
}
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Distortion)
};
//==============================================================================
diff --git a/src/dmt/dsp/effect/Effect.h b/src/dmt/dsp/effect/Effect.h
index 5224abb6..8942637e 100644
--- a/src/dmt/dsp/effect/Effect.h
+++ b/src/dmt/dsp/effect/Effect.h
@@ -33,5 +33,6 @@
#include "./Distortion.h"
#include "./HeretikProcessor.h"
#include "./LowpassProcessor.h"
+#include "./NeutrinoProcessor.h"
//==============================================================================
\ No newline at end of file
diff --git a/src/dmt/dsp/effect/HeretikProcessor.h b/src/dmt/dsp/effect/HeretikProcessor.h
index f86e3b77..e1d15506 100644
--- a/src/dmt/dsp/effect/HeretikProcessor.h
+++ b/src/dmt/dsp/effect/HeretikProcessor.h
@@ -160,6 +160,8 @@ class alignas(64) HeretikProcessor
std::array feedbackBuffer;
Filter leftFilter;
Filter rightFilter;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HeretikProcessor)
};
//==============================================================================
diff --git a/src/dmt/dsp/effect/LowpassProcessor.h b/src/dmt/dsp/effect/LowpassProcessor.h
index 9e1eccb2..7fe5227d 100644
--- a/src/dmt/dsp/effect/LowpassProcessor.h
+++ b/src/dmt/dsp/effect/LowpassProcessor.h
@@ -262,6 +262,8 @@ class alignas(64) LowpassProcessor
// They do the actual filtering of the audio.
FilterArray leftFilters;
FilterArray rightFilters;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LowpassProcessor)
};
//==============================================================================
diff --git a/src/dmt/gui/panel/AnalogOscillatorPanel.h b/src/dmt/dsp/effect/NeutrinoProcessor.h
similarity index 52%
rename from src/dmt/gui/panel/AnalogOscillatorPanel.h
rename to src/dmt/dsp/effect/NeutrinoProcessor.h
index 1ad9f7df..5acb9fd3 100644
--- a/src/dmt/gui/panel/AnalogOscillatorPanel.h
+++ b/src/dmt/dsp/effect/NeutrinoProcessor.h
@@ -1,56 +1,107 @@
-//==============================================================================
-/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
- * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
- * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
- * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
- * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
- *
- * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
- * External use is permitted but not recommended.
- * No support or compatibility guarantees are provided.
- *
- * License:
- * This code is licensed under the GPLv3 license. You are permitted to use and
- * modify this code under the terms of this license.
- * You must adhere GPLv3 license for any project using this code or parts of it.
- * Your are not allowed to use this code in any closed-source project.
- *
- * Description:
- * AnalogOscillatorPanel is a GUI component that provides controls for an analog
- * oscillator.
- *
- * Authors: Lunix-420 (Primary Author)
- */
-//==============================================================================
-
-#pragma once
-
-//==============================================================================
-
-#include "gui/panel/AbstractPanel.h"
-#include
-
-//==============================================================================
-
-namespace dmt {
-namespace gui {
-namespace panel {
-
-//==============================================================================
-class AnalogOscillatorPanel : public dmt::gui::panel::AbstractPanel
-{
-public:
- AnalogOscillatorPanel(/*juce::AudioProcessorValueTreeState& apvts*/)
- : AbstractPanel("Classic Oscillator")
- { //
- }
-
-private:
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AnalogOscillatorPanel)
-};
-//==============================================================================
-} // namespace panels
-} // namespace gui
-} // namespace dmt
+//==============================================================================
+/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
+ * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
+ * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
+ * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
+ * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
+ *
+ * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
+ * External use is permitted but not recommended.
+ * No support or compatibility guarantees are provided.
+ *
+ * License:
+ * This code is licensed under the GPLv3 license. You are permitted to use and
+ * modify this code under the terms of this license.
+ * You must adhere GPLv3 license for any project using this code or parts of it.
+ * Your are not allowed to use this code in any closed-source project.
+ *
+ * Description:
+ * Neutrino Processor class for processing audio buffers to generate a kick drum
+ * sound.
+ *
+ * Authors:
+ * Lunix-420 (Primary Author)
+ */
+//==============================================================================
+
+#pragma once
+
+//==============================================================================
+
+#include "dsp/synth/NeutrinoSynthVoice.h"
+#include "dsp/synth/SynthSound.h"
+#include
+#include
+
+namespace dmt {
+namespace dsp {
+namespace effect {
+
+//==============================================================================
+
+/**
+ * @brief Neutrino Processor
+ *
+ * This class processes audio buffers to generate a kick drum sound.
+ */
+class alignas(64) NeutrinoProcessor
+{
+ constexpr static float MIN_FREQUENCY = 20.0f;
+ constexpr static float MAX_FREQUENCY = 20000.0f;
+
+ using AudioBuffer = juce::AudioBuffer;
+ using SynthVoice = dmt::dsp::synth::NeutrinoSynthVoice;
+ using SynthSound = dmt::dsp::synth::SynthSound;
+
+public:
+ NeutrinoProcessor(juce::AudioProcessorValueTreeState& _apvts)
+ : apvts(_apvts)
+ {
+ synth.addSound(new SynthSound());
+ synth.addVoice(new SynthVoice(apvts));
+ }
+
+ //==============================================================================
+ /**
+ * @brief Prepares the processor with the given sample rate.
+ *
+ * @param _newSampleRate The sample rate.
+ */
+ inline void prepare(const double _newSampleRate,
+ const int _samplesPerBlock) noexcept
+ {
+ sampleRate = static_cast(_newSampleRate);
+
+ synth.setCurrentPlaybackSampleRate(sampleRate);
+
+ for (int i = 0; i < synth.getNumVoices(); i++) {
+ auto voice = dynamic_cast(synth.getVoice(i));
+ voice->prepareToPlay(sampleRate, _samplesPerBlock, 2);
+ }
+ }
+
+ inline void processBlock(AudioBuffer& _buffer,
+ juce::MidiBuffer& _midiMessages) noexcept
+ {
+ if (sampleRate <= 0.0f) {
+ return;
+ }
+
+ synth.renderNextBlock(_buffer, _midiMessages, 0, _buffer.getNumSamples());
+ }
+
+private:
+ //==============================================================================
+ juce::Synthesiser synth;
+ juce::AudioProcessorValueTreeState& apvts;
+ float sampleRate = -1.0f;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NeutrinoProcessor)
+};
+
+//==============================================================================
+} // namespace effect
+} // namespace dsp
+} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/dsp/envelope/AdhEnvelope.h b/src/dmt/dsp/envelope/AdhEnvelope.h
index 2e184ebf..a862bbe6 100644
--- a/src/dmt/dsp/envelope/AdhEnvelope.h
+++ b/src/dmt/dsp/envelope/AdhEnvelope.h
@@ -50,23 +50,39 @@ class AhdEnvelope
public:
struct Parameters
{
+ bool enabled = true;
float attack = 0.015f;
float hold = 0.08f;
float decay = 0.5f;
-
- float attackSkew = 0;
- float decaySkew = 10;
+ float attackBend = 0;
+ float decayBend = 0;
+ float depth = 1.0f;
};
enum class State
{
+ Disabled,
Attack,
Hold,
Decay,
Idle
};
- constexpr AhdEnvelope() noexcept = default;
+ AhdEnvelope() noexcept = default;
+
+ inline void setParameters(const juce::AudioProcessorValueTreeState& apvts,
+ juce::String prefix) noexcept
+ {
+ juce::String base = prefix + "Env";
+ params.enabled =
+ apvts.getRawParameterValue(base + "Enabled")->load() > 0.5f;
+ params.attack = apvts.getRawParameterValue(base + "Attack")->load();
+ params.hold = apvts.getRawParameterValue(base + "Hold")->load();
+ params.decay = apvts.getRawParameterValue(base + "Decay")->load();
+ params.attackBend = apvts.getRawParameterValue(base + "AttackBend")->load();
+ params.decayBend = apvts.getRawParameterValue(base + "DecayBend")->load();
+ params.depth = apvts.getRawParameterValue(base + "Depth")->load();
+ }
/**
* @brief Set the envelope parameters.
@@ -77,6 +93,13 @@ class AhdEnvelope
params = _newParams;
}
+ /**
+ * @brief Get the Parameters object
+ *
+ * @return The current Parameters object
+ */
+ inline Parameters getParameters() const noexcept { return params; }
+
/**
* @brief Set the sample rate.
* @param _newSampleRate The new sample rate to set.
@@ -97,11 +120,13 @@ class AhdEnvelope
*/
[[nodiscard]] inline State getState() const noexcept
{
- if (sampleIndex < getHoldStart()) [[likely]]
+ if (!params.enabled)
+ return State::Disabled;
+ if (sampleIndex < getHoldStart())
return State::Attack;
- if (sampleIndex < getDecayStart()) [[likely]]
+ if (sampleIndex < getDecayStart())
return State::Hold;
- if (sampleIndex < getDecayEnd()) [[likely]]
+ if (sampleIndex < getDecayEnd())
return State::Decay;
return State::Idle;
}
@@ -130,11 +155,13 @@ class AhdEnvelope
constexpr float zero = 0.0f;
switch (_state) {
+ case State::Disabled:
+ return 0.0f;
case State::Attack: {
const float normalizedPosition =
static_cast(sampleIndex) / sampleRate;
- const float skew = getSkew(State::Attack);
- return std::pow(normalizedPosition / params.attack, skew);
+ const float phaseProgress = normalizedPosition / params.attack;
+ return applyAtanBend(phaseProgress, params.attackBend);
}
case State::Hold:
return one;
@@ -142,8 +169,8 @@ class AhdEnvelope
const float decayStart = static_cast(getDecayStart());
const float normalizedPosition =
(static_cast(sampleIndex) - decayStart) / sampleRate;
- const float skew = getSkew(State::Decay);
- return one - std::pow(normalizedPosition / params.decay, skew);
+ const float phaseProgress = normalizedPosition / params.decay;
+ return one - applyAtanBend(phaseProgress, params.decayBend);
}
default:
return zero;
@@ -151,20 +178,29 @@ class AhdEnvelope
}
/**
- * @brief Get the skew value for the given state.
- * @param _state The current state of the envelope.
- * @return The skew value.
+ * @brief Apply atan-based bend to normalized phase in range [0, 1].
*/
- [[nodiscard]] inline float getSkew(const State _state) const noexcept
+ [[nodiscard]] static inline float applyAtanBend(float normalizedPhase,
+ float bend) noexcept
{
- switch (_state) {
- case State::Attack:
- return dmt::math::linearToExponent(params.attackSkew);
- case State::Decay:
- return dmt::math::linearToExponent(-params.decaySkew);
- default:
- return 1.0f;
- }
+ using juce::jlimit;
+ using std::atan, std::pow, std::abs;
+
+ // We make the bend curve more exponential to make it feel linear
+ const float k = pow(0.5f * abs(bend), 2.0f);
+ const float x = jlimit(0.0f, 1.0f, normalizedPhase);
+ const float normalizer = atan(k);
+
+ // No bend
+ if (k <= 0.01f) [[unlikely]]
+ return x;
+
+ // Positive bend
+ if (bend > 0.0f)
+ return atan(k * x) / normalizer;
+
+ // Negative bend
+ return 1.0f - (atan(k * (1.0f - x)) / normalizer);
}
/**
@@ -199,6 +235,8 @@ class AhdEnvelope
float sampleRate = -1.0f;
Parameters params;
size_t sampleIndex = 0;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AhdEnvelope)
};
//==============================================================================
diff --git a/src/dmt/dsp/synth/AnalogOscillator.h b/src/dmt/dsp/synth/AnalogOscillator.h
deleted file mode 100644
index 6dc04b0b..00000000
--- a/src/dmt/dsp/synth/AnalogOscillator.h
+++ /dev/null
@@ -1,297 +0,0 @@
-//==============================================================================
-/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
- * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
- * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
- * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
- * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
- *
- * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
- * External use is permitted but not recommended.
- * No support or compatibility guarantees are provided.
- *
- * License:
- * This code is licensed under the GPLv3 license. You are permitted to use and
- * modify this code under the terms of this license.
- * You must adhere GPLv3 license for any project using this code or parts of it.
- * Your are not allowed to use this code in any closed-source project.
- *
- * Description:
- * High-performance analog oscillator for real-time audio synthesis.
- *
- * Authors:
- * Lunix-420 (Primary Author)
- */
-//==============================================================================
-
-#pragma once
-
-//==============================================================================
-
-#include "AnalogWaveform.h"
-#include
-
-//==============================================================================
-
-namespace dmt {
-namespace dsp {
-namespace synth {
-
-//==============================================================================
-
-/**
- * @class AnalogOscillator
- * @brief High-performance analog oscillator for real-time audio synthesis.
- *
- * This class is designed for maximum real-time performance, using aggressive
- * optimizations such as constexpr, inline, noexcept, and forceinline. It
- * generates analog waveforms with various modulation capabilities.
- */
-class alignas(64) AnalogOscillator
-{
- using Math = juce::dsp::FastMathApproximations;
- static constexpr float twoPi = juce::MathConstants::twoPi;
- static constexpr float pi = juce::MathConstants::pi;
-
-public:
- //==============================================================================
- /**
- * @brief Sets the sample rate for the oscillator.
- * @param _newSampleRate The new sample rate in Hz.
- */
- inline void setSampleRate(const float _newSampleRate) noexcept
- {
- TRACER("AnalogOscillator::setSampleRate");
- float rangeEnd =
- std::nextafter(392000.0f, std::numeric_limits::max());
- const juce::Range validRange(20.0f, rangeEnd);
- jassert(validRange.contains(_newSampleRate));
- sampleRate = _newSampleRate;
- }
-
- //==============================================================================
- /**
- * @brief Generates the next sample of the waveform.
- * @return The next sample value.
- */
- [[nodiscard]] forcedinline float getNextSample() noexcept
- {
- TRACER("AnalogOscillator::getNextSample");
- if (sampleRate <= 0.0f)
- return 0.0f;
-
- advancePhase();
-
- auto syncedPhase = getSyncedPhase(phase * pwmModifier);
- auto bendedPhase = getBendedPhase(syncedPhase);
-
- if (phase >= twoPi / pwmModifier)
- return 0.0f;
-
- float sample = waveform.getSample(bendedPhase);
- distortSample(sample);
- return std::clamp(sample, -1.0f, +1.0f);
- }
-
- //==============================================================================
- /**
- * @brief Sets the frequency of the oscillator.
- * @param _newFrequency The new frequency in Hz.
- */
- inline void setFrequency(const float _newFrequency) noexcept
- {
- TRACER("AnalogOscillator::setFrequency");
- frequency = _newFrequency;
- }
-
- //==============================================================================
- /**
- * @brief Sets the waveform type of the oscillator.
- * @param _type The new waveform type.
- */
- inline void setWaveformType(
- const dmt::dsp::synth::AnalogWaveform::Type _type) noexcept
- {
- TRACER("AnalogOscillator::setWaveformType");
- waveform.type = _type;
- }
-
- //==============================================================================
- /**
- * @brief Sets the drive level for waveform distortion.
- * @param _newDrive The new drive level.
- */
- inline void setDrive(const float _newDrive) noexcept
- {
- TRACER("AnalogOscillator::setDrive");
- drive = _newDrive;
- }
-
- //==============================================================================
- /**
- * @brief Sets the bias level for waveform distortion.
- * @param _newBias The new bias level.
- */
- inline void setBias(const float _newBias) noexcept
- {
- TRACER("AnalogOscillator::setBias");
- bias = _newBias;
- }
-
- //==============================================================================
- /**
- * @brief Sets the initial phase of the oscillator.
- * @param _newPhase The new phase value.
- */
- inline void setPhase(const float _newPhase) noexcept
- {
- TRACER("AnalogOscillator::setPhase");
- phase = _newPhase;
- }
-
- //==============================================================================
- /**
- * @brief Sets the bend modifier for waveform shaping.
- * @param _newBendModifier The new bend modifier value.
- */
- inline void setBend(const float _newBendModifier) noexcept
- {
- TRACER("AnalogOscillator::setBend");
- float rangeEnd = std::nextafter(100.0f, std::numeric_limits::max());
- const juce::NormalisableRange sourceRange(-100.0f, rangeEnd);
- jassert(sourceRange.getRange().contains(_newBendModifier));
- const auto normalisedValue = sourceRange.convertTo0to1(_newBendModifier);
- const juce::NormalisableRange targetRange(0.1f, 0.9f);
- posityCycleRatio = targetRange.convertFrom0to1(normalisedValue);
- }
-
- //==============================================================================
- /**
- * @brief Sets the PWM (Pulse Width Modulation) modifier.
- * @param _newPwmModifier The new PWM modifier value.
- */
- inline void setPwm(const float _newPwmModifier) noexcept
- {
- TRACER("AnalogOscillator::setPwm");
- float rangeEnd = std::nextafter(100.0f, std::numeric_limits::max());
- const juce::NormalisableRange sourceRange(0.0f, rangeEnd);
- jassert(sourceRange.getRange().contains(_newPwmModifier));
- const auto normalisedValue = sourceRange.convertTo0to1(_newPwmModifier);
- const juce::NormalisableRange targetRange(1.0f, 5.0f);
- pwmModifier = targetRange.convertFrom0to1(normalisedValue);
- }
-
- //==============================================================================
- /**
- * @brief Sets the sync modifier for phase synchronization.
- * @param _newSyncModifier The new sync modifier value.
- */
- inline void setSync(const float _newSyncModifier) noexcept
- {
- TRACER("AnalogOscillator::setSync");
- float rangeEnd = std::nextafter(100.0f, std::numeric_limits::max());
- const juce::NormalisableRange sourceRange(0.0f, rangeEnd);
- jassert(sourceRange.getRange().contains(_newSyncModifier));
- const auto normalisedValue = sourceRange.convertTo0to1(_newSyncModifier);
- const juce::NormalisableRange targetRange(1.0f, 5.0f);
- syncModifier = targetRange.convertFrom0to1(normalisedValue);
- }
-
-private:
- dmt::dsp::synth::AnalogWaveform waveform;
- float frequency = 50.0f;
- float sampleRate = -1.0f;
- float phase = 0.0f;
-
- float drive = 0.0f;
- float bias = 0.0f;
- float pwmModifier = 1.0f;
- float syncModifier = 1.0f;
- float posityCycleRatio = 0.5f;
-
- //==============================================================================
- /**
- * @brief Advances the phase of the oscillator.
- */
- forcedinline void advancePhase() noexcept
- {
- TRACER("AnalogOscillator::advancePhase");
- float cycleLength = sampleRate / frequency;
- float phaseDelta = twoPi / cycleLength;
- phase += phaseDelta;
-
- if (phase >= twoPi) {
- phase -= twoPi;
- }
- }
-
- //==============================================================================
- /**
- * @brief Computes the synced phase based on the raw phase and sync modifier.
- * @param _rawPhase The raw phase value.
- * @return The synced phase value.
- */
- forcedinline float getSyncedPhase(float _rawPhase) const noexcept
- {
- TRACER("AnalogOscillator::getSyncedPhase");
- float syncedPhase = _rawPhase * syncModifier;
- while (syncedPhase >= twoPi) {
- syncedPhase -= twoPi;
- }
- return syncedPhase;
- }
-
- //==============================================================================
- /**
- * @brief Computes the bended phase based on the raw phase and bend modifier.
- * @param _rawPhase The raw phase value.
- * @return The bended phase value.
- */
- forcedinline float getBendedPhase(float _rawPhase) const noexcept
- {
- TRACER("AnalogOscillator::getBendedPhase");
- auto bendedPhase = _rawPhase;
-
- float positiveCycleSize = posityCycleRatio * twoPi;
- float negativeCycleRatio = 1.0f - posityCycleRatio;
- float negativeCycleSize = negativeCycleRatio * twoPi;
-
- if (_rawPhase <= positiveCycleSize) {
- bendedPhase /= (posityCycleRatio * 2.0f);
- }
-
- if (_rawPhase > positiveCycleSize) {
- bendedPhase = (_rawPhase - positiveCycleSize) / negativeCycleSize;
- bendedPhase = bendedPhase * pi + pi;
- }
- return bendedPhase;
- }
-
- //==============================================================================
- /**
- * @brief Applies distortion to the sample based on the drive and bias
- * settings.
- * @param _sample The sample value to be distorted.
- */
- forcedinline void distortSample(float& _sample) const noexcept
- {
- TRACER("AnalogOscillator::distortSample");
- constexpr float magicNumber = 0.7615941559558f;
- if (drive >= 1.0f) {
- _sample = Math::tanh(drive * _sample);
- } else {
- float invertedDrive = 1.0f - drive;
- float wetSample = drive * Math::tanh(_sample);
- float drySample = invertedDrive * _sample * magicNumber;
- _sample = wetSample + drySample;
- }
-
- _sample = _sample + bias;
- }
-};
-
-//==============================================================================
-} // namespace synth
-} // namespace dsp
-} // namespace dmt
diff --git a/src/dmt/dsp/synth/DigitalOscillator.h b/src/dmt/dsp/synth/DigitalOscillator.h
new file mode 100644
index 00000000..7d274e77
--- /dev/null
+++ b/src/dmt/dsp/synth/DigitalOscillator.h
@@ -0,0 +1,362 @@
+//==============================================================================
+/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
+ * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
+ * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
+ * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
+ * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
+ *
+ * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
+ * External use is permitted but not recommended.
+ * No support or compatibility guarantees are provided.
+ *
+ * License:
+ * This code is licensed under the GPLv3 license. You are permitted to use and
+ * modify this code under the terms of this license.
+ * You must adhere GPLv3 license for any project using this code or parts of it.
+ * Your are not allowed to use this code in any closed-source project.
+ *
+ * Description:
+ * High-performance digital oscillator for real-time audio synthesis.
+ *
+ * Authors:
+ * Lunix-420 (Primary Author)
+ */
+//==============================================================================
+
+#pragma once
+
+//==============================================================================
+
+#include "dsp/synth/DigitalWaveform.h"
+#include
+
+//==============================================================================
+
+namespace dmt {
+namespace dsp {
+namespace synth {
+//==============================================================================
+
+/**
+ * @class DigitalOscillator
+ * @brief High-performance digital oscillator for real-time audio synthesis.
+ *
+ * This class is designed for maximum real-time performance, using aggressive
+ * optimizations such as constexpr, inline, noexcept, and forceinline. It
+ * generates digital waveforms with various modulation capabilities.
+ */
+class alignas(64) DigitalOscillator
+{
+ using Math = juce::dsp::FastMathApproximations;
+ using String = juce::String;
+ using DigitalWaveform = dmt::dsp::synth::DigitalWaveform;
+
+ static constexpr float twoPi = juce::MathConstants::twoPi;
+ static constexpr float halfPi = juce::MathConstants::halfPi;
+ static constexpr float pi = juce::MathConstants::pi;
+
+public:
+ struct Parameters
+ {
+ public:
+ DigitalWaveform::Type type = DigitalWaveform::Type::Sine;
+ float bias = 0.0f;
+ float drive = 0.0f;
+ float pwm = 0.0f;
+ float clip = 0.0f;
+ float warp = 0.0f;
+
+ private:
+ float sync = 0.0f;
+ float bend = 0.0f;
+
+ public:
+ //==============================================================================
+ inline float getBend() const noexcept { return bend; }
+ inline void setBend(const float _newBend) noexcept
+ {
+ TRACER("DigitalOscillator::setBend");
+ float rangeEnd =
+ std::nextafter(100.0f, std::numeric_limits::max());
+ const juce::NormalisableRange sourceRange(-100.0f, rangeEnd);
+ jassert(sourceRange.getRange().contains(_newBend));
+ const auto normalisedValue = sourceRange.convertTo0to1(_newBend);
+ const juce::NormalisableRange targetRange(0.1f, 0.9f);
+ bend = targetRange.convertFrom0to1(normalisedValue);
+ }
+
+ //==============================================================================
+ inline float getSync() const noexcept { return sync; }
+ inline void setSync(const float _newSync) noexcept
+ {
+ TRACER("DigitalOscillator::setSync");
+ float rangeEnd =
+ std::nextafter(100.0f, std::numeric_limits::max());
+ const juce::NormalisableRange sourceRange(0.0f, rangeEnd);
+ jassert(sourceRange.getRange().contains(_newSync));
+ const auto normalisedValue = sourceRange.convertTo0to1(_newSync);
+ const juce::NormalisableRange targetRange(1.0f, 5.0f);
+ sync = targetRange.convertFrom0to1(normalisedValue);
+ }
+ };
+
+ //==============================================================================
+ // Oscillator
+
+ //============================================================================
+ DigitalOscillator() = default;
+
+public:
+ inline void setParameters(const juce::AudioProcessorValueTreeState& apvts,
+ String prefix)
+ {
+ auto type = params.type;
+ float warp = params.warp;
+ float bend = params.getBend();
+ float pwm = params.pwm;
+ float sync = params.getSync();
+ float bias = params.bias;
+ float clip = params.clip;
+ float drive = params.drive;
+
+ String base = prefix + "DigitalOscillator";
+ type = static_cast(
+ apvts.getRawParameterValue(base + "Type")->load());
+ bend = apvts.getRawParameterValue(base + "Bend")->load();
+ warp = apvts.getRawParameterValue(base + "Warp")->load();
+ pwm = apvts.getRawParameterValue(base + "Pwm")->load();
+ sync = apvts.getRawParameterValue(base + "Sync")->load();
+ bias = apvts.getRawParameterValue(base + "Bias")->load();
+ clip = apvts.getRawParameterValue(base + "Clip")->load();
+ drive = apvts.getRawParameterValue(base + "Drive")->load();
+
+ // These don't need mapping so we can set them directly
+ waveform.type = type;
+ params.pwm = pwm;
+ params.bias = bias;
+ params.clip = clip;
+ params.drive = drive;
+ params.warp = warp;
+
+ // These need mapping so we use setters to do that
+ params.setBend(bend);
+ params.setSync(sync);
+
+ // Eagerly compute PWM end sample
+ computePwmEndSample();
+ }
+
+ //==============================================================================
+ /**
+ * @brief Sets the sample rate for the oscillator.
+ * @param _newSampleRate The new sample rate in Hz.
+ */
+ inline void setSampleRate(const float _newSampleRate) noexcept
+ {
+ TRACER("DigitalOscillator::setSampleRate");
+ sampleRate = _newSampleRate;
+ computePwmEndSample();
+ }
+
+ //==============================================================================
+ /**
+ * @brief Generates the next sample of the waveform.
+ * @return The next sample value.
+ */
+ [[nodiscard]] forcedinline float getNextSample() noexcept
+ {
+ TRACER("DigitalOscillator::getNextSample");
+
+ using std::pow, std::clamp;
+
+ if (sampleRate <= 0.0f)
+ return 0.0f;
+
+ advancePhase();
+
+ auto warpedPhase = getWarpPhase(phase);
+ auto syncedPhase = getSyncedPhase(warpedPhase);
+ auto bendedPhase = getBendedPhase(syncedPhase);
+
+ auto pwmPhase = bendedPhase;
+ auto pwm = params.pwm;
+ const float pwmNormalized = 1.0f - (pwm / 100.0f); // 0.8
+
+ if (pwmPhase >= twoPi * pwmNormalized) {
+ return pwmEndSample;
+ }
+
+ if (pwmNormalized > 0.0f || pwmNormalized < 1.0f) {
+ pwmPhase = clamp(pwmPhase / pwmNormalized, 0.0f, twoPi);
+ }
+
+ float sample = waveform.getSample(pwmPhase);
+ sample = distortSample(sample);
+ return clamp(sample, -1.0f, +1.0f);
+ }
+
+ //==============================================================================
+ // Experimental phase warp function
+ float getWarpPhase(float _phase) const noexcept
+ {
+ const float k = params.warp;
+ const float normalizedPhase = _phase / pi; // Normalize phase to [0, 2]
+
+ auto b = [k](float x) { return (1.0 + 2.0 * k) * x; };
+ auto c = [k](float x) { return (1.0 - 2.0 * k) * x + 2.0 * k; };
+ auto d = [k](float x) { return (1.0 + 2.0 * k) * x - 4.0 * k; };
+
+ if (normalizedPhase < 0.5) {
+ return b(normalizedPhase) * pi;
+ }
+
+ if (normalizedPhase < 1.5) {
+ return c(normalizedPhase) * pi;
+ }
+
+ return d(normalizedPhase) * pi;
+ }
+
+ //==============================================================================
+ void reset() noexcept
+ {
+ TRACER("DigitalOscillator::reset");
+ phase = 0.0f;
+ computePwmEndSample();
+ }
+
+ //==============================================================================
+ /**
+ * @brief Sets the frequency of the oscillator.
+ * @param _newFrequency The new frequency in Hz.
+ */
+ inline void setFrequency(const float _newFrequency) noexcept
+ {
+ TRACER("DigitalOscillator::setFrequency");
+ frequency = _newFrequency;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Advances the phase of the oscillator.
+ */
+ forcedinline void advancePhase() noexcept
+ {
+ TRACER("DigitalOscillator::advancePhase");
+ float cycleLength = sampleRate / frequency;
+ float phaseDelta = twoPi / cycleLength;
+ phase += phaseDelta;
+
+ if (phase >= twoPi) {
+ phase -= twoPi;
+ }
+ }
+
+ //==============================================================================
+ /**
+ * @brief Computes the synced phase based on the raw phase and sync modifier.
+ * @param _rawPhase The raw phase value.
+ * @return The synced phase value.
+ */
+ forcedinline float getSyncedPhase(float _rawPhase) const noexcept
+ {
+ TRACER("DigitalOscillator::getSyncedPhase");
+ float syncedPhase = _rawPhase * params.getSync();
+ while (syncedPhase >= twoPi) {
+ syncedPhase -= twoPi;
+ }
+ return syncedPhase;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Computes the bended phase based on the raw phase and bend modifier.
+ * @param _rawPhase The raw phase value.
+ * @return The bended phase value.
+ */
+ forcedinline float getBendedPhase(float _rawPhase) const noexcept
+ {
+ TRACER("DigitalOscillator::getBendedPhase");
+ auto bendedPhase = _rawPhase;
+
+ const float bend = params.getBend();
+ float positiveCycleSize = bend * twoPi;
+ float negativeCycleRatio = 1.0f - bend;
+ float negativeCycleSize = negativeCycleRatio * twoPi;
+
+ if (_rawPhase <= positiveCycleSize) {
+ bendedPhase /= (bend * 2.0f);
+ }
+
+ if (_rawPhase > positiveCycleSize) {
+ bendedPhase = (_rawPhase - positiveCycleSize) / negativeCycleSize;
+ bendedPhase = bendedPhase * pi + pi;
+ }
+ return bendedPhase;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Applies distortion to the sample based on the drive and bias
+ * settings.
+ * @param _sample The sample value to be distorted.
+ */
+ forcedinline float distortSample(float _sample) const noexcept
+ {
+ TRACER("DigitalOscillator::distortSample");
+
+ using std::clamp, std::abs, std::atan, std::tan;
+
+ float drive = params.drive;
+ float bias = params.bias;
+ float clip = params.clip + 1.0f;
+ float k = abs(drive);
+
+ float biasSample = _sample + bias;
+ float clipSample = clamp(biasSample * clip, -1.0f, 1.0f);
+
+ if (k < 0.01f)
+ return clipSample;
+
+ if (drive > 0.0f) {
+ float normalizer = atan(k);
+ return atan(k * clipSample) / normalizer;
+ } else {
+ float normalizer = atan(k * 0.2f);
+ return tan(clipSample * normalizer) / (k * 0.2f);
+ }
+ }
+
+private:
+ //==============================================================================
+ /**
+ * @brief Computes the PWM end-of-cycle sample.
+ */
+ void computePwmEndSample() noexcept
+ {
+ TRACER("DigitalOscillator::computePwmEndSample");
+
+ // We sample the waveform at 2Ο to get it's last sample and use it as PWM
+ // fill
+ float endPhase = getBendedPhase(getSyncedPhase(twoPi));
+ pwmEndSample = waveform.getSample(endPhase);
+ distortSample(pwmEndSample);
+ pwmEndSample = std::clamp(pwmEndSample, -1.0f, +1.0f);
+ }
+
+ //==============================================================================
+ DigitalWaveform waveform;
+ Parameters params;
+ float frequency = 50.0f;
+ float sampleRate = -1.0f;
+ float phase = 0.0f;
+ float pwmEndSample = 0.0f;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DigitalOscillator)
+ //==============================================================================
+};
+} // namespace synth
+} // namespace dsp
+} // namespace dmt
diff --git a/src/dmt/dsp/synth/AnalogWaveform.h b/src/dmt/dsp/synth/DigitalWaveform.h
similarity index 94%
rename from src/dmt/dsp/synth/AnalogWaveform.h
rename to src/dmt/dsp/synth/DigitalWaveform.h
index a590cc4d..32253786 100644
--- a/src/dmt/dsp/synth/AnalogWaveform.h
+++ b/src/dmt/dsp/synth/DigitalWaveform.h
@@ -1,153 +1,161 @@
-//==============================================================================
-/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
- * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
- * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
- * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
- * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
- *
- * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
- * External use is permitted but not recommended.
- * No support or compatibility guarantees are provided.
- *
- * License:
- * This code is licensed under the GPLv3 license. You are permitted to use and
- * modify this code under the terms of this license.
- * You must adhere GPLv3 license for any project using this code or parts of it.
- * Your are not allowed to use this code in any closed-source project.
- *
- * Description:
- * Get the options for the properties file with predefined settings.
- *
- * Authors:
- * Lunix-420 (Primary Author)
- */
-//==============================================================================
-
-#pragma once
-
-//==============================================================================
-
-#include
-
-//==============================================================================
-
-namespace dmt {
-namespace dsp {
-namespace synth {
-
-//==============================================================================
-/**
- * @brief Represents different types of analog waveforms.
- */
-struct AnalogWaveform
-{
- static constexpr float twoPi = juce::MathConstants::twoPi;
- static constexpr float pi = juce::MathConstants::pi;
-
- //==============================================================================
- /**
- * @brief Enumeration of waveform types.
- */
- enum class Type
- {
- Sine,
- Saw,
- Triangle,
- Square
- };
-
- static const inline juce::StringArray waveformNames = { "Sine",
- "Saw",
- "Triangle",
- "Square" };
-
- //==============================================================================
-
- Type type = Type::Sine;
-
- //==============================================================================
- /**
- * @brief Generate a triangle waveform sample.
- * @param _x The phase of the waveform.
- * @return The waveform sample.
- */
- inline float triangle(float _x) const noexcept
- {
- while (_x > twoPi)
- _x -= twoPi;
- float result = 2.0f * (_x / twoPi - 0.5f);
- if (result > 0.5f)
- result = 1.0f - result;
- if (result < -0.5f)
- result = -1.0f - result;
- return 2 * result;
- }
-
- //==============================================================================
- /**
- * @brief Generate a saw waveform sample.
- * @param _x The phase of the waveform.
- * @return The waveform sample.
- */
- inline float saw(float _x) const noexcept
- {
- while (_x > twoPi)
- _x -= twoPi;
- return 2.0f * (_x / twoPi - 0.5f);
- }
-
- //==============================================================================
- /**
- * @brief Generate a sine waveform sample.
- * @param _x The phase of the waveform.
- * @return The waveform sample.
- */
- inline float sine(float _x) const noexcept { return std::sin(_x); }
-
- //==============================================================================
- /**
- * @brief Generate a square waveform sample.
- * @param _x The phase of the waveform.
- * @return The waveform sample.
- */
- inline float square(float _x) const noexcept
- {
- return (sine(_x) > 0.0f) ? 1.0f : -1.0f;
- }
-
- //==============================================================================
- /**
- * @brief Get the waveform sample based on the current type.
- * @param _x The phase of the waveform.
- * @return The waveform sample.
- */
- [[nodiscard]] inline float getSample(float _x) const noexcept
- {
- switch (type) {
- case Type::Sine:
- return sine(_x);
- case Type::Saw:
- return saw(_x);
- case Type::Triangle:
- return triangle(_x);
- case Type::Square:
- return square(_x);
- default:
- // impossible to reach this point, exit with assertion
- jassert(false);
- return 0.0f;
- }
- }
-
- //==============================================================================
-};
-
-//==============================================================================
-
-} // namespace synth
-} // namespace dsp
-} // namespace dmt
-
-//==============================================================================
+//==============================================================================
+/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
+ * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
+ * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
+ * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
+ * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
+ *
+ * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
+ * External use is permitted but not recommended.
+ * No support or compatibility guarantees are provided.
+ *
+ * License:
+ * This code is licensed under the GPLv3 license. You are permitted to use and
+ * modify this code under the terms of this license.
+ * You must adhere GPLv3 license for any project using this code or parts of it.
+ * Your are not allowed to use this code in any closed-source project.
+ *
+ * Description:
+ * Get the options for the properties file with predefined settings.
+ *
+ * Authors:
+ * Lunix-420 (Primary Author)
+ */
+//==============================================================================
+
+#pragma once
+
+//==============================================================================
+
+#include
+
+//==============================================================================
+
+namespace dmt {
+namespace dsp {
+namespace synth {
+
+//==============================================================================
+/**
+ * @brief Represents different types of digital waveforms.
+ */
+struct DigitalWaveform
+{
+ static constexpr float twoPi = juce::MathConstants::twoPi;
+ static constexpr float pi = juce::MathConstants::pi;
+
+ //============================================================================
+ /**
+ * @brief Construct a new Digital Waveform object
+ */
+ DigitalWaveform() = default;
+
+ //==============================================================================
+ /**
+ * @brief Enumeration of waveform types.
+ */
+ enum class Type
+ {
+ Sine,
+ Saw,
+ Triangle,
+ Square
+ };
+
+ static const inline juce::StringArray waveformNames = { "Sine",
+ "Saw",
+ "Triangle",
+ "Square" };
+
+ //==============================================================================
+
+ Type type = Type::Sine;
+
+ //==============================================================================
+ /**
+ * @brief Generate a triangle waveform sample.
+ * @param _x The phase of the waveform.
+ * @return The waveform sample.
+ */
+ inline float triangle(float _x) const noexcept
+ {
+ while (_x > twoPi)
+ _x -= twoPi;
+ float result = 2.0f * (_x / twoPi - 0.5f);
+ if (result > 0.5f)
+ result = 1.0f - result;
+ if (result < -0.5f)
+ result = -1.0f - result;
+ return 2 * result;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Generate a saw waveform sample.
+ * @param _x The phase of the waveform.
+ * @return The waveform sample.
+ */
+ inline float saw(float _x) const noexcept
+ {
+ while (_x > twoPi)
+ _x -= twoPi;
+ return 2.0f * (_x / twoPi - 0.5f);
+ }
+
+ //==============================================================================
+ /**
+ * @brief Generate a sine waveform sample.
+ * @param _x The phase of the waveform.
+ * @return The waveform sample.
+ */
+ inline float sine(float _x) const noexcept { return std::sin(_x); }
+
+ //==============================================================================
+ /**
+ * @brief Generate a square waveform sample.
+ * @param _x The phase of the waveform.
+ * @return The waveform sample.
+ */
+ inline float square(float _x) const noexcept
+ {
+ return (sine(_x) > 0.0f) ? 1.0f : -1.0f;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Get the waveform sample based on the current type.
+ * @param _x The phase of the waveform.
+ * @return The waveform sample.
+ */
+ [[nodiscard]] inline float getSample(float _x) const noexcept
+ {
+ switch (type) {
+ case Type::Sine:
+ return sine(_x);
+ case Type::Saw:
+ return saw(_x);
+ case Type::Triangle:
+ return triangle(_x);
+ case Type::Square:
+ return square(_x);
+ default:
+ // impossible to reach this point, exit with assertion
+ jassert(false);
+ return 0.0f;
+ }
+ }
+
+ //==============================================================================
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DigitalWaveform)
+};
+
+//==============================================================================
+
+} // namespace synth
+} // namespace dsp
+} // namespace dmt
+
+//==============================================================================
diff --git a/src/dmt/dsp/synth/SynthVoice.h b/src/dmt/dsp/synth/NeutrinoSynthVoice.h
similarity index 69%
rename from src/dmt/dsp/synth/SynthVoice.h
rename to src/dmt/dsp/synth/NeutrinoSynthVoice.h
index 0be08335..b4b6336f 100644
--- a/src/dmt/dsp/synth/SynthVoice.h
+++ b/src/dmt/dsp/synth/NeutrinoSynthVoice.h
@@ -1,324 +1,319 @@
-//==============================================================================
-/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
- * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
- * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
- * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
- * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
- *
- * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
- * External use is permitted but not recommended.
- * No support or compatibility guarantees are provided.
- *
- * License:
- * This code is licensed under the GPLv3 license. You are permitted to use and
- * modify this code under the terms of this license.
- * You must adhere GPLv3 license for any project using this code or parts of it.
- * Your are not allowed to use this code in any closed-source project.
- *
- * Description:
- * Get the options for the properties file with predefined settings.
- *
- * Authors:
- * Lunix-420 (Primary Author)
- */
-//==============================================================================
-
-#pragma once
-
-//==============================================================================
-
-#include "dsp/envelope/AdhEnvelope.h"
-#include "dsp/synth/AnalogOscillator.h"
-#include
-
-//==============================================================================
-
-namespace dmt {
-namespace dsp {
-namespace synth {
-
-//==============================================================================
-
-/**
- * @class SynthVoice
- * @brief A class representing a synthesizer voice.
- */
-class alignas(64) SynthVoice : public juce::SynthesiserVoice
-{
-public:
- //==============================================================================
- /**
- * @brief Constructor for SynthVoice.
- * @param _apvts Reference to the AudioProcessorValueTreeState.
- */
- SynthVoice(juce::AudioProcessorValueTreeState& _apvts) noexcept
- : apvts(_apvts)
- {
- TRACER("SynthVoice::SynthVoice");
- }
-
- //==============================================================================
- /**
- * @brief Determines if the voice can play a given sound.
- * @param _sound Pointer to the SynthesiserSound.
- * @return True if the sound can be played, false otherwise.
- */
- bool canPlaySound(juce::SynthesiserSound* _sound) override
- {
- TRACER("SynthVoice::canPlaySound");
- return dynamic_cast(_sound) != nullptr;
- }
-
- //==============================================================================
- /**
- * @brief Handles the event when a controller is moved.
- *
- * This function is called when a controller is moved, providing the
- * controller number and the new value of the controller. It is intended to be
- * overridden by derived classes to implement specific behavior for controller
- * movements.
- *
- * @param controllerNumber The number of the controller that was moved.
- * @param newControllerValue The new value of the controller.
- */
- void controllerMoved(int /*controllerNumber*/,
- int /*newControllerValue*/) noexcept override
- {
- TRACER("SynthVoice::controllerMoved");
- }
-
- //==============================================================================
- /**
- * @brief Handles the event when the pitch wheel is moved.
- *
- * This function is called whenever the pitch wheel is moved. The new pitch
- * wheel value is passed as an argument, but it is currently unused.
- *
- * @param newPitchWheelValue The new value of the pitch wheel.
- */
- void pitchWheelMoved(int /*newPitchWheelValue*/) noexcept override
- {
- TRACER("SynthVoice::pitchWheelMoved");
- }
-
- //==============================================================================
- /**
- * @brief Prepares the voice to play.
- * @param _sampleRate The sample rate.
- * @param _samplesPerBlock Number of samples per block.
- * @param _outputChannels Number of output channels.
- */
- void prepareToPlay(double _sampleRate,
- int /*_samplesPerBlock*/,
- int /*_outputChannels*/) noexcept
- {
- TRACER("SynthVoice::prepareToPlay");
- if (_sampleRate <= 0)
- return;
-
- gainEnvelope.setSampleRate(static_cast(_sampleRate));
- pitchEnvelope.setSampleRate(static_cast(_sampleRate));
- osc.setSampleRate(static_cast(_sampleRate));
-
- isPrepared = true;
- }
-
- //==============================================================================
- /**
- * @brief Starts a note.
- * @param _midiNoteNumber The MIDI note number.
- * @param _velocity The velocity of the note.
- * @param _sound Pointer to the SynthesiserSound.
- * @param _currentPitchWheelPosition The current pitch wheel position.
- */
- void startNote(int _midiNoteNumber,
- float /*_velocity*/,
- juce::SynthesiserSound* /*_sound*/,
- int /*_currentPitchWheelPosition*/) noexcept override
- {
- TRACER("SynthVoice::startNote");
- osc.setPhase(0.0f);
- note = _midiNoteNumber;
-
- updateEnvelopeParameters();
- gainEnvelope.noteOn();
- pitchEnvelope.noteOn();
-
- callOnNoteReceivers();
- }
-
- //==============================================================================
- void stopNote(float /*_velocity*/, bool /*_allowTailOff*/) noexcept override
- {
- TRACER("SynthVoice::stopNote");
- }
-
- /**
- * @brief Renders the next block of audio.
- * @param _outputBuffer The output buffer.
- * @param _startSample The start sample index.
- * @param _numSamples The number of samples to render.
- */
- void renderNextBlock(juce::AudioBuffer& _outputBuffer,
- int _startSample,
- int _numSamples) noexcept override
- {
- TRACER("SynthVoice::renderNextBlock");
- if (!isVoiceActive() || !isPrepared)
- return;
-
- updateEnvelopeParameters();
- updateOscillatorParameters();
-
- const float oscGain =
- apvts.getRawParameterValue("osc1DistortionPreGain")->load();
- const int oscOctave = apvts.getRawParameterValue("osc1VoiceOctave")->load();
- const int oscSemitone =
- apvts.getRawParameterValue("osc1VoiceSemitone")->load();
- const float oscModDepth =
- apvts.getRawParameterValue("osc1PitchEnvDepth")->load();
-
- const auto endSample = _numSamples + _startSample;
- auto* leftChannel = _outputBuffer.getWritePointer(0);
- auto* rightChannel = _outputBuffer.getWritePointer(1);
-
- for (int sample = _startSample; sample < endSample; ++sample) {
- osc.setFrequency(getNextFrequency(oscOctave, oscSemitone, oscModDepth));
- const auto rawSample = osc.getNextSample();
- const auto gainedSample = applyGain(rawSample, oscGain);
- leftChannel[sample] = gainedSample;
- rightChannel[sample] = gainedSample;
- }
- }
-
- //==============================================================================
- /**
- * @brief Adds a callback function to be called when a note is received.
- * @param _callbackFunction The callback function.
- */
- void addOnNoteReceivers(std::function _callbackFunction) noexcept
- {
- TRACER("SynthVoice::addOnNoteReceivers");
- onNoteReceivers.push_back(std::move(_callbackFunction));
- }
-
- //==============================================================================
- /**
- * @brief Calls all registered note receiver callback functions.
- */
- void callOnNoteReceivers() noexcept
- {
- TRACER("SynthVoice::callOnNoteReceivers");
- for (const auto& func : onNoteReceivers) {
- func();
- }
- }
-
-protected:
- //==============================================================================
- /**
- * @brief Updates the envelope parameters from the
- * AudioProcessorValueTreeState.
- */
- void updateEnvelopeParameters() noexcept
- {
- TRACER("SynthVoice::updateEnvelopeParameters");
- dmt::dsp::envelope::AhdEnvelope::Parameters gainEnvParameters;
- gainEnvParameters.attack =
- apvts.getRawParameterValue("osc1GainEnvAttack")->load();
- gainEnvParameters.hold =
- apvts.getRawParameterValue("osc1GainEnvHold")->load();
- gainEnvParameters.decay =
- apvts.getRawParameterValue("osc1GainEnvDecay")->load();
- gainEnvParameters.decaySkew =
- apvts.getRawParameterValue("osc1GainEnvSkew")->load();
- gainEnvParameters.attackSkew = 0;
- gainEnvelope.setParameters(gainEnvParameters);
-
- dmt::dsp::envelope::AhdEnvelope::Parameters pitchEnvParameters;
- pitchEnvParameters.attack = 0;
- pitchEnvParameters.hold =
- apvts.getRawParameterValue("osc1PitchEnvHold")->load();
- pitchEnvParameters.decay =
- apvts.getRawParameterValue("osc1PitchEnvDecay")->load();
- pitchEnvParameters.decaySkew =
- apvts.getRawParameterValue("osc1PitchEnvSkew")->load();
- pitchEnvParameters.attackSkew = 0;
- pitchEnvelope.setParameters(pitchEnvParameters);
- }
-
- //==============================================================================
- /**
- * @brief Updates the oscillator parameters from the
- * AudioProcessorValueTreeState.
- */
- void updateOscillatorParameters() noexcept
- {
- TRACER("SynthVoice::updateOscillatorParameters");
- osc.setWaveformType(static_cast(
- apvts.getRawParameterValue("osc1WaveformType")->load()));
- osc.setDrive(apvts.getRawParameterValue("osc1DistortionType")->load());
- osc.setBias(apvts.getRawParameterValue("osc1DistortionSymmetry")->load());
- osc.setBend(apvts.getRawParameterValue("osc1WaveformBend")->load());
- osc.setPwm(apvts.getRawParameterValue("osc1WaveformPwm")->load());
- osc.setSync(apvts.getRawParameterValue("osc1WaveformSync")->load());
- }
-
- //==============================================================================
- /**
- * @brief Calculates the next frequency for the oscillator.
- * @param _rawOctave The raw octave value.
- * @param _rawSemitone The raw semitone value.
- * @param _rawModDepth The raw modulation depth.
- * @return The next frequency.
- */
- [[nodiscard]] float getNextFrequency(const int _rawOctave,
- const int _rawSemitone,
- const float _rawModDepth) noexcept
- {
- TRACER("SynthVoice::getNextFrequency");
- const int octaves = 12 * _rawOctave;
- const int semitone = octaves + _rawSemitone;
- const int baseNote = note + semitone;
- const float baseFreq = juce::MidiMessage::getMidiNoteInHertz(baseNote);
- const float modDepth = _rawModDepth * 2e4f;
- const float envelopeSample = pitchEnvelope.getNextSample();
- const float maxFreq = std::clamp(baseFreq + modDepth, baseFreq, 2e4f);
- const float newFreq = juce::mapToLog10(envelopeSample, baseFreq, maxFreq);
- return std::clamp(newFreq, 20.0f, 2e4f);
- }
-
- //==============================================================================
- /**
- * @brief Applies gain to a sample.
- * @param _sample The input sample.
- * @param _oscGain The oscillator gain.
- * @return The gained sample.
- */
- [[nodiscard]] float applyGain(float _sample, float _oscGain) noexcept
- {
- TRACER("SynthVoice::applyGain");
- const float envGain = gainEnvelope.getNextSample();
- const float gain = juce::Decibels::decibelsToGain(_oscGain, -96.0f);
- return _sample * envGain * gain;
- }
-
-private:
- juce::AudioProcessorValueTreeState& apvts;
- dmt::dsp::synth::AnalogOscillator osc;
- dmt::dsp::envelope::AhdEnvelope gainEnvelope;
- dmt::dsp::envelope::AhdEnvelope pitchEnvelope;
- int note = 0;
- bool isPrepared = false;
- std::vector> onNoteReceivers;
-};
-
-//==============================================================================
-
-} // namespace synth
-} // namespace dsp
-} // namespace dmt
-
-//==============================================================================
+//==============================================================================
+/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
+ * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
+ * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
+ * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
+ * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
+ * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
+ *
+ * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
+ * External use is permitted but not recommended.
+ * No support or compatibility guarantees are provided.
+ *
+ * License:
+ * This code is licensed under the GPLv3 license. You are permitted to use and
+ * modify this code under the terms of this license.
+ * You must adhere GPLv3 license for any project using this code or parts of it.
+ * Your are not allowed to use this code in any closed-source project.
+ *
+ * Description:
+ *
+ *
+ * Authors:
+ * Lunix-420 (Primary Author)
+ */
+//==============================================================================
+
+#pragma once
+
+//==============================================================================
+
+#include "dsp/envelope/AdhEnvelope.h"
+#include "dsp/synth/DigitalOscillator.h"
+#include
+
+//==============================================================================
+
+namespace dmt {
+namespace dsp {
+namespace synth {
+
+//==============================================================================
+
+class alignas(64) NeutrinoSynthVoice : public juce::SynthesiserVoice
+{
+ using DigitalOscillator = dmt::dsp::synth::DigitalOscillator;
+ using DigitalWaveform = dmt::dsp::synth::DigitalWaveform;
+ using AhdEnvelope = dmt::dsp::envelope::AhdEnvelope;
+
+public:
+ //==============================================================================
+ /**
+ * @brief Constructor for SynthVoice.
+ * @param _apvts Reference to the AudioProcessorValueTreeState.
+ */
+ NeutrinoSynthVoice(juce::AudioProcessorValueTreeState& _apvts) noexcept
+ : apvts(_apvts)
+ {
+ TRACER("NeutrinoSynthVoice::NeutrinoSynthVoice");
+ }
+
+ //==============================================================================
+ /**
+ * @brief Determines if the voice can play a given sound.
+ * @param _sound Pointer to the SynthesiserSound.
+ * @return True if the sound can be played, false otherwise.
+ */
+ bool canPlaySound(juce::SynthesiserSound* _sound) override
+ {
+ TRACER("NeutrinoSynthVoice::canPlaySound");
+ return dynamic_cast(_sound) != nullptr;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Handles the event when a controller is moved.
+ *
+ * This function is called when a controller is moved, providing the
+ * controller number and the new value of the controller. It is intended to be
+ * overridden by derived classes to implement specific behavior for controller
+ * movements.
+ *
+ * @param controllerNumber The number of the controller that was moved.
+ * @param newControllerValue The new value of the controller.
+ */
+ void controllerMoved(int /*controllerNumber*/,
+ int /*newControllerValue*/) noexcept override
+ {
+ TRACER("NeutrinoSynthVoice::controllerMoved");
+ }
+
+ //==============================================================================
+ /**
+ * @brief Handles the event when the pitch wheel is moved.
+ *
+ * This function is called whenever the pitch wheel is moved. The new pitch
+ * wheel value is passed as an argument, but it is currently unused.
+ *
+ * @param newPitchWheelValue The new value of the pitch wheel.
+ */
+ void pitchWheelMoved(int /*newPitchWheelValue*/) noexcept override
+ {
+ TRACER("NeutrinoSynthVoice::pitchWheelMoved");
+ }
+
+ //==============================================================================
+ /**
+ * @brief Prepares the voice to play.
+ * @param _sampleRate The sample rate.
+ * @param _samplesPerBlock Number of samples per block.
+ * @param _outputChannels Number of output channels.
+ */
+ void prepareToPlay(double _sampleRate,
+ int /*_samplesPerBlock*/,
+ int /*_outputChannels*/) noexcept
+ {
+ TRACER("NeutrinoSynthVoice::prepareToPlay");
+ if (_sampleRate <= 0)
+ return;
+
+ gainEnvelope.setSampleRate(static_cast(_sampleRate));
+ pitchEnv1.setSampleRate(static_cast(_sampleRate));
+ pitchEnv2.setSampleRate(static_cast(_sampleRate));
+ osc.setSampleRate(static_cast(_sampleRate));
+
+ isPrepared = true;
+ }
+
+ //==============================================================================
+ /**
+ * @brief Starts a note.
+ * @param _midiNoteNumber The MIDI note number.
+ * @param _velocity The velocity of the note.
+ * @param _sound Pointer to the SynthesiserSound.
+ * @param _currentPitchWheelPosition The current pitch wheel position.
+ */
+ void startNote(int _midiNoteNumber,
+ float /*_velocity*/,
+ juce::SynthesiserSound* /*_sound*/,
+ int /*_currentPitchWheelPosition*/) noexcept override
+ {
+ TRACER("NeutrinoSynthVoice::startNote");
+
+ note = _midiNoteNumber;
+
+ updateEnvelopeParameters();
+ gainEnvelope.noteOn();
+ pitchEnv1.noteOn();
+ pitchEnv2.noteOn();
+ osc.reset();
+
+ callOnNoteReceivers();
+ }
+
+ //==============================================================================
+ void stopNote(float /*_velocity*/, bool /*_allowTailOff*/) noexcept override
+ {
+ TRACER("NeutrinoSynthVoice::stopNote");
+ // clearCurrentNote();
+ }
+
+ /**
+ * @brief Renders the next block of audio.
+ * @param _outputBuffer The output buffer.
+ * @param _startSample The start sample index.
+ * @param _numSamples The number of samples to render.
+ */
+ void renderNextBlock(juce::AudioBuffer& _outputBuffer,
+ int _startSample,
+ int _numSamples) noexcept override
+ {
+ TRACER("NeutrinoSynthVoice::renderNextBlock");
+ if (!isVoiceActive() || !isPrepared)
+ return;
+
+ updateEnvelopeParameters();
+ updateOscillatorParameters();
+
+ const float oscGain = 0.0f;
+
+ const auto endSample = _numSamples + _startSample;
+ auto* leftChannel = _outputBuffer.getWritePointer(0);
+ auto* rightChannel = _outputBuffer.getWritePointer(1);
+
+ for (int sample = _startSample; sample < endSample; ++sample) {
+ const float freq = getNextFrequency();
+ osc.setFrequency(freq);
+
+ float rawSample = osc.getNextSample();
+
+ const auto gainedSample = applyGain(rawSample, oscGain);
+ leftChannel[sample] += gainedSample;
+ rightChannel[sample] += gainedSample;
+ }
+ }
+
+ //==============================================================================
+ /**
+ * @brief Calculates the next frequency for the oscillator.
+ * @param _rawOctave The raw octave value.
+ * @param _rawSemitone The raw semitone value.
+ * @param _rawModDepth The raw modulation depth.
+ * @return The next frequency.
+ */
+ [[nodiscard]] float getNextFrequency() noexcept
+ {
+ TRACER("SynthVoice::getNextFrequency");
+ using juce::mapToLog10, juce::MidiMessage;
+ using std::clamp;
+
+ const int octave = -1;
+ const int semitone = 0;
+ const float minFreq = 20.0f;
+ const float maxFreq = 20000.0f;
+ const int baseNote = (note + semitone + (octave * 12));
+ const float baseFreq = MidiMessage::getMidiNoteInHertz(baseNote);
+
+ // How far the envelope should modulate, in 0.0f to 1.0f representing 0% to
+ // 100% of the frequency range.
+ const float oscModDepth1 = pitchEnv1.getParameters().depth;
+ const float oscModDepth2 = pitchEnv2.getParameters().depth;
+
+ // These are 0.0f to +1.0f of the envelope value
+ const float osc1Sample = pitchEnv1.getNextSample();
+ const float osc2Sample = pitchEnv2.getNextSample();
+
+ const float osc1ModSample =
+ mapToLog10(osc1Sample * oscModDepth1, minFreq, maxFreq);
+
+ const float osc2ModSample =
+ mapToLog10(osc2Sample * oscModDepth2, minFreq, maxFreq);
+
+ const float modulatedFreq = baseFreq + osc1ModSample + osc2ModSample;
+ return clamp(modulatedFreq, minFreq, maxFreq);
+ }
+
+ //==============================================================================
+ /**
+ * @brief Adds a callback function to be called when a note is received.
+ * @param _callbackFunction The callback function.
+ */
+ void addOnNoteReceivers(std::function _callbackFunction) noexcept
+ {
+ TRACER("SynthVoice::addOnNoteReceivers");
+ onNoteReceivers.push_back(std::move(_callbackFunction));
+ }
+
+ //==============================================================================
+ /**
+ * @brief Calls all registered note receiver callback functions.
+ */
+ void callOnNoteReceivers() noexcept
+ {
+ TRACER("SynthVoice::callOnNoteReceivers");
+ for (const auto& func : onNoteReceivers) {
+ func();
+ }
+ }
+
+protected:
+ //==============================================================================
+ /**
+ * @brief Updates the envelope parameters from the
+ * AudioProcessorValueTreeState.
+ */
+ void updateEnvelopeParameters() noexcept
+ {
+ TRACER("SynthVoice::updateEnvelopeParameters");
+ gainEnvelope.setParameters(apvts, "NeutrinoGain");
+ pitchEnv1.setParameters(apvts, "NeutrinoPitch1");
+ pitchEnv2.setParameters(apvts, "NeutrinoPitch2");
+ }
+
+ //==============================================================================
+ /**
+ * @brief Updates the oscillator parameters from the
+ * AudioProcessorValueTreeState.
+ */
+ void updateOscillatorParameters() noexcept
+ {
+ TRACER("SynthVoice::updateOscillatorParameters");
+ osc.setParameters(apvts, "Neutrino");
+ }
+
+ //==============================================================================
+ /**
+ * @brief Applies gain to a sample.
+ * @param _sample The input sample.
+ * @param _oscGain The oscillator gain.
+ * @return The gained sample.
+ */
+ [[nodiscard]] float applyGain(float _sample, float _oscGain) noexcept
+ {
+ TRACER("SynthVoice::applyGain");
+ const float envGain = gainEnvelope.getNextSample();
+ const float gain = juce::Decibels::decibelsToGain(_oscGain, -96.0f);
+ return _sample * envGain * gain;
+ }
+
+private:
+ juce::AudioProcessorValueTreeState& apvts;
+ DigitalOscillator osc;
+ AhdEnvelope gainEnvelope;
+ AhdEnvelope pitchEnv1;
+ AhdEnvelope pitchEnv2;
+ int note = 0;
+ bool isPrepared = false;
+ std::vector> onNoteReceivers;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NeutrinoSynthVoice)
+};
+
+//==============================================================================
+
+} // namespace synth
+} // namespace dsp
+} // namespace dmt
+
+//==============================================================================
diff --git a/src/dmt/dsp/synth/Synth.h b/src/dmt/dsp/synth/Synth.h
index 54b42c87..cb03414d 100644
--- a/src/dmt/dsp/synth/Synth.h
+++ b/src/dmt/dsp/synth/Synth.h
@@ -29,9 +29,9 @@
//==============================================================================
-#include "./AnalogOscillator.h"
-#include "./AnalogWaveform.h"
+#include "./DigitalOscillator.h"
+#include "./DigitalWaveform.h"
+#include "./NeutrinoSynthVoice.h"
#include "./SynthSound.h"
-#include "./SynthVoice.h"
//==============================================================================
\ No newline at end of file
diff --git a/src/dmt/dsp/synth/SynthSound.h b/src/dmt/dsp/synth/SynthSound.h
index a9bd8c27..f30d1d74 100644
--- a/src/dmt/dsp/synth/SynthSound.h
+++ b/src/dmt/dsp/synth/SynthSound.h
@@ -45,9 +45,13 @@ namespace synth {
class SynthSound : public juce::SynthesiserSound
{
public:
+ SynthSound() = default;
+
//==============================================================================
bool appliesToNote [[nodiscard]] (int) noexcept override { return true; }
bool appliesToChannel [[nodiscard]] (int) noexcept override { return true; }
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SynthSound)
};
//==============================================================================
diff --git a/src/dmt/gui/component/AbstractSliderComponent.h b/src/dmt/gui/component/AbstractSliderComponent.h
index d9ee4b12..8734ad8d 100644
--- a/src/dmt/gui/component/AbstractSliderComponent.h
+++ b/src/dmt/gui/component/AbstractSliderComponent.h
@@ -138,6 +138,8 @@ class AbstractSliderComponent
Label infoLabel;
Unit::Type unitType;
Fonts fonts;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AbstractSliderComponent)
};
} // namespace component
diff --git a/src/dmt/gui/component/LinearSliderComponent.h b/src/dmt/gui/component/LinearSliderComponent.h
index 8b67b656..35f63633 100644
--- a/src/dmt/gui/component/LinearSliderComponent.h
+++ b/src/dmt/gui/component/LinearSliderComponent.h
@@ -123,6 +123,17 @@ class LinearSliderComponent
};
}
+ /**
+ * @brief
+ *
+ */
+ ~LinearSliderComponent()
+ {
+ TRACER("LinearSliderComponent::~LinearSliderComponent");
+ // Remove slider listener
+ slider.removeListener(this);
+ }
+
/**
* @brief Lays out the child components.
*
diff --git a/src/dmt/gui/component/OscillatorDisplayComponent.h b/src/dmt/gui/component/OscillatorDisplayComponent.h
index 5bc25d84..b0ae96b2 100644
--- a/src/dmt/gui/component/OscillatorDisplayComponent.h
+++ b/src/dmt/gui/component/OscillatorDisplayComponent.h
@@ -1,159 +1,162 @@
-//==============================================================================
-/* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
- * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
- * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
- * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
- * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
- * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
- *
- * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
- * External use is permitted but not recommended.
- * No support or compatibility guarantees are provided.
- *
- * License:
- * This code is licensed under the GPLv3 license. You are permitted to use and
- * modify this code under the terms of this license.
- * You must adhere GPLv3 license for any project using this code or parts of it.
- * Your are not allowed to use this code in any closed-source project.
- *
- * Description:
- * OscillatorDisplayComponent provides a real-time visualization of an
- * oscillator's waveform. It uses a lookup table to efficiently render the
- * waveform based on the current oscillator parameters. The component supports
- * customizable shadows for enhanced visual appeal and is designed to be
- * responsive to parameter changes, updating the display accordingly.
- *
- * Authors:
- * Lunix-420 (Primary Author)
- */
-//==============================================================================
-
-#pragma once
-
-//==============================================================================
-
-#include "dsp/synth/AnalogOscillator.h"
-#include "gui/widget/Shadow.h"
-#include "utility/Settings.h"
-#include
-
-//==============================================================================
-namespace dmt {
-namespace gui {
-namespace component {
-//==============================================================================
-// TODO: Make this use the new display system
-class OscillatorDisplayComponent
- : public juce::Component
- , public juce::Timer
- , public dmt::Scaleable
-{
- using Shadow = dmt::gui::widget::Shadow;
- using AnalogOscillator = dmt::dsp::synth::AnalogOscillator;
-
- //============================================================================
- // General
- using Settings = dmt::Settings::OscillatorDisplay;
- const int& fps = dmt::Settings::framerate;
- const int& resolution = Settings::resolution;
-
- // Shadows
- const bool& drawOuterShadow = Settings::drawOuterShadow;
- const bool& drawInnerShadow = Settings::drawInnerShadow;
- const juce::Colour& outerShadowColour = Settings::outerShadowColour;
- const juce::Colour& innerShadowColour = Settings::innerShadowColour;
- const float& outerShadowRadius = Settings::outerShadowRadius;
- const float& innerShadowRadius = Settings::innerShadowRadius;
-
-public:
- //============================================================================
- OscillatorDisplayComponent(juce::AudioProcessorValueTreeState& apvts)
- : apvts(apvts)
- {
- TRACER("OscillatorDisplayComponent::OscillatorDisplayComponent");
- osc.setSampleRate((float)resolution + 1.0f);
- osc.setFrequency(1.0f);
- startTimerHz(60);
- }
- //============================================================================
- void paint(juce::Graphics& g) override
- {
- TRACER("OscillatorDisplayComponent::paint");
- const auto bounds = this->getLocalBounds().toFloat();
- }
-
-protected:
- //==============================================================================
- void timerCallback()
- {
- TRACER("OscillatorDisplayComponent::timerCallback");
- if (isParametersChanged()) {
- this->buildTable();
- this->repaint();
- }
- }
-
- void buildTable()
- {
- TRACER("OscillatorDisplayComponent::buildTable");
- osc.setPhase(0.0f);
- table.initialise(
- [&](std::size_t index) { return (float)osc.getNextSample(); },
- resolution);
- }
-
- bool isParametersChanged()
- {
- TRACER("OscillatorDisplayComponent::isParametersChanged");
- }
-
- //==============================================================================
- juce::Path getPath(juce::Rectangle bounds)
- {
- TRACER("OscillatorDisplayComponent::getPath");
- bounds.setY(bounds.getY() + (bounds.getHeight() / 10.0f));
- bounds.setHeight(bounds.getHeight() - (bounds.getHeight() / 5.0f));
-
- auto outerBounds = bounds;
- bounds = bounds.reduced(bounds.getWidth() / 6.0f);
-
- juce::Path path;
-
- auto startX = bounds.getX();
- auto startY = bounds.getY() + (bounds.getHeight() / 2.0f);
- juce::Point start(startX, startY);
- path.startNewSubPath(start);
-
- auto width = bounds.getWidth();
-
- for (size_t i = 0; i < width; i++) {
- auto x = bounds.getX() + i;
- auto y = bounds.getY() + (bounds.getHeight() / 2.0f) -
- (table[i / width * resolution] * bounds.getHeight() / 2.0f);
- juce::Point p(x, y);
- path.lineTo(p);
- }
-
- auto endX = bounds.getX() + bounds.getWidth();
- auto endY = bounds.getY() + (bounds.getHeight() / 2.0f);
- juce::Point end(endX, endY);
- path.lineTo(end);
-
- return path;
- }
- //============================================================================
-private:
- Shadow outerShadow =
- Shadow(drawOuterShadow, outerShadowColour, outerShadowRadius, false);
- Shadow innerShadow =
- Shadow(drawInnerShadow, innerShadowColour, innerShadowRadius, true);
-
- AnalogOscillator osc;
- juce::dsp::LookupTable table;
- juce::AudioProcessorValueTreeState& apvts;
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscillatorDisplayComponent)
-};
-} // namespace components
-} // namespace gui
-} // namespace dmt
\ No newline at end of file
+// //==============================================================================
+// /* βββββββ βββββββ ββββββββββββββββββββββββ βββ βββββββ βββ ββββββ βββ
+// * ββββββββββββββββ βββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββ
+// * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββββ
+// * βββ βββββββββββββββββββββββ βββ βββββββββββ βββ ββββββ βββββ
+// * ββββββββββββββ βββ βββββββββββ βββ βββ ββββββββββββββββ βββ βββ
+// * βββββββ ββββββ βββββββββββ βββ βββ βββ βββββββ βββ βββ βββ
+// * Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
+// *
+// * Part of the Dimethoxy Library, primarily intended for Dimethoxy plugins.
+// * External use is permitted but not recommended.
+// * No support or compatibility guarantees are provided.
+// *
+// * License:
+// * This code is licensed under the GPLv3 license. You are permitted to use
+// and
+// * modify this code under the terms of this license.
+// * You must adhere GPLv3 license for any project using this code or parts of
+// it.
+// * Your are not allowed to use this code in any closed-source project.
+// *
+// * Description:
+// * OscillatorDisplayComponent provides a real-time visualization of an
+// * oscillator's waveform. It uses a lookup table to efficiently render the
+// * waveform based on the current oscillator parameters. The component
+// supports
+// * customizable shadows for enhanced visual appeal and is designed to be
+// * responsive to parameter changes, updating the display accordingly.
+// *
+// * Authors:
+// * Lunix-420 (Primary Author)
+// */
+// //==============================================================================
+
+// #pragma once
+
+// //==============================================================================
+
+// #include "dsp/synth/AnalogOscillator.h"
+// #include "gui/widget/Shadow.h"
+// #include "utility/Settings.h"
+// #include
+
+// //==============================================================================
+// namespace dmt {
+// namespace gui {
+// namespace component {
+// //==============================================================================
+// // TODO: Make this use the new display system
+// class OscillatorDisplayComponent
+// : public juce::Component
+// , public juce::Timer
+// , public dmt::Scaleable
+// {
+// using Shadow = dmt::gui::widget::Shadow;
+// using AnalogOscillator = dmt::dsp::synth::AnalogOscillator;
+
+// //============================================================================
+// // General
+// using Settings = dmt::Settings::OscillatorDisplay;
+// const int& fps = dmt::Settings::framerate;
+// const int& resolution = Settings::resolution;
+
+// // Shadows
+// const bool& drawOuterShadow = Settings::drawOuterShadow;
+// const bool& drawInnerShadow = Settings::drawInnerShadow;
+// const juce::Colour& outerShadowColour = Settings::outerShadowColour;
+// const juce::Colour& innerShadowColour = Settings::innerShadowColour;
+// const float& outerShadowRadius = Settings::outerShadowRadius;
+// const float& innerShadowRadius = Settings::innerShadowRadius;
+
+// public:
+// //============================================================================
+// OscillatorDisplayComponent(juce::AudioProcessorValueTreeState& apvts)
+// : apvts(apvts)
+// {
+// TRACER("OscillatorDisplayComponent::OscillatorDisplayComponent");
+// osc.setSampleRate((float)resolution + 1.0f);
+// osc.setFrequency(1.0f);
+// startTimerHz(60);
+// }
+// //============================================================================
+// void paint(juce::Graphics& g) override
+// {
+// TRACER("OscillatorDisplayComponent::paint");
+// const auto bounds = this->getLocalBounds().toFloat();
+// }
+
+// protected:
+// //==============================================================================
+// void timerCallback()
+// {
+// TRACER("OscillatorDisplayComponent::timerCallback");
+// if (isParametersChanged()) {
+// this->buildTable();
+// this->repaint();
+// }
+// }
+
+// void buildTable()
+// {
+// TRACER("OscillatorDisplayComponent::buildTable");
+// osc.setPhase(0.0f);
+// table.initialise(
+// [&](std::size_t index) { return (float)osc.getNextSample(); },
+// resolution);
+// }
+
+// bool isParametersChanged()
+// {
+// TRACER("OscillatorDisplayComponent::isParametersChanged");
+// }
+
+// //==============================================================================
+// juce::Path getPath(juce::Rectangle bounds)
+// {
+// TRACER("OscillatorDisplayComponent::getPath");
+// bounds.setY(bounds.getY() + (bounds.getHeight() / 10.0f));
+// bounds.setHeight(bounds.getHeight() - (bounds.getHeight() / 5.0f));
+
+// auto outerBounds = bounds;
+// bounds = bounds.reduced(bounds.getWidth() / 6.0f);
+
+// juce::Path path;
+
+// auto startX = bounds.getX();
+// auto startY = bounds.getY() + (bounds.getHeight() / 2.0f);
+// juce::Point start(startX, startY);
+// path.startNewSubPath(start);
+
+// auto width = bounds.getWidth();
+
+// for (size_t i = 0; i < width; i++) {
+// auto x = bounds.getX() + i;
+// auto y = bounds.getY() + (bounds.getHeight() / 2.0f) -
+// (table[i / width * resolution] * bounds.getHeight() / 2.0f);
+// juce::Point p(x, y);
+// path.lineTo(p);
+// }
+
+// auto endX = bounds.getX() + bounds.getWidth();
+// auto endY = bounds.getY() + (bounds.getHeight() / 2.0f);
+// juce::Point end(endX, endY);
+// path.lineTo(end);
+
+// return path;
+// }
+// //============================================================================
+// private:
+// Shadow outerShadow =
+// Shadow(drawOuterShadow, outerShadowColour, outerShadowRadius, false);
+// Shadow innerShadow =
+// Shadow(drawInnerShadow, innerShadowColour, innerShadowRadius, true);
+
+// AnalogOscillator osc;
+// juce::dsp::LookupTable table;
+// juce::AudioProcessorValueTreeState& apvts;
+// JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscillatorDisplayComponent)
+// };
+// } // namespace components
+// } // namespace gui
+// } // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/gui/component/RotarySliderComponent.h b/src/dmt/gui/component/RotarySliderComponent.h
index 538e490f..4f6ecd52 100644
--- a/src/dmt/gui/component/RotarySliderComponent.h
+++ b/src/dmt/gui/component/RotarySliderComponent.h
@@ -110,6 +110,16 @@ class RotarySliderComponent
};
}
+ /**
+ * @brief Destructor for `RotarySliderComponent`.
+ */
+ ~RotarySliderComponent()
+ {
+ TRACER("RotarySliderComponent::~RotarySliderComponent");
+ // Remove slider listener
+ slider.removeListener(this);
+ }
+
/**
* @brief Handles component resizing and lays out child widgets.
*
diff --git a/src/dmt/gui/component/SettingsEditorComponent.h b/src/dmt/gui/component/SettingsEditorComponent.h
index 7f4f164b..a7bbe607 100644
--- a/src/dmt/gui/component/SettingsEditorComponent.h
+++ b/src/dmt/gui/component/SettingsEditorComponent.h
@@ -130,7 +130,7 @@ class SettingsEditor
void onCategorySelectedCallback(TreeAdapter::Category& category)
{
TRACER("SettingsEditor::onCategorySelectedCallback");
- std::cout << "Selected category: " << category.name << std::endl;
+
valueEditorList.setCategory(category);
valueEditorList.setOptimalSize(editorViewport.getWidth());
}
diff --git a/src/dmt/gui/display/AbstractDisplay.h b/src/dmt/gui/display/AbstractDisplay.h
index 46f868eb..0aa88115 100644
--- a/src/dmt/gui/display/AbstractDisplay.h
+++ b/src/dmt/gui/display/AbstractDisplay.h
@@ -67,6 +67,8 @@ class AbstractDisplay
// General
using Display = dmt::Settings::Display;
const juce::Colour& backgroundColour = Display::backgroundColour;
+ const juce::Colour& displayForegroundColour =
+ dmt::Settings::Panel::backgroundColour;
// Layout
const float& rawCornerSize = Display::cornerSize;
@@ -104,6 +106,11 @@ class AbstractDisplay
addAndMakeVisible(innerShadow);
}
+ //============================================================================
+ /** @brief Destructor for `AbstractDisplay`.
+ */
+ inline ~AbstractDisplay() { stopRepaintTimer(); }
+
//==============================================================================
/**
* @brief Paints the component, including background, border, and display
@@ -121,26 +128,22 @@ class AbstractDisplay
// Precalculation
const auto borderStrength = rawBorderStrength * size;
const auto cornerSize = rawCornerSize * size;
+ const auto padding = rawPadding * size;
const float outerCornerSize = cornerSize;
const float innerCornerSize = std::clamp(
outerCornerSize - (borderStrength * 0.5f), 0.0f, outerCornerSize);
- // Draw background if border is disabled
- if (!drawBorder) {
- _g.setColour(backgroundColour);
- _g.fillRoundedRectangle(outerBounds.toFloat(), outerCornerSize);
- }
+ // Draw background
+ _g.setColour(backgroundColour);
+ _g.fillRoundedRectangle(innerBounds.toFloat(), innerCornerSize);
- // Draw background and border if border is enabled
- if (drawBorder) {
- _g.setColour(borderColour);
- _g.fillRoundedRectangle(outerBounds.toFloat(), outerCornerSize);
- _g.setColour(backgroundColour);
- _g.fillRoundedRectangle(innerBounds.toFloat(), innerCornerSize);
- }
// Draw display
paintDisplay(_g, innerBounds);
+ // Draw outer background to hide display overdraw
+ _g.setColour(displayForegroundColour);
+ _g.drawRect(getLocalBounds().toFloat(), padding);
+
// We need to draw the border again because drawing it once didn't cut it
if (drawBorder) {
_g.setColour(borderColour);
diff --git a/src/dmt/gui/display/OscilloscopeDisplay.h b/src/dmt/gui/display/OscilloscopeDisplay.h
index bf6866e4..3b7c00de 100644
--- a/src/dmt/gui/display/OscilloscopeDisplay.h
+++ b/src/dmt/gui/display/OscilloscopeDisplay.h
@@ -84,16 +84,17 @@ class OscilloscopeDisplay
OscilloscopeDisplay(FifoAudioBuffer& _fifoBuffer,
AudioProcessorValueTreeState& _apvts,
bool _useDefaultSettings = false)
- : ringBuffer(2, 4096)
+ : apvts(_apvts)
+ , ringBuffer(2, 4096)
, fifoBuffer(_fifoBuffer)
, leftOscilloscope(ringBuffer, 0, size)
, rightOscilloscope(ringBuffer, 1, size)
, useDefaultSettings(_useDefaultSettings)
{
if (!useDefaultSettings) {
- _apvts.addParameterListener("OscilloscopeZoom", this);
- _apvts.addParameterListener("OscilloscopeThickness", this);
- _apvts.addParameterListener("OscilloscopeGain", this);
+ apvts.addParameterListener("OscilloscopeZoom", this);
+ apvts.addParameterListener("OscilloscopeThickness", this);
+ apvts.addParameterListener("OscilloscopeGain", this);
} else {
// Use default values from dmt::Settings::Oscilloscope
setZoom(dmt::Settings::Oscilloscope::defaultZoom);
@@ -101,6 +102,21 @@ class OscilloscopeDisplay
setHeight(dmt::Settings::Oscilloscope::defaultGain);
}
}
+
+ ~OscilloscopeDisplay() override
+ {
+ // Remove parameter listeners
+ if (!useDefaultSettings) {
+ // Assuming you have access to the AudioProcessorValueTreeState instance
+ // here, you would remove the listeners. This is just a placeholder.
+ apvts.removeParameterListener("OscilloscopeZoom", this);
+ apvts.removeParameterListener("OscilloscopeThickness", this);
+ apvts.removeParameterListener("OscilloscopeGain", this);
+ }
+
+ // Stop the repaint timer
+ stopRepaintTimer();
+ }
//==============================================================================
void extendResized(
const juce::Rectangle& _displayBounds) noexcept override
@@ -160,10 +176,10 @@ class OscilloscopeDisplay
rightScopeBounds.getHeight());
// Draw oscilloscope images
- g.drawImageAt(leftOscilloscope.getImage(),
+ g.drawImageAt(leftOscilloscope.getFrontImage(),
leftOscilloscope.getBounds().getX(),
leftOscilloscope.getBounds().getY());
- g.drawImageAt(rightOscilloscope.getImage(),
+ g.drawImageAt(rightOscilloscope.getFrontImage(),
rightOscilloscope.getBounds().getX(),
rightOscilloscope.getBounds().getY());
}
@@ -256,11 +272,13 @@ class OscilloscopeDisplay
}
//==============================================================================
private:
+ AudioProcessorValueTreeState& apvts;
RingAudioBuffer ringBuffer;
FifoAudioBuffer& fifoBuffer;
Oscilloscope leftOscilloscope;
Oscilloscope rightOscilloscope;
bool useDefaultSettings;
+
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscilloscopeDisplay)
diff --git a/src/dmt/gui/panel/AbstractPanel.h b/src/dmt/gui/panel/AbstractPanel.h
index e18aab3d..14193cf5 100644
--- a/src/dmt/gui/panel/AbstractPanel.h
+++ b/src/dmt/gui/panel/AbstractPanel.h
@@ -142,6 +142,19 @@ class AbstractPanel
addAndMakeVisible(innerShadow);
}
+ //==============================================================================
+ /**
+ * @brief Destructor for `AbstractPanel`.
+ */
+ ~AbstractPanel() override
+ {
+ TRACER("AbstractPanel::~AbstractPanel");
+
+ // Remove listeners
+ nextButton.removeListener(this);
+ prevButton.removeListener(this);
+ }
+
//==============================================================================
/**
* @brief Paints the panel, including background, border, and debug overlays.
diff --git a/src/dmt/gui/panel/AnalogGainPanel.h b/src/dmt/gui/panel/AnalogGainPanel.h
index 7f0b556b..2ba41662 100644
--- a/src/dmt/gui/panel/AnalogGainPanel.h
+++ b/src/dmt/gui/panel/AnalogGainPanel.h
@@ -70,10 +70,10 @@ class AnalogGainPanel : public dmt::gui::panel::AbstractPanel
Unit::Type::Milliseconds,
LinearSliderType::Positive,
LinearSliderOrientation::Vertical)
- , skewSlider(apvts,
- juce::String("Skew"),
- juce::String("osc1GainEnvSkew"),
- Unit::Type::EnvelopeSkew,
+ , bendSlider(apvts,
+ juce::String("Bend"),
+ juce::String("osc1GainEnvBend"),
+ Unit::Type::EnvelopeBend,
LinearSliderType::Positive,
LinearSliderOrientation::Vertical)
{
@@ -82,7 +82,7 @@ class AnalogGainPanel : public dmt::gui::panel::AbstractPanel
addAndMakeVisible(attackSlider);
addAndMakeVisible(holdSlider);
addAndMakeVisible(decaySlider);
- addAndMakeVisible(skewSlider);
+ addAndMakeVisible(bendSlider);
}
void extendResize() noexcept override
@@ -96,7 +96,7 @@ class AnalogGainPanel : public dmt::gui::panel::AbstractPanel
const int attackCol = 7;
const int holdCol = 11;
const int decayCol = 15;
- const int skewCol = 19;
+ const int bendCol = 19;
const auto attackSliderPrimaryPoint =
this->getGridPoint(bounds, attackCol, primaryRow);
@@ -119,19 +119,19 @@ class AnalogGainPanel : public dmt::gui::panel::AbstractPanel
decaySlider.setBoundsByPoints(decaySliderPrimaryPoint,
decaySliderSecondaryPoint);
- const auto skewSliderPrimaryPoint =
- this->getGridPoint(bounds, skewCol, primaryRow);
- const auto skewSliderSecondaryPoint =
- this->getGridPoint(bounds, skewCol, secundaryRow);
- skewSlider.setBoundsByPoints(skewSliderPrimaryPoint,
- skewSliderSecondaryPoint);
+ const auto bendSliderPrimaryPoint =
+ this->getGridPoint(bounds, bendCol, primaryRow);
+ const auto bendSliderSecondaryPoint =
+ this->getGridPoint(bounds, bendCol, secundaryRow);
+ bendSlider.setBoundsByPoints(bendSliderPrimaryPoint,
+ bendSliderSecondaryPoint);
}
private:
LinearSliderComponent attackSlider;
LinearSliderComponent holdSlider;
LinearSliderComponent decaySlider;
- LinearSliderComponent skewSlider;
+ LinearSliderComponent bendSlider;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AnalogGainPanel)
};
diff --git a/src/dmt/gui/panel/AnalogPitchPanel.h b/src/dmt/gui/panel/AnalogPitchPanel.h
index 22228368..dd3eb7b6 100644
--- a/src/dmt/gui/panel/AnalogPitchPanel.h
+++ b/src/dmt/gui/panel/AnalogPitchPanel.h
@@ -56,30 +56,30 @@ class AnalogPitchPanel : public dmt::gui::panel::AbstractPanel
Unit::Type::Milliseconds,
LinearSliderType::Positive,
LinearSliderOrientation::Vertical)
+ , depthSlider(apvts,
+ juce::String("Depth"),
+ juce::String("osc1PitchEnvDepth"),
+ Unit::Type::Frequency,
+ LinearSliderType::Positive,
+ LinearSliderOrientation::Vertical)
, decaySlider(apvts,
juce::String("Decay"),
juce::String("osc1PitchEnvDecay"),
Unit::Type::Milliseconds,
LinearSliderType::Positive,
LinearSliderOrientation::Vertical)
- , skewSlider(apvts,
- juce::String("Skew"),
- juce::String("osc1PitchEnvSkew"),
- Unit::Type::EnvelopeSkew,
+ , bendSlider(apvts,
+ juce::String("Bend"),
+ juce::String("osc1PitchEnvBend"),
+ Unit::Type::EnvelopeBend,
LinearSliderType::Positive,
LinearSliderOrientation::Vertical)
- , depthSlider(apvts,
- juce::String("Depth"),
- juce::String("osc1PitchEnvDepth"),
- Unit::Type::Frequency,
- LinearSliderType::Positive,
- LinearSliderOrientation::Vertical)
{
setLayout({ 25, 32 });
addAndMakeVisible(attackSlider);
addAndMakeVisible(depthSlider);
addAndMakeVisible(decaySlider);
- addAndMakeVisible(skewSlider);
+ addAndMakeVisible(bendSlider);
}
void extendResize() noexcept override
@@ -91,7 +91,7 @@ class AnalogPitchPanel : public dmt::gui::panel::AbstractPanel
const int attackCol = 7;
const int decayCol = 11;
- const int skewCol = 15;
+ const int bendCol = 15;
const int depthCol = 19;
const auto attackSliderPrimaryPoint =
@@ -108,12 +108,12 @@ class AnalogPitchPanel : public dmt::gui::panel::AbstractPanel
decaySlider.setBoundsByPoints(decaySliderPrimaryPoint,
decaySliderSecondaryPoint);
- const auto skewSliderPrimaryPoint =
- this->getGridPoint(bounds, skewCol, primaryRow);
- const auto skewSliderSecondaryPoint =
- this->getGridPoint(bounds, skewCol, secundaryRow);
- skewSlider.setBoundsByPoints(skewSliderPrimaryPoint,
- skewSliderSecondaryPoint);
+ const auto bendSliderPrimaryPoint =
+ this->getGridPoint(bounds, bendCol, primaryRow);
+ const auto bendSliderSecondaryPoint =
+ this->getGridPoint(bounds, bendCol, secundaryRow);
+ bendSlider.setBoundsByPoints(bendSliderPrimaryPoint,
+ bendSliderSecondaryPoint);
const auto depthSliderPrimaryPoint =
this->getGridPoint(bounds, depthCol, primaryRow);
@@ -127,7 +127,7 @@ class AnalogPitchPanel : public dmt::gui::panel::AbstractPanel
LinearSliderComponent attackSlider;
LinearSliderComponent depthSlider;
LinearSliderComponent decaySlider;
- LinearSliderComponent skewSlider;
+ LinearSliderComponent bendSlider;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AnalogPitchPanel)
};
//==============================================================================
diff --git a/src/dmt/gui/panel/OscillatorPanel.h b/src/dmt/gui/panel/OscillatorPanel.h
index fd844409..a4ece0b1 100644
--- a/src/dmt/gui/panel/OscillatorPanel.h
+++ b/src/dmt/gui/panel/OscillatorPanel.h
@@ -31,7 +31,6 @@
//==============================================================================
#include "gui/panel/AbstractPanel.h"
-#include "gui/panel/AnalogOscillatorPanel.h"
#include "gui/panel/Carousel.h"
#include "gui/panel/ModernOscillatorPanel.h"
#include
@@ -49,7 +48,6 @@ class OscillatorPanel : public dmt::gui::panel::Carousel
OscillatorPanel()
: Carousel()
{
- panels.push_back(std::make_unique());
panels.push_back(std::make_unique());
init();
}
diff --git a/src/dmt/gui/panel/Panel.h b/src/dmt/gui/panel/Panel.h
index 894599be..204288ff 100644
--- a/src/dmt/gui/panel/Panel.h
+++ b/src/dmt/gui/panel/Panel.h
@@ -31,7 +31,6 @@
#include "./AbstractPanel.h"
#include "./AnalogGainPanel.h"
-#include "./AnalogOscillatorPanel.h"
#include "./AnalogPitchPanel.h"
#include "./Carousel.h"
#include "./DisfluxPanel.h"
diff --git a/src/dmt/gui/preset/FolderManager.h b/src/dmt/gui/preset/FolderManager.h
index 29076b95..ea4a8fb4 100644
--- a/src/dmt/gui/preset/FolderManager.h
+++ b/src/dmt/gui/preset/FolderManager.h
@@ -64,11 +64,15 @@ class FolderManager : juce::ValueTree::Listener
valueTreeState.state.getPropertyAsValue(folderNameProperty, nullptr));
}
+ ~FolderManager() override { valueTreeState.state.removeListener(this); }
+
private:
//==============================================================================
juce::AudioProcessorValueTreeState& valueTreeState;
juce::StringArray folderList;
juce::Value currentFolder;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FolderManager)
};
} // namespace preset
} // namespace gui
diff --git a/src/dmt/gui/preset/PresetManager.h b/src/dmt/gui/preset/PresetManager.h
index 1548b800..758a4829 100644
--- a/src/dmt/gui/preset/PresetManager.h
+++ b/src/dmt/gui/preset/PresetManager.h
@@ -182,6 +182,8 @@ class PresetManager : juce::ValueTree::Listener
//==============================================================================
juce::AudioProcessorValueTreeState& valueTreeState;
juce::Value currentPreset;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PresetManager)
};
} // namespace preset
} // namespace gui
diff --git a/src/dmt/gui/widget/AbstractButton.h b/src/dmt/gui/widget/AbstractButton.h
index af2071f5..365f12bb 100644
--- a/src/dmt/gui/widget/AbstractButton.h
+++ b/src/dmt/gui/widget/AbstractButton.h
@@ -140,6 +140,13 @@ class AbstractButton
hoverIconImageComponent.setVisible(false);
addMouseListener(this, true);
+
+ // Reorder the components
+ innerShadow.toBack();
+ outerShadow.toBack();
+ clickedBackgroundImageComponent.toBack();
+ hoverBackgroundImageComponent.toBack();
+ backgroundImageComponent.toBack();
}
//==============================================================================
@@ -164,8 +171,19 @@ class AbstractButton
TRACER("AbstractButton::resized");
auto bounds = getLocalBounds();
const auto buttonPadding = rawButtonPadding * size;
- auto innerBounds = bounds.reduced(buttonPadding);
+ const auto innerBounds = bounds.reduced(buttonPadding);
const auto cornerRadius = rawCornerRadius * size;
+ const auto currentScale = static_cast(scale);
+
+ if (innerBounds == lastInnerBounds &&
+ juce::approximatelyEqual(cornerRadius, lastCornerRadius) &&
+ juce::approximatelyEqual(currentScale, lastScaleFactor)) {
+ return;
+ }
+
+ lastInnerBounds = innerBounds;
+ lastCornerRadius = cornerRadius;
+ lastScaleFactor = currentScale;
setShadowBounds(innerBounds, cornerRadius);
setBackgroundBounds(innerBounds);
@@ -292,13 +310,6 @@ class AbstractButton
innerShadowPath.addRoundedRectangle(_innerBounds, _cornerRadius);
innerShadow.setPath(innerShadowPath);
innerShadow.setBoundsRelative(0.0f, 0.0f, 1.0f, 1.0f);
-
- // Reorder the components
- innerShadow.toBack();
- outerShadow.toBack();
- clickedBackgroundImageComponent.toBack();
- hoverBackgroundImageComponent.toBack();
- backgroundImageComponent.toBack();
}
//==============================================================================
@@ -472,6 +483,9 @@ class AbstractButton
ImageComponent iconImageComponent;
Image hoverIconImage;
ImageComponent hoverIconImageComponent;
+ juce::Rectangle lastInnerBounds;
+ float lastCornerRadius = -1.0f;
+ float lastScaleFactor = -1.0f;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AbstractButton)
diff --git a/src/dmt/gui/widget/BorderButton.h b/src/dmt/gui/widget/BorderButton.h
index 175e1e96..3865c5ed 100644
--- a/src/dmt/gui/widget/BorderButton.h
+++ b/src/dmt/gui/widget/BorderButton.h
@@ -101,7 +101,12 @@ class BorderButton
/**
* @brief Destructor for BorderButton.
*/
- ~BorderButton() override = default;
+ ~BorderButton() override
+ {
+ TRACER("BorderButton::~BorderButton");
+ // Stop the repaint timer to clean up resources
+ stopRepaintTimer();
+ }
//==============================================================================
/**
diff --git a/src/dmt/gui/widget/MinMaxRenderer.h b/src/dmt/gui/widget/MinMaxRenderer.h
index 92f40485..459f1e06 100644
--- a/src/dmt/gui/widget/MinMaxRenderer.h
+++ b/src/dmt/gui/widget/MinMaxRenderer.h
@@ -67,6 +67,10 @@ namespace widget {
template
class MinMaxRenderer : public OscilloscopeRenderer
{
+ //============================================================================
+public:
+ MinMaxRenderer() = default;
+
//============================================================================
public:
using RingBuffer = typename OscilloscopeRenderer::RingBuffer;
@@ -258,6 +262,8 @@ class MinMaxRenderer : public OscilloscopeRenderer
return path;
}
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MinMaxRenderer)
};
} // namespace widget
diff --git a/src/dmt/gui/widget/Oscilloscope.h b/src/dmt/gui/widget/Oscilloscope.h
index 37c8d02a..eb50d02e 100644
--- a/src/dmt/gui/widget/Oscilloscope.h
+++ b/src/dmt/gui/widget/Oscilloscope.h
@@ -33,7 +33,6 @@
#include "gui/widget/MinMaxRenderer.h"
#include "gui/widget/PathStrokeRenderer.h"
#include
-#include
//==============================================================================
@@ -52,14 +51,14 @@ namespace widget {
* buffers, optimized for real-time use in GUI applications. It leverages a
* background thread to render the waveform into a JUCE image, which can be
* efficiently displayed in the GUI. The design ensures thread safety and
- * minimal locking overhead, using a read-write lock for image access.
+ * minimal overhead using lock-free double buffering for image access.
*
* The oscilloscope is intended to be used with a lock-free ring buffer for
* audio data, and supports customization of amplitude, thickness, and
* samples-per-pixel for flexible display scaling.
*
* The rendering thread is started upon construction and stopped on destruction.
- * The image is updated periodically, and can be retrieved via getImage().
+ * The image is updated periodically, and can be retrieved via getFrontImage().
*/
template
class alignas(64) Oscilloscope : public juce::Thread
@@ -72,7 +71,6 @@ class alignas(64) Oscilloscope : public juce::Thread
using String = juce::String;
using Thread = juce::Thread;
using PixelFormat = juce::Image::PixelFormat;
- using ReadWriteLock = juce::ReadWriteLock;
using Settings = dmt::Settings;
using Renderer = OscilloscopeRenderer;
@@ -94,7 +92,7 @@ class alignas(64) Oscilloscope : public juce::Thread
, ringBuffer(_ringBuffer)
, channel(_channel)
, size(_sizeFactor)
- , renderer(std::make_unique>())
+ , renderer(std::make_shared>())
{
startThread();
}
@@ -110,17 +108,18 @@ class alignas(64) Oscilloscope : public juce::Thread
//==============================================================================
/**
- * @brief Retrieves a copy of the current oscilloscope image.
+ * @brief Retrieves the current front image for GUI rendering.
*
- * @return A copy of the rendered JUCE image.
+ * @return A reference to the front buffer image.
*
* @details
- * The returned image is thread-safe and can be used in the GUI.
+ * The render thread only writes to the back buffer, so the front image is
+ * safe for concurrent GUI reads.
*/
- [[nodiscard]] inline juce::Image getImage() const
+ [[nodiscard]] inline const juce::Image& getFrontImage() const noexcept
{
- const ScopedReadLock readLock(imageLock);
- return image.createCopy();
+ const int frontIndex = frontBufferIndex.load(std::memory_order_acquire);
+ return images[static_cast(frontIndex)];
}
//==============================================================================
@@ -134,8 +133,11 @@ class alignas(64) Oscilloscope : public juce::Thread
*/
inline void setBounds(juce::Rectangle _newBounds)
{
- resizeImage(_newBounds.getWidth(), _newBounds.getHeight());
bounds = _newBounds;
+
+ renderWidth.store(_newBounds.getWidth(), std::memory_order_relaxed);
+ renderHeight.store(_newBounds.getHeight(), std::memory_order_relaxed);
+ resizePending.store(true, std::memory_order_release);
}
//==============================================================================
@@ -160,7 +162,7 @@ class alignas(64) Oscilloscope : public juce::Thread
*/
inline void setRawSamplesPerPixel(float _newRawSamplesPerPixel) noexcept
{
- rawSamplesPerPixel = _newRawSamplesPerPixel;
+ rawSamplesPerPixel.store(_newRawSamplesPerPixel, std::memory_order_relaxed);
}
//==============================================================================
@@ -174,7 +176,7 @@ class alignas(64) Oscilloscope : public juce::Thread
*/
inline void setAmplitude(float _newAmplitude) noexcept
{
- amplitude = _newAmplitude;
+ amplitude.store(_newAmplitude, std::memory_order_relaxed);
}
//==============================================================================
@@ -188,7 +190,7 @@ class alignas(64) Oscilloscope : public juce::Thread
*/
inline void setThickness(float _newThickness) noexcept
{
- thickness = _newThickness;
+ thickness.store(_newThickness, std::memory_order_relaxed);
}
//==============================================================================
@@ -198,14 +200,16 @@ class alignas(64) Oscilloscope : public juce::Thread
* @param _newRenderer A unique pointer to the new renderer implementation.
*
* @details
- * Swaps the current rendering strategy under the write lock to ensure
- * thread safety with the rendering thread. The previous renderer is
- * destroyed when the new one is set.
+ * Swaps the current rendering strategy atomically to avoid contention with
+ * the rendering thread. The previous renderer is destroyed when the new one
+ * is set.
*/
inline void setRenderer(std::unique_ptr _newRenderer)
{
- const ScopedWriteLock writeLock(imageLock);
- renderer = std::move(_newRenderer);
+ std::atomic_store_explicit(
+ &renderer,
+ std::shared_ptr(std::move(_newRenderer)),
+ std::memory_order_release);
}
//==============================================================================
@@ -216,7 +220,7 @@ class alignas(64) Oscilloscope : public juce::Thread
*
* @details
* Periodically updates the oscilloscope image by rendering the latest audio
- * samples. Uses a write lock to ensure exclusive access to the image.
+ * samples. Uses a back buffer to avoid GUI contention.
* The wait interval is set high to minimize CPU usage; rendering is not
* continuous but event-driven.
*/
@@ -224,7 +228,12 @@ class alignas(64) Oscilloscope : public juce::Thread
{
while (!threadShouldExit()) {
wait(10000);
- const ScopedWriteLock writeLock(imageLock);
+
+ if (resizePending.exchange(false, std::memory_order_acq_rel)) {
+ resizeImage(renderWidth.load(std::memory_order_relaxed),
+ renderHeight.load(std::memory_order_relaxed));
+ }
+
render();
}
}
@@ -242,23 +251,35 @@ class alignas(64) Oscilloscope : public juce::Thread
inline void resizeImage(const int _width, const int _height)
{
TRACER("Oscilloscope::resizeImage");
- const ScopedWriteLock writeLock(imageLock);
// Avoid illegal sizes
if (_width <= 0 || _height <= 0) {
return;
}
- image = Image(PixelFormat::ARGB, _width + 10, _height, true);
+#if OS_IS_WINDOWS
+ for (auto& image : images) {
+ image = Image(
+ PixelFormat::ARGB, _width + 10, _height, true, juce::OpenGLImageType());
+ }
+#else
+ for (auto& image : images) {
+ image = Image(PixelFormat::ARGB, _width, _height, true);
+ }
+#endif
+
+ frontBufferIndex.store(0, std::memory_order_release);
subPixelOffset = 0.0f;
- juce::Graphics imageGraphics(image);
- imageGraphics.setColour(juce::Colours::white);
- imageGraphics.drawLine(0,
- static_cast(_height) / 2.0f,
- static_cast(_width + 10),
- static_cast(_height) / 2.0f,
- 3.0f);
+ for (auto& image : images) {
+ juce::Graphics imageGraphics(image);
+ imageGraphics.setColour(juce::Colours::white);
+ imageGraphics.drawLine(0,
+ static_cast(_height) / 2.0f,
+ static_cast(_width + 10),
+ static_cast(_height) / 2.0f,
+ 3.0f);
+ }
}
//==============================================================================
@@ -273,54 +294,85 @@ class alignas(64) Oscilloscope : public juce::Thread
inline void render()
{
TRACER("Oscilloscope::render");
- const int width = bounds.getWidth();
- const int height = bounds.getHeight();
+
+ const int width = renderWidth.load(std::memory_order_relaxed);
+ const int height = renderHeight.load(std::memory_order_relaxed);
+
+ if (width <= 0 || height <= 0)
+ return;
+
const int halfHeight = height / 2;
- float samplesPerPixel = rawSamplesPerPixel * size;
+
+ const float samplesPerPixel =
+ rawSamplesPerPixel.load(std::memory_order_relaxed) * size;
+
+ if (samplesPerPixel <= 0.0f)
+ return;
const int bufferSize = ringBuffer.getNumSamples();
const int readPosition = ringBuffer.getReadPosition(channel);
const int samplesToRead = bufferSize - readPosition;
const int maxSamplesToDraw =
- static_cast(std::floor(samplesPerPixel * static_cast(width)));
+ (int)std::floor(samplesPerPixel * (float)width);
+
const int samplesToDraw = jmin(samplesToRead, maxSamplesToDraw);
const int firstSamplesToDraw = readPosition;
- const float exactPixelsToDraw =
- static_cast(samplesToDraw) / samplesPerPixel;
+ const float exactPixelsToDraw = (float)samplesToDraw / samplesPerPixel;
+
const float totalShift = exactPixelsToDraw + subPixelOffset;
- const int pixelToDraw = static_cast(totalShift);
+ const int pixelToDraw = (int)totalShift;
+
+ if (pixelToDraw <= 0)
+ return;
+
ringBuffer.incrementReadPosition(channel, samplesToDraw);
- // Image move
- const int destX = 0 - pixelToDraw;
- image.moveImageSection(destX, // destX
- 0, // destY
- 0, // srcX
- 0, // srcY
- width + 10, // width
- height); // height
-
- // Clear the new part of the image
- juce::Rectangle clearRect(
- width - pixelToDraw + 10, 0, pixelToDraw, height);
- image.clear(clearRect, juce::Colours::transparentBlack);
-
- // Delegate drawing to the active renderer
- juce::Graphics imageGraphics(image);
+ const int currentFront = frontBufferIndex.load(std::memory_order_acquire);
+ const int backIndex = currentFront == 0 ? 1 : 0;
+
+ auto& backImage = images[(size_t)backIndex];
+
+ backImage = images[(size_t)currentFront].createCopy();
+
+ // Left scrolling 2.0: Hopefully without crashing host DAWs
+ const int shift = jmin(pixelToDraw, width);
+ if (shift > 0) {
+ // Move content LEFT safely inside bounds
+ // backImage.moveImageSection(0,
+ // 0, // destX, destY
+ // shift,
+ // 0, // sourceX, sourceY
+ // width - shift,
+ // height);
+
+ backImage.clear(juce::Rectangle(width - shift, 0, shift, height),
+ juce::Colours::transparentBlack);
+ }
+
+ // Render new audio data
+ juce::Graphics g(backImage);
+
const typename Renderer::RenderContext context{
firstSamplesToDraw,
samplesToDraw,
- static_cast(width - pixelToDraw) + subPixelOffset,
+ (float)(width - shift) + subPixelOffset,
1.0f / samplesPerPixel,
halfHeight,
- amplitude,
- thickness,
+ amplitude.load(std::memory_order_relaxed),
+ thickness.load(std::memory_order_relaxed),
size
};
- subPixelOffset = totalShift - static_cast(pixelToDraw);
- renderer->draw(imageGraphics, ringBuffer, channel, context);
+
+ subPixelOffset = totalShift - (float)pixelToDraw;
+
+ if (auto currentRenderer =
+ std::atomic_load_explicit(&renderer, std::memory_order_acquire)) {
+ currentRenderer->draw(g, ringBuffer, channel, context);
+ }
+
+ frontBufferIndex.store(backIndex, std::memory_order_release);
}
//==============================================================================
@@ -333,14 +385,18 @@ class alignas(64) Oscilloscope : public juce::Thread
//==============================================================================
// Other members
juce::Rectangle bounds = juce::Rectangle(0, 0, 1, 1);
- Image image = Image(PixelFormat::ARGB, 1, 1, true);
- ReadWriteLock imageLock;
-
- std::unique_ptr renderer;
+ std::array images = { Image(PixelFormat::ARGB, 1, 1, true),
+ Image(PixelFormat::ARGB, 1, 1, true) };
+ std::atomic frontBufferIndex{ 0 };
+ std::atomic renderWidth{ 1 };
+ std::atomic renderHeight{ 1 };
+ std::atomic resizePending{ true };
+
+ std::shared_ptr renderer;
float subPixelOffset = 0.0f;
- float rawSamplesPerPixel = 10.0f;
- float amplitude = 1.0f;
- float thickness = 3.0f;
+ std::atomic rawSamplesPerPixel{ 10.0f };
+ std::atomic amplitude{ 1.0f };
+ std::atomic thickness{ 3.0f };
const float& size;
//==============================================================================
diff --git a/src/dmt/gui/widget/OscilloscopeRenderer.h b/src/dmt/gui/widget/OscilloscopeRenderer.h
index 11cf9341..2db1da00 100644
--- a/src/dmt/gui/widget/OscilloscopeRenderer.h
+++ b/src/dmt/gui/widget/OscilloscopeRenderer.h
@@ -106,6 +106,11 @@ class OscilloscopeRenderer
/** Global size scaling factor. */
float sizeFactor;
};
+ //============================================================================
+ /**
+ * @brief Default constructor for OscilloscopeRenderer.
+ */
+ OscilloscopeRenderer() = default;
//============================================================================
/**
@@ -184,6 +189,8 @@ class OscilloscopeRenderer
/** Current X position with sub-pixel precision for visual continuity. */
float currentX = 0.0f;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OscilloscopeRenderer)
};
} // namespace widget
diff --git a/src/dmt/gui/widget/PathStrokeRenderer.h b/src/dmt/gui/widget/PathStrokeRenderer.h
index 0f41570a..6decb29c 100644
--- a/src/dmt/gui/widget/PathStrokeRenderer.h
+++ b/src/dmt/gui/widget/PathStrokeRenderer.h
@@ -130,6 +130,8 @@ class PathStrokeRenderer : public OscilloscopeRenderer
return path;
}
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PathStrokeRenderer)
};
} // namespace widget
diff --git a/src/dmt/gui/widget/Shadow.h b/src/dmt/gui/widget/Shadow.h
index 53aff869..af7752de 100644
--- a/src/dmt/gui/widget/Shadow.h
+++ b/src/dmt/gui/widget/Shadow.h
@@ -33,6 +33,7 @@
#include "utility/Scaleable.h"
#include "utility/Settings.h"
#include
+#include
//==============================================================================
@@ -228,15 +229,8 @@ class Shadow
inline void drawInnerForPath(juce::Graphics& _g, juce::Path _target)
{
TRACER("Shadow::drawInnerForPath");
- juce::Graphics::ScopedSaveState saveState(_g);
- juce::Path shadowPath(_target);
- shadowPath.addRectangle(_target.getBounds().expanded(10 * scale));
- shadowPath.setUsingNonZeroWinding(false);
- _g.reduceClipRegion(_target);
- juce::DropShadow ds(*colour,
- static_cast(radius * size * scale),
- offset * scale); // dereference pointer
- ds.drawForPath(_g, shadowPath);
+ updateShadowParameters();
+ innerShadowRenderer.render(_g, _target);
}
//==============================================================================
@@ -255,16 +249,34 @@ class Shadow
TRACER("Shadow::drawOuterForPath");
juce::Graphics::ScopedSaveState saveState(_g);
juce::Path shadowPath(_target);
- shadowPath.addRectangle(_target.getBounds().expanded(10 * scale));
+ shadowPath.addRectangle(_target.getBounds().expanded(10.0f));
shadowPath.setUsingNonZeroWinding(false);
_g.reduceClipRegion(shadowPath);
- juce::DropShadow ds(*colour,
- static_cast(radius * size * scale),
- offset * scale); // dereference pointer
- ds.drawForPath(_g, _target);
+ updateShadowParameters();
+ outerShadowRenderer.render(_g, _target);
}
private:
+ inline void updateShadowParameters()
+ {
+ // Rendering uses a graphics transform for HiDPI, so keep shadow parameters
+ // in logical units to avoid applying scale twice.
+ const auto scaledRadius = static_cast(radius * size);
+ const auto scaledOffset = juce::Point(static_cast(offset.x),
+ static_cast(offset.y));
+
+ if (inner) {
+ innerShadowRenderer.setColor(*colour)
+ .setRadius(scaledRadius)
+ .setOffset(scaledOffset);
+ return;
+ }
+
+ outerShadowRenderer.setColor(*colour)
+ .setRadius(scaledRadius)
+ .setOffset(scaledOffset);
+ }
+
inline void refreshCachedImageIfNeeded(bool forceRepaint = false)
{
if (getWidth() == 0 || getHeight() == 0)
@@ -303,6 +315,9 @@ class Shadow
bool needsRepaint = true;
float lastRenderedScale = 0.0f;
+ melatonin::DropShadow outerShadowRenderer;
+ melatonin::InnerShadow innerShadowRenderer;
+
Image image = Image(PixelFormat::ARGB, 1, 1, true);
//==============================================================================
diff --git a/src/dmt/gui/window/Alerts.h b/src/dmt/gui/window/Alerts.h
index 44505048..120c9726 100644
--- a/src/dmt/gui/window/Alerts.h
+++ b/src/dmt/gui/window/Alerts.h
@@ -128,6 +128,8 @@ class Alerts
const bool& drawInnerShadow = AlertSettings::drawInnerShadow;
public:
+ // Track last repaint time for frame-independent timing
+ double lastRepaintTimeMs = juce::Time::getMillisecondCounterHiRes();
//==============================================================================
/**
* @brief Alert type enumeration.
@@ -174,6 +176,12 @@ class Alerts
startRepaintTimer();
}
+ //==============================================================================
+ /**
+ * @brief Destructor for `Alerts`.
+ */
+ inline ~Alerts() { stopRepaintTimer(); }
+
//==============================================================================
/**
* @brief Pushes a new alert to the overlay.
@@ -284,8 +292,11 @@ class Alerts
inline void repaintTimerCallback() noexcept override
{
TRACER("Alerts::repaintTimerCallback");
+ double nowMs = juce::Time::getMillisecondCounterHiRes();
+ double elapsedSec = (nowMs - lastRepaintTimeMs) * 0.001;
+ lastRepaintTimeMs = nowMs;
for (int i = static_cast(alerts.size()); --i >= 0;) {
- alerts.getReference(i).age += Settings::framerate / 1000.0f;
+ alerts.getReference(i).age += static_cast(elapsedSec);
if (alerts.getReference(i).age >= maxAge)
alerts.remove(i);
}
diff --git a/src/dmt/gui/window/Compositor.h b/src/dmt/gui/window/Compositor.h
index 20eb5be7..e5e5320e 100644
--- a/src/dmt/gui/window/Compositor.h
+++ b/src/dmt/gui/window/Compositor.h
@@ -159,7 +159,14 @@ class Compositor
//============================================================================
/** @brief Destructor for `Compositor`. */
- ~Compositor() noexcept override { removeListenerRecursively(this); }
+ ~Compositor() noexcept override
+ {
+ // Stop timer FIRST to prevent callbacks during destruction
+ stopTimer();
+
+ // Remove all component listeners to prevent callbacks from child components
+ removeListenerRecursively(this);
+ }
//============================================================================
/** @brief Paints the component. */
@@ -178,6 +185,7 @@ class Compositor
void resized() noexcept override
{
TRACER("Compositor::resized");
+ propagateSizeFactor();
const auto bounds = getLocalBounds();
// Alerts
@@ -408,6 +416,8 @@ class Compositor
TRACER("Compositor::resetSettingsCallback");
properties.resetToFallback();
+ propagateSizeFactor();
+
// Gotta call this first to recalculate the global size
const auto& parent = getParentComponent();
if (parent != nullptr) {
@@ -462,6 +472,7 @@ class Compositor
void valueEditorListenerCallback() override
{
TRACER("Compositor::valueEditorListenerCallback");
+ propagateSizeFactor();
resizedRecursively(this);
}
@@ -566,12 +577,26 @@ class Compositor
* components that implement the IScaleable interface receive the updated
* size factor.
*
+ * @param force If true, propagation runs even when the size factor itself
+ * did not change. This is required for dynamically added
+ * scaleable children.
+ *
* @note This is typically triggered by the Compositor in response to
* hierarchy changes or user scaling actions.
*/
- void propagateSizeFactor() noexcept
+ void propagateSizeFactor(const bool force = false) noexcept
{
TRACER("Compositor::propagateSizeFactor");
+ if (isPropagatingSizeFactor)
+ return;
+
+ if (!force &&
+ juce::approximatelyEqual(lastPropagatedSizeFactor, sizeFactor))
+ return;
+
+ const juce::ScopedValueSetter isPropagatingGuard(
+ isPropagatingSizeFactor, true);
+ lastPropagatedSizeFactor = sizeFactor;
setSizeFactorRecursively(this);
}
@@ -656,6 +681,7 @@ class Compositor
{
if (!c)
return;
+ c->removeComponentListener(this);
c->addComponentListener(this);
for (auto* child : c->getChildren())
if (auto* cc = dynamic_cast(child))
@@ -737,8 +763,11 @@ class Compositor
*/
void componentChildrenChanged(juce::Component& component) override
{
+ if (isPropagatingSizeFactor)
+ return;
+
addListenerToChildren(&component);
- propagateSizeFactor();
+ propagateSizeFactor(true);
}
private:
@@ -759,6 +788,8 @@ class Compositor
int baseHeight = 0;
int baseWidth = 0;
const float& sizeFactor;
+ float lastPropagatedSizeFactor = std::numeric_limits::quiet_NaN();
+ bool isPropagatingSizeFactor = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Compositor)
diff --git a/src/dmt/gui/window/Layout.h b/src/dmt/gui/window/Layout.h
index a72773e2..359b54a9 100644
--- a/src/dmt/gui/window/Layout.h
+++ b/src/dmt/gui/window/Layout.h
@@ -134,6 +134,8 @@ class Layout : public juce::Component
PanelSpanList panelSpans;
GridSeparatorLayout columnSeparators;
GridSeparatorLayout rowSeparators;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Layout)
};
} // namespace window
diff --git a/src/dmt/gui/window/Tooltip.h b/src/dmt/gui/window/Tooltip.h
index bd9c6d48..f90556bc 100644
--- a/src/dmt/gui/window/Tooltip.h
+++ b/src/dmt/gui/window/Tooltip.h
@@ -110,7 +110,7 @@ class Tooltip
/**
* @brief Destructor.
*/
- inline ~Tooltip() override = default;
+ inline ~Tooltip() { stopRepaintTimer(); }
//==============================================================================
/**
diff --git a/src/dmt/model/AhdEnvelopeParameters.h b/src/dmt/model/AhdEnvelopeParameters.h
index 5a055083..21f6e2bd 100644
--- a/src/dmt/model/AhdEnvelopeParameters.h
+++ b/src/dmt/model/AhdEnvelopeParameters.h
@@ -1,61 +1,73 @@
-#pragma once
-
-#include
-
-//==============================================================================
-namespace dmt {
-namespace model {
-static inline juce::AudioProcessorParameterGroup
-envelopeParameterGroup(juce::String parentUid,
- juce::String suffix,
- std::array defaultValues)
-{
- using ParameterInt = juce::AudioParameterInt;
- using ParameterFloat = juce::AudioParameterFloat;
- using ParameterChoice = juce::AudioParameterChoice;
- using NormalisableRange = juce::NormalisableRange;
-
- juce::String uid = parentUid + suffix + "Env";
-
- return juce::AudioProcessorParameterGroup(
- uid, // group ID
- suffix + "Envelope", // group name
- "|", // separator
- std::make_unique(uid + "Attack", // parameter ID
- "Attack", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 0.3f, // rangeEnd
- 0.001f, // intervalValue
- 0.5f), // skewFactor
- defaultValues[0]), // defaultValue
- std::make_unique(uid + "Hold", // parameter ID
- "Hold", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 0.3f, // rangeEnd
- 0.001f, // intervalValue
- 0.5f), // skewFactor
- defaultValues[1]), // defaultValue
- std::make_unique(uid + "Decay", // parameter ID
- "Decay", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 1.0f, // rangeEnd
- 0.001f, // intervalValue
- 0.5f), // skewFactor
- defaultValues[2]), // defaultValue
- std::make_unique(uid + "Depth", // parameter ID
- "Depth", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 1.0f, // rangeEnd
- 0.001f, // intervalValue
- 0.5f), // skewFactor
- defaultValues[3]), // defaultValue
- std::make_unique(uid + "Skew", // parameter ID
- "Skew", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 16.0f, // rangeEnd
- 0.1f, // intervalValue
- 1.0f), // skewFactor
- defaultValues[4])); // defaultValue
-}
-} // namespace model
+#pragma once
+
+#include
+
+//==============================================================================
+namespace dmt {
+namespace model {
+static inline juce::AudioProcessorParameterGroup
+envelopeParameterGroup(juce::String parentUid,
+ juce::String suffix,
+ std::array defaultValues)
+{
+ using ParameterInt = juce::AudioParameterInt;
+ using ParameterFloat = juce::AudioParameterFloat;
+ using ParameterChoice = juce::AudioParameterChoice;
+ using NormalisableRange = juce::NormalisableRange;
+
+ juce::String uid = parentUid + suffix + "Env";
+
+ return juce::AudioProcessorParameterGroup(
+ uid, // group ID
+ suffix + "Envelope", // group name
+ "|", // separator
+ std::make_unique(uid + "Enabled", // parameter ID
+ "Enabled", // parameter name
+ StringArray{ "Off", "On" }, // choices
+ 1), // defaultValue
+ std::make_unique(uid + "Attack", // parameter ID
+ "Attack", // parameter name
+ NormalisableRange(0.0f, // rangeStart
+ 0.3f, // rangeEnd
+ 0.001f, // intervalValue
+ 0.5f), // skewFactor
+ defaultValues[0]), // defaultValue
+ std::make_unique(uid + "Hold", // parameter ID
+ "Hold", // parameter name
+ NormalisableRange(0.0f, // rangeStart
+ 0.3f, // rangeEnd
+ 0.001f, // intervalValue
+ 0.5f), // skewFactor
+ defaultValues[1]), // defaultValue
+ std::make_unique(uid + "Decay", // parameter ID
+ "Decay", // parameter name
+ NormalisableRange(0.0f, // rangeStart
+ 1.0f, // rangeEnd
+ 0.001f, // intervalValue
+ 0.5f), // skewFactor
+ defaultValues[2]), // defaultValue
+ std::make_unique(uid + "Depth", // parameter ID
+ "Depth", // parameter name
+ NormalisableRange(0.0f, // rangeStart
+ 1.0f, // rangeEnd
+ 0.001f, // intervalValue
+ 0.5f), // skewFactor
+ defaultValues[3]), // defaultValue
+ std::make_unique(uid + "AttackBend", // parameter ID
+ "Attack Bend", // parameter name
+ NormalisableRange(-20.0f, // rangeStart
+ 20.0f, // rangeEnd
+ 0.1f, // intervalValue
+ 1.0f), // skewFactor
+ defaultValues[4]), // defaultValue
+ std::make_unique(uid + "DecayBend", // parameter ID
+ "Decay Bend", // parameter name
+ NormalisableRange(-20.0f, // rangeStart
+ 20.0f, // rangeEnd
+ 0.1f, // intervalValue
+ 1.0f), // skewFactor
+ defaultValues[5]) // defaultValue
+ );
+}
+} // namespace model
} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/DigitalOscillatorParameters.h b/src/dmt/model/DigitalOscillatorParameters.h
new file mode 100644
index 00000000..99758438
--- /dev/null
+++ b/src/dmt/model/DigitalOscillatorParameters.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include "../dsp/synth/DigitalOscillator.h"
+#include
+
+//==============================================================================
+namespace dmt {
+namespace model {
+static inline juce::AudioProcessorParameterGroup
+digitalOscillatorParameterGroup(juce::String parentUid)
+{
+ // using ParameterInt = juce::AudioParameterInt;
+ using ParameterFloat = juce::AudioParameterFloat;
+ using ParameterChoice = juce::AudioParameterChoice;
+ using NormalisableRange = juce::NormalisableRange;
+ using ParameterGroup = juce::AudioProcessorParameterGroup;
+ using String = juce::String;
+
+ using DigitalWaveform = dmt::dsp::synth::DigitalWaveform;
+
+ String uid = parentUid + "DigitalOscillator";
+
+ return juce::AudioProcessorParameterGroup(
+ uid, // group ID
+ "Waveform", // group name
+ "|", // separator
+ std::make_unique(uid + "Type", // parameter ID
+ "Type", // parameter name
+ DigitalWaveform::waveformNames, // choices
+ 2), // defaultValue
+ std::make_unique(uid + "Warp",
+ "Warp",
+ NormalisableRange(0.f, // rangeStart
+ 1.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ 0.0f),
+ std::make_unique(uid + "Bend",
+ "Bend",
+ NormalisableRange(-100.f, // rangeStart
+ 100.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ 0.0f), // defaultValue
+ std::make_unique(uid + "Pwm",
+ "Pwm",
+ NormalisableRange(1.0f, // rangeStart
+ 100.f, // rangeEnd
+ .1f, // intervalValue
+ 1.f), // skewFactor
+ 0.0f), // defaultValue
+ std::make_unique(uid + "Sync",
+ "Sync",
+ NormalisableRange(0.f, // rangeStart
+ 100.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ .0f), // defaultValue
+ std::make_unique(uid + "Bias",
+ "Bias",
+ NormalisableRange(-1.f, // rangeStart
+ 1.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ 0.0f), // defaultValue
+ std::make_unique(uid + "Clip",
+ "Clip",
+ NormalisableRange(0.f, // rangeStart
+ 1.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ 0.0f), // defaultValue
+ std::make_unique(uid + "Drive",
+ "Drive",
+ NormalisableRange(-20.f, // rangeStart
+ +20.f, // rangeEnd
+ .01f, // intervalValue
+ 1.f), // skewFactor
+ 3.0f) // defaultValue
+ );
+}
+} // namespace model
+} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/Model.h b/src/dmt/model/Model.h
index 7b1df985..1b7a5846 100644
--- a/src/dmt/model/Model.h
+++ b/src/dmt/model/Model.h
@@ -2,4 +2,5 @@
#include "DisfluxParameters.h"
#include "GlobalParameters.h"
#include "HeretikParameters.h"
+#include "NeutrinoParameters.h"
#include "OscilloscopeParameters.h"
diff --git a/src/dmt/model/NeutrinoParameters.h b/src/dmt/model/NeutrinoParameters.h
new file mode 100644
index 00000000..83d27bf1
--- /dev/null
+++ b/src/dmt/model/NeutrinoParameters.h
@@ -0,0 +1,38 @@
+#pragma once
+//==============================================================================
+#include "AhdEnvelopeParameters.h"
+#include "DigitalOscillatorParameters.h"
+#include
+//==============================================================================
+namespace dmt {
+namespace model {
+//==============================================================================
+static inline juce::AudioProcessorParameterGroup
+neutrinoParameterGroup(juce::String parentUid, [[maybe_unused]] int versionHint)
+{
+ using ParameterInt = juce::AudioParameterInt;
+ using ParameterFloat = juce::AudioParameterFloat;
+ using ParameterChoice = juce::AudioParameterChoice;
+ using NormalisableRange = juce::NormalisableRange;
+ using ParameterGroup = juce::AudioProcessorParameterGroup;
+
+ juce::String uid = parentUid + "Neutrino";
+
+ return juce::AudioProcessorParameterGroup(
+ uid, // group ID
+ "Neutrino", // group name
+ "|", // separator
+
+ // Oscillators
+ std::make_unique(digitalOscillatorParameterGroup(uid)),
+
+ // Envelopes
+ std::make_unique(envelopeParameterGroup(
+ uid, "Gain", { 0.0f, 0.055f, 0.350f, 0.0f, 0.0f, 0.0f })),
+ std::make_unique(envelopeParameterGroup(
+ uid, "Pitch1", { 0.0f, 0.0f, 0.185f, 0.033f, 0.0f, 0.0f })),
+ std::make_unique(envelopeParameterGroup(
+ uid, "Pitch2", { 0.0f, 0.0f, 0.02f, 0.033f, 0.0f, 0.0f })));
+}
+} // namespace model
+} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/OscSendParameterGroup.h b/src/dmt/model/OscSendParameterGroup.h
deleted file mode 100644
index afbc7652..00000000
--- a/src/dmt/model/OscSendParameterGroup.h
+++ /dev/null
@@ -1,34 +0,0 @@
-#include
-//==============================================================================
-namespace dmt {
-namespace model {
-static inline juce::AudioProcessorParameterGroup
-oscSendParameterGroup(juce::String parentUid, juce::String channel)
-{
- using ParameterFloat = juce::AudioParameterFloat;
- using NormalisableRange = juce::NormalisableRange;
-
- juce::String uid = parentUid + "Send" + channel;
-
- return juce::AudioProcessorParameterGroup(
- uid, // group ID
- "OscSend", // group name
- "|", // separator
- std::make_unique(uid + "Gain", // parameter ID
- "Gain", // parameter name
- NormalisableRange(-96.f, // rangeStart
- 0.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0.f),
- std::make_unique(uid + "Pan", // parameter ID
- "Pan", // parameter name
- NormalisableRange(-1.f, // rangeStart
- 1.f, // rangeEnd
- .01f, // intervalValue
- 1.f), // skewFactor
- 0.f)); // defaultValue
-}
-
-} // namespace model
-} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/OscillatorParameters.h b/src/dmt/model/OscillatorParameters.h
deleted file mode 100644
index 2ddaf9c6..00000000
--- a/src/dmt/model/OscillatorParameters.h
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma once
-
-#include "../dsp/synth/AnalogWaveform.h"
-#include "AhdEnvelopeParameters.h"
-#include "DistortionParameters.h"
-#include "OscSendParameterGroup.h"
-#include "VoiceParameters.h"
-#include "WaveformParameters.h"
-#include
-//==============================================================================
-namespace dmt {
-namespace model {
-static inline juce::AudioProcessorParameterGroup
-oscillatorParameterGroup(int index)
-{
- using ParameterGroup = juce::AudioProcessorParameterGroup;
-
- juce::String uid = juce::String("osc" + juce::String(index));
-
- return juce::AudioProcessorParameterGroup(
- uid, // group ID
- "Oscillator", // group name
- "|", // separator
- std::make_unique(waveformParameterGroup(uid)),
- std::make_unique(voiceParameterGroup(uid)),
- std::make_unique(distortionParameterGroup(uid)),
- std::make_unique(
- envelopeParameterGroup(uid, "Gain", { 0.0f, 0.04f, 0.335f, 0.0f, 2.0f })),
- std::make_unique(
- envelopeParameterGroup(uid, "Pitch", { 0.0f, 0.0f, 0.144f, 1.0f, 7.5f })),
- std::make_unique(oscSendParameterGroup(uid, "A")),
- std::make_unique(oscSendParameterGroup(uid, "B")),
- std::make_unique(oscSendParameterGroup(uid, "C")));
-}
-} // namespace model
-} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/ParameterLayout.h b/src/dmt/model/ParameterLayout.h
deleted file mode 100644
index 46d60d5e..00000000
--- a/src/dmt/model/ParameterLayout.h
+++ /dev/null
@@ -1,34 +0,0 @@
-//==============================================================================
-
-#pragma once
-
-#include "../dsp/filter/FilterProcessor.h"
-
-#include "OscillatorParameters.h"
-
-#include
-
-//==============================================================================
-namespace dmt {
-//==============================================================================
-static inline juce::AudioProcessorValueTreeState::ParameterLayout
-createParameterLayout()
-{
- using ParameterInt = juce::AudioParameterInt;
- using ParameterFloat = juce::AudioParameterFloat;
- using ParameterChoice = juce::AudioParameterChoice;
- using ParameterGroup = juce::AudioProcessorParameterGroup;
- using NormalisableRange = juce::NormalisableRange;
- namespace Model = dmt::model;
- return juce::AudioProcessorValueTreeState::ParameterLayout{
- std::make_unique("oscGain", // parameter ID
- "Attack", // parameter name
- NormalisableRange(0.0f, // rangeStart
- 1.0f, // rangeEnd
- 0.001f, // intervalValue
- 0.5f), // skewFactor
- 0.0f), // defaultValue
- std::make_unique(Model::oscillatorParameterGroup(1))
- };
-}
-} // namespace dmt
diff --git a/src/dmt/model/VoiceParameters.h b/src/dmt/model/VoiceParameters.h
deleted file mode 100644
index 90a713e4..00000000
--- a/src/dmt/model/VoiceParameters.h
+++ /dev/null
@@ -1,104 +0,0 @@
-#pragma once
-
-#include
-
-//==============================================================================
-namespace dmt {
-namespace model {
-static inline juce::AudioProcessorParameterGroup
-voiceParameterGroup(juce::String parentUid)
-{
- using ParameterInt = juce::AudioParameterInt;
- using ParameterFloat = juce::AudioParameterFloat;
- using ParameterChoice = juce::AudioParameterChoice;
- using NormalisableRange = juce::NormalisableRange;
-
- juce::String uid = parentUid + "Voice";
-
- return juce::AudioProcessorParameterGroup(
- uid, // group ID
- "Voice", // group name
- "|", // separator
- std::make_unique(uid + "Octave", // parameter ID
- "Octave", // parameter name
- -4, // rangeStart
- 4, // rangeEnd
- 0), // defaultValue
- std::make_unique(uid + "Semitone", // parameter ID
- "Semitones", // parameter name
- 0, // rangeStart
- 11, // rangeEnd
- 0), // defaultValue
- std::make_unique(uid + "Fine", // parameter ID
- "Fine", // parameter name
- NormalisableRange(-100.f, // rangeStart
- 100.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0),
- std::make_unique(uid + "Density", // parameter ID
- "Density", // parameter name
- 1, // rangeStart
- 8, // rangeEnd
- 1), // defaultValue
- std::make_unique(uid + "Detune", // parameter ID
- "Detune", // parameter name
- NormalisableRange(0.f, // rangeStart
- 100.f,
- .1f,
- 1.f),
- 0.f),
- std::make_unique(uid + "Distribution", // parameter ID
- "Distribution", // parameter name
- juce::StringArray{ "Linear",
- "Quadratic",
- "Cubic",
- "Octic",
- "Square Root",
- "Cube Root",
- "Octic Root",
- "Sine",
- "Random" }, // choices
- 0), // default index
- std::make_unique(uid + "Blend", // parameter ID
- "Blend", // parameter name
- NormalisableRange(0.f, // rangeStart
- 100.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0.f), // defaultValue
- std::make_unique(uid + "Width", // parameter ID
- "Width", // parameter name
- NormalisableRange(0.f, // rangeStart
- 100.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0.f), // defaultValue
- std::make_unique(
- uid + "Seed", // parameter ID
- "Seed", // parameter name
- juce::StringArray{ "Random",
- "Equal",
- "Static #1",
- "Static #2",
- "Static #3",
- "Static #4",
- "Static #5" }, // choices
- 0), // default index
- std::make_unique(uid + "Random", // parameter ID
- "Random", // parameter name
- NormalisableRange(0.f, // rangeStart
- 100.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0.f),
- std::make_unique(uid + "Phase", // parameter ID
- "Phase", // parameter name
- NormalisableRange(0.f, // rangeStart
- 100.f, // rangeEnd
- .1f, // intervalValue
- 1.f), // skewFactor
- 0.f)); // defaultValue
-}
-} // namespace model
-} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/model/WaveformParameters.h b/src/dmt/model/WaveformParameters.h
deleted file mode 100644
index fd61baf5..00000000
--- a/src/dmt/model/WaveformParameters.h
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma once
-
-#include
-#include "../dsp/synth/AnalogWaveform.h"
-
-//==============================================================================
-namespace dmt {
-namespace model {
-static inline juce::AudioProcessorParameterGroup
-waveformParameterGroup(juce::String parentUid)
-{
- using ParameterInt = juce::AudioParameterInt;
- using ParameterFloat = juce::AudioParameterFloat;
- using ParameterChoice = juce::AudioParameterChoice;
- using NormalisableRange = juce::NormalisableRange;
-
- juce::String uid = parentUid + "Waveform";
-
- return juce::AudioProcessorParameterGroup(
- uid, // group ID
- "Waveform", // group name
- "|", // separator
- std::make_unique(uid + "Type", // parameter ID
- "Type", // parameter name
- dmt::dsp::synth::AnalogWaveform::waveformNames, // choices
- 2), // defaultIndex
- std::make_unique(uid + "Bend",
- "Bend",
- NormalisableRange(-100.f, // rangeStart
- 100.f, // rangeEnd
- .01f, // intervalValue
- 1.f), // skewFactor
- .0f), // defaultValue
- std::make_unique(uid + "Pwm",
- "Pwm",
- NormalisableRange(.0f, // rangeStart
- 100.f, // rangeEnd
- .01f, // intervalValue
- 1.f), // skewFactor
- 4.f), // defaultValue
- std::make_unique(uid + "Sync",
- "Sync",
- NormalisableRange(0.f, // rangeStart
- 100.f, // rangeEnd
- .01f, // intervalValue
- 1.f), // skewFactor
- .0f) // defaultValue)
- );
-}
-} // namespace model
-} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/scripts/add_subtree.sh b/src/dmt/scripts/add_subtree.sh
new file mode 100644
index 00000000..0cb19e59
--- /dev/null
+++ b/src/dmt/scripts/add_subtree.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+git remote add dmt-upstream https://github.com/Dimethoxy/DMT.git
+
+git fetch dmt-upstream
+
+git subtree add --prefix=src/dmt dmt-upstream main --squash
\ No newline at end of file
diff --git a/src/dmt/scripts/pull_subtree.sh b/src/dmt/scripts/pull_subtree.sh
new file mode 100644
index 00000000..be0d2863
--- /dev/null
+++ b/src/dmt/scripts/pull_subtree.sh
@@ -0,0 +1 @@
+git subtree pull --prefix=src/dmt dmt-upstream main --squash
\ No newline at end of file
diff --git a/src/dmt/utility/HostContextMenu.h b/src/dmt/utility/HostContextMenu.h
index b48e5cd5..179b6ffb 100644
--- a/src/dmt/utility/HostContextMenu.h
+++ b/src/dmt/utility/HostContextMenu.h
@@ -146,5 +146,7 @@ class HostContextMenu
jassertfalse; // Could not find the editor in the hierarchy
return nullptr;
}
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HostContextMenu)
};
} // namespace dmt
\ No newline at end of file
diff --git a/src/dmt/utility/RepaintTimer.h b/src/dmt/utility/RepaintTimer.h
index c8006aa9..75ac0819 100644
--- a/src/dmt/utility/RepaintTimer.h
+++ b/src/dmt/utility/RepaintTimer.h
@@ -85,7 +85,7 @@ class RepaintTimer : private juce::Timer
* Ensures that the timer is stopped and resources are released. No
* side-effects beyond JUCE Timer cleanup.
*/
- inline ~RepaintTimer() noexcept override = default;
+ inline ~RepaintTimer() { stopRepaintTimer(); }
//============================================================================
/**
diff --git a/src/dmt/utility/Scaleable.h b/src/dmt/utility/Scaleable.h
index c1018446..7e610ac9 100644
--- a/src/dmt/utility/Scaleable.h
+++ b/src/dmt/utility/Scaleable.h
@@ -215,21 +215,57 @@ class Scaleable : public IScaleable
*/
[[nodiscard]] float getScaleFactor() const noexcept
{
- // TODO: Component Scale factor is fucked, for now we just use the main
- // display's scale factor using the fallback
- /*
+
const auto* component = static_cast(getSelf());
+ auto& displays = juce::Desktop::getInstance().getDisplays();
+
+ // 1. Try to get the scale factor from the host peer
+ float hostScale = -1.0f;
+ if (component != nullptr) {
+ if (auto* peer = component->getPeer())
+ hostScale = peer->getPlatformScaleFactor();
+ }
+ // 2. Try to get the scale factor from the component itself
+ float componentScale = -1.0f;
if (component != nullptr) {
- const auto componentScale =
+ componentScale =
juce::Component::getApproximateScaleFactorForComponent(component);
+ }
+
+ // 3. Try to resolve display from component position
+ float displayScale = -1.0f;
+ if (component != nullptr) {
+ const auto screenPos = component->getScreenPosition();
- if (std::isfinite(componentScale) && componentScale > 0.0f)
- return componentScale;
+ if (auto* d = displays.getDisplayForPoint(screenPos)) {
+ displayScale = static_cast(d->scale);
+ }
}
- */
- return getFallbackScaleFactor();
+ // 4. Fallback to primary display
+ float primaryScale = -1.0f;
+ if (auto* primary = displays.getPrimaryDisplay()) {
+ primaryScale = static_cast(primary->scale);
+ }
+
+ // Very hacky heuristic to determine the most likely correct scale factor
+ using juce::approximatelyEqual;
+
+ if (!approximatelyEqual(hostScale, 1.0f) && hostScale > 0.0f)
+ return hostScale;
+
+ if (!approximatelyEqual(componentScale, 1.0f) && componentScale > 0.0f)
+ return componentScale;
+
+ if (!approximatelyEqual(displayScale, 1.0f) && displayScale > 0.0f)
+ return displayScale;
+
+ if (!approximatelyEqual(primaryScale, 1.0f) && primaryScale > 0.0f)
+ return primaryScale;
+
+ // If none is greater than 1, just return 1 (no scaling)
+ return 1.0f;
}
private:
@@ -248,32 +284,6 @@ class Scaleable : public IScaleable
return static_cast(this);
}
- //============================================================================
- /**
- * @brief Get a fallback platform DPI scaling factor.
- *
- * Used when the component is not yet attached to a hierarchy.
- */
- static float getFallbackScaleFactor() noexcept
- {
- // Find the main display
- auto* mainDisplay =
- juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
-
- if (mainDisplay == nullptr) {
- jassertfalse; // Could not find main display
- return 1.0f;
- }
-
- // Get the scale factor from the main display
- const auto fallbackScale = static_cast(mainDisplay->scale);
-
- if (std::isfinite(fallbackScale) && fallbackScale > 0.0f)
- return fallbackScale;
-
- return 1.0f;
- }
-
/**
* @brief Set the scaling factor for this component.
*
@@ -282,7 +292,7 @@ class Scaleable : public IScaleable
*/
void setSizeFactor(const float& newSize) noexcept override
{
- if (&newSize != &internalSize) {
+ if (!juce::approximatelyEqual(newSize, internalSize)) {
internalSize = newSize;
}
}
diff --git a/src/dmt/utility/Settings.h b/src/dmt/utility/Settings.h
index 564d8fc7..029aeb44 100644
--- a/src/dmt/utility/Settings.h
+++ b/src/dmt/utility/Settings.h
@@ -39,21 +39,21 @@
// OS constexprs set by CMake preprocessor definitions
#if defined(CMAKE_OS_IS_WINDOWS) && CMAKE_OS_IS_WINDOWS
-static constexpr bool OS_IS_WINDOWS = true;
+#define OS_IS_WINDOWS 1
#else
-static constexpr bool OS_IS_WINDOWS = false;
+#define OS_IS_WINDOWS 0
#endif
#if defined(CMAKE_OS_IS_DARWIN) && CMAKE_OS_IS_DARWIN
-static constexpr bool OS_IS_DARWIN = true;
+#define OS_IS_DARWIN 1
#else
-static constexpr bool OS_IS_DARWIN = false;
+#define OS_IS_DARWIN 0
#endif
#if defined(CMAKE_OS_IS_LINUX) && CMAKE_OS_IS_LINUX
-static constexpr bool OS_IS_LINUX = true;
+#define OS_IS_LINUX 1
#else
-static constexpr bool OS_IS_LINUX = false;
+#define OS_IS_LINUX 0
#endif
static_assert(
@@ -125,9 +125,16 @@ struct Settings
static inline auto& debugGrid =
container.add("General.ShowDebugGrid", false);
static inline auto& displayUpdateNotifications =
- container.add("General.DisplayUpdateNotifications", true);
+ container.add("General.DisplayUpdateNotifications", false);
static inline auto& themeVersion =
container.add("General.ThemeVersion", 2);
+#if OS_IS_LINUX
+ static inline auto& useOpenGL =
+ container.add("General.UseOpenGL", true);
+#elif OS_IS_DARWIN
+ static inline auto& useOpenGL =
+ container.add("General.UseOpenGL", false);
+#endif
private:
//==============================================================================
@@ -213,7 +220,8 @@ struct Settings
container.add("Header.BorderButtonBorderColour",
Colours::success.darker(0.5f));
static inline auto& borderButtonFontColour =
- container.add("Header.BorderButtonFontColour", Colours::shadow);
+ container.add("Header.BorderButtonFontColour",
+ Colours::background);
static inline auto& borderButtonFontSize =
container.add("Header.BorderButtonFontSize", 20.0f);
static inline auto& borderButtonHeight =
@@ -426,9 +434,9 @@ struct Settings
static inline auto& fontColour =
container.add("Button.FontColour", Colours::font);
static inline auto& hoverColour =
- container.add("Button.HoverColour", Colours::primary);
+ container.add("Button.HoverColour", Colours::font);
static inline auto& clickColour =
- container.add("Button.ClickColour", Colours::font);
+ container.add("Button.ClickColour", Colours::primary);
static inline auto& outerShadowRadius =
container.add("Button.OuterShadowRadius", 5.0f);
static inline auto& innerShadowRadius =
@@ -472,9 +480,9 @@ struct Settings
static inline auto& innerShadowColour =
container.add("Panel.InnerShadowColour", Colours::shadow);
static inline auto& outerShadowRadius =
- container.add("Panel.OuterShadowRadius", 10.0f);
+ container.add("Panel.OuterShadowRadius", 5.0f);
static inline auto& innerShadowRadius =
- container.add("Panel.InnerShadowRadius", 10.0f);
+ container.add("Panel.InnerShadowRadius", 5.0f);
static inline auto& fontColor =
container.add("Panel.FontColor", Colours::font);
static inline auto& fontSize =
diff --git a/src/dmt/utility/Unit.h b/src/dmt/utility/Unit.h
index 7de6072c..6cfa04ad 100644
--- a/src/dmt/utility/Unit.h
+++ b/src/dmt/utility/Unit.h
@@ -83,7 +83,7 @@ struct alignas(8) Unit
Bitdepth,
VoiceDensity,
VoiceDistribution,
- EnvelopeSkew,
+ EnvelopeBend,
Milliseconds,
OscilloscopeZoom,
OscilloscopeThickness,
@@ -168,6 +168,9 @@ struct alignas(8) Unit
return String(static_cast(static_cast(_value * 100.0f))) +
String("%");
break;
+ case Type::EnvelopeBend:
+ return String(_value, 1) + String("x");
+ break;
// Heretik
case Type::HeretikPreGain: