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 Image of the GUI - ## πŸ”₯ 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: