diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..9f1dd95ec
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+root = true
+
+[*.{h,hh,hpp,hxx,c,cpp,cc,cxx}]
+charset = utf-8
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml
index 240e3e232..2565099b6 100644
--- a/.github/workflows/build_android.yml
+++ b/.github/workflows/build_android.yml
@@ -44,7 +44,7 @@ jobs:
distribution: ${{env.JAVA_DISTRIBUTION}}
java-version: ${{env.JAVA_VERSION}}
- name: Setup Android SDK
- uses: android-actions/setup-android@v2.0.10
+ uses: android-actions/setup-android@v3
- name: Setup Android NDK
uses: nttld/setup-ndk@v1
with:
@@ -71,7 +71,7 @@ jobs:
distribution: ${{env.JAVA_DISTRIBUTION}}
java-version: ${{env.JAVA_VERSION}}
- name: Setup Android SDK
- uses: android-actions/setup-android@v2.0.10
+ uses: android-actions/setup-android@v3
- name: Setup Android NDK
uses: nttld/setup-ndk@v1
with:
@@ -102,7 +102,7 @@ jobs:
distribution: ${{env.JAVA_DISTRIBUTION}}
java-version: ${{env.JAVA_VERSION}}
- name: Setup Android SDK
- uses: android-actions/setup-android@v2.0.10
+ uses: android-actions/setup-android@v3
- name: Setup Android NDK
uses: nttld/setup-ndk@v1
with:
diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml
index 7e68cbdbe..fa5eae5e6 100644
--- a/.github/workflows/build_linux.yml
+++ b/.github/workflows/build_linux.yml
@@ -58,6 +58,7 @@ jobs:
build_tests_debug:
runs-on: ubuntu-latest
needs: [configure]
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
@@ -78,6 +79,7 @@ jobs:
build_tests_release:
runs-on: ubuntu-latest
needs: [configure]
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml
index 727899b23..22238a1be 100644
--- a/.github/workflows/build_macos.yml
+++ b/.github/workflows/build_macos.yml
@@ -49,6 +49,7 @@ jobs:
build_tests_debug:
runs-on: macos-latest
needs: [configure]
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
@@ -68,6 +69,7 @@ jobs:
build_tests_release:
runs-on: macos-latest
needs: [configure]
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml
index 7cd685ea3..69762fa70 100644
--- a/.github/workflows/build_windows.yml
+++ b/.github/workflows/build_windows.yml
@@ -31,6 +31,7 @@ concurrency:
jobs:
build_tests_debug:
runs-on: windows-latest
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
@@ -43,6 +44,7 @@ jobs:
build_tests_release:
runs-on: windows-latest
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index b1849ac5b..d2d07b853 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -55,6 +55,7 @@ env:
jobs:
cpp-coverage:
runs-on: ubuntu-latest
+ timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -113,4 +114,4 @@ jobs:
if: always()
with:
name: coverage-reports
- path: ${{ runner.workspace }}/build/coverage/
\ No newline at end of file
+ path: ${{ runner.workspace }}/build/coverage/
diff --git a/.gitignore b/.gitignore
index f6f87ab43..5eca3712c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,12 +36,13 @@
# Intermediates
build/*
-standalone/*/build/*
cmake-build*
out/*
# Ides/Agents
-.vscode
-.idea
-.vs
-.claude
+.vscode/
+.idea/
+.vs/
+.claude/
+.pytest_cache/
+.cache/
diff --git a/AGENTS.md b/AGENTS.md
index f078ef2e5..7ffd48e55 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,7 +5,7 @@ This document provides directive guidelines for AI assistants working on the YUP
## Project Context
- **Project Type:** C++ graphics/audio library
- **License:** ISC License
-- **Copyright:** `Copyright (c) 2025 - kunitoki@gmail.com`
+- **Copyright:** `Copyright (c) 2026 - kunitoki@gmail.com`
- **Based On:** Fork of JUCE7 ISC Modules
- **Build System:** CMake
- **Testing Framework:** Google Test
@@ -23,7 +23,7 @@ This document provides directive guidelines for AI assistants working on the YUP
==============================================================================
This file is part of the YUP library.
- Copyright (c) 2025 - kunitoki@gmail.com
+ Copyright (c) 2026 - kunitoki@gmail.com
YUP is an open source library subject to open-source licensing.
diff --git a/CLAUDE.md b/CLAUDE.md
index f078ef2e5..7ffd48e55 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -5,7 +5,7 @@ This document provides directive guidelines for AI assistants working on the YUP
## Project Context
- **Project Type:** C++ graphics/audio library
- **License:** ISC License
-- **Copyright:** `Copyright (c) 2025 - kunitoki@gmail.com`
+- **Copyright:** `Copyright (c) 2026 - kunitoki@gmail.com`
- **Based On:** Fork of JUCE7 ISC Modules
- **Build System:** CMake
- **Testing Framework:** Google Test
@@ -23,7 +23,7 @@ This document provides directive guidelines for AI assistants working on the YUP
==============================================================================
This file is part of the YUP library.
- Copyright (c) 2025 - kunitoki@gmail.com
+ Copyright (c) 2026 - kunitoki@gmail.com
YUP is an open source library subject to open-source licensing.
diff --git a/LICENSE b/LICENSE
index ddef4260f..eced558ba 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
ISC License
-YUP - Copyright (c) 2024 kunitoki@gmail.com
+YUP - Copyright (c) 2024-2026 kunitoki@gmail.com
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is
hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
diff --git a/README.md b/README.md
index 6c6b1c6e5..af37eada0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# YUP: Cross-Platform Application And Plugin Development Library
+
+
+# YUP! Cross-Platform Application And Plugin Development Library
diff --git a/cmake/platforms/ios/Info.plist b/cmake/platforms/ios/Info.plist
index 37a1c9267..3a307ec88 100644
--- a/cmake/platforms/ios/Info.plist
+++ b/cmake/platforms/ios/Info.plist
@@ -30,5 +30,7 @@
${MACOSX_BUNDLE_COPYRIGHT}
NSHighResolutionCapable
+ UIApplicationSupportsIndirectInputEvents
+
\ No newline at end of file
diff --git a/cmake/platforms/yup_android.cmake b/cmake/platforms/yup_android.cmake
index 93dd22349..190a05168 100644
--- a/cmake/platforms/yup_android.cmake
+++ b/cmake/platforms/yup_android.cmake
@@ -26,7 +26,7 @@ include (${CMAKE_CURRENT_LIST_DIR}/../yup_utilities.cmake)
function (_yup_android_prepare_gradle)
set (options "")
set (one_value_args
- MIN_SDK_VERSION COMPILE_SDK_VERSION TARGET_SDK_VERSION
+ BASE_PATH MIN_SDK_VERSION COMPILE_SDK_VERSION TARGET_SDK_VERSION
TARGET_NAME TARGET_ICON ABI TOOLCHAIN PLATFORM STL CPP_VERSION CMAKE_VERSION
APPLICATION_ID APPLICATION_NAMESPACE APPLICATION_CMAKELISTS_PATH APPLICATION_VERSION)
set (multi_value_args "")
@@ -54,7 +54,7 @@ function (_yup_android_prepare_gradle)
file (RELATIVE_PATH YUP_ANDROID_APPLICATION_PATH "${CMAKE_CURRENT_BINARY_DIR}/app" "${YUP_ANDROID_APPLICATION_PATH}")
# Prepare files
- set (BASE_FILES_PATH "${CMAKE_SOURCE_DIR}/cmake/platforms/android")
+ set (BASE_FILES_PATH "${YUP_ANDROID_BASE_PATH}/platforms/android")
configure_file (${BASE_FILES_PATH}/build.gradle.kts.in ${CMAKE_CURRENT_BINARY_DIR}/build.gradle.kts)
configure_file (${BASE_FILES_PATH}/settings.gradle.kts.in ${CMAKE_CURRENT_BINARY_DIR}/settings.gradle.kts)
configure_file (${BASE_FILES_PATH}/app/build.gradle.kts.in ${CMAKE_CURRENT_BINARY_DIR}/app/build.gradle.kts)
diff --git a/cmake/platforms/yup_emscripten.cmake b/cmake/platforms/yup_emscripten.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_emscripten.cmake
+++ b/cmake/platforms/yup_emscripten.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/platforms/yup_ios.cmake b/cmake/platforms/yup_ios.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_ios.cmake
+++ b/cmake/platforms/yup_ios.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/platforms/yup_linux.cmake b/cmake/platforms/yup_linux.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_linux.cmake
+++ b/cmake/platforms/yup_linux.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/platforms/yup_mac.cmake b/cmake/platforms/yup_mac.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_mac.cmake
+++ b/cmake/platforms/yup_mac.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/platforms/yup_uwp.cmake b/cmake/platforms/yup_uwp.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_uwp.cmake
+++ b/cmake/platforms/yup_uwp.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/platforms/yup_windows.cmake b/cmake/platforms/yup_windows.cmake
index e69de29bb..4710c7eca 100644
--- a/cmake/platforms/yup_windows.cmake
+++ b/cmake/platforms/yup_windows.cmake
@@ -0,0 +1,18 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
diff --git a/cmake/yup.cmake b/cmake/yup.cmake
index bf3cf8974..f931b9bbd 100644
--- a/cmake/yup.cmake
+++ b/cmake/yup.cmake
@@ -74,11 +74,11 @@ function (_yup_setup_platform)
_yup_message (STATUS "Setting up for ${platform} platform")
_yup_message (STATUS "Running on cmake ${CMAKE_VERSION}")
- set (YUP_PLATFORM "${platform}" PARENT_SCOPE)
+ set (YUP_PLATFORM "${platform}" CACHE STRING INTERNAL)
foreach (platform_name ${platforms})
string (TOUPPER "${platform_name}" platform_name_upper)
- set (YUP_PLATFORM_${platform_name_upper} ON PARENT_SCOPE)
+ set (YUP_PLATFORM_${platform_name_upper} ON CACHE BOOL INTERNAL)
endforeach()
endfunction()
diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake
index 4c0eea401..1ffebf3cc 100644
--- a/cmake/yup_modules.cmake
+++ b/cmake/yup_modules.cmake
@@ -735,7 +735,9 @@ function (yup_add_module module_path modules_definitions module_group)
# ==== Fetch Python if needed
if (module_needs_python)
- if (NOT YUP_BUILD_WHEEL)
+ if (YUP_ENABLE_STATIC_PYTHON_LIBS)
+ list (APPEND module_libs ${Python_LIBRARIES})
+ elseif (NOT YUP_BUILD_WHEEL)
list (APPEND module_libs Python::Python)
if (YUP_PLATFORM_MAC)
list (APPEND module_link_options "-Wl,-weak_reference_mismatches,weak")
diff --git a/cmake/yup_python.cmake b/cmake/yup_python.cmake
index 71bf42da2..fb3d88710 100644
--- a/cmake/yup_python.cmake
+++ b/cmake/yup_python.cmake
@@ -40,7 +40,7 @@ function (yup_prepare_python_stdlib target_name python_tools_path output_variabl
set (python_embeddable_url "https://www.python.org/ftp/python/${python_version_string}/python-${python_version_string}-embed-amd64.zip")
FetchContent_Declare (python_embed_env URL ${python_embeddable_url})
if (NOT python_embed_env_POPULATED)
- FetchContent_Populate(python_embed_env)
+ FetchContent_MakeAvailable (python_embed_env)
endif()
get_filename_component (python_root_path "${python_embed_env_SOURCE_DIR}" REALPATH)
diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake
index 2218a98be..3c4225675 100644
--- a/cmake/yup_standalone.cmake
+++ b/cmake/yup_standalone.cmake
@@ -29,14 +29,14 @@ function (yup_standalone_app)
INITIAL_MEMORY PTHREAD_POOL_SIZE CUSTOM_PLIST CUSTOM_SHELL)
set (multi_value_args
# Globals
- DEFINITIONS COMPILE_OPTIONS MODULES LINK_OPTIONS
+ DEFINITIONS COMPILE_OPTIONS MODULES SOURCES LINK_OPTIONS
# Emscripten
PRELOAD_FILES)
cmake_parse_arguments (YUP_ARG "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})
_yup_set_default (YUP_ARG_TARGET_CXX_STANDARD 20)
- _yup_set_default (YUP_ARG_TARGET_ICON "${CMAKE_SOURCE_DIR}/cmake/resources/app-icon.png")
+ _yup_set_default (YUP_ARG_TARGET_ICON "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/resources/app-icon.png")
set (target_name "${YUP_ARG_TARGET_NAME}")
set (target_version "${YUP_ARG_TARGET_VERSION}")
@@ -63,11 +63,12 @@ function (yup_standalone_app)
if (YUP_TARGET_ANDROID)
_yup_message (STATUS "${target_name} - Creating java gradle project")
_yup_android_prepare_gradle(
- TARGET_NAME ${target_name}
- TARGET_ICON ${target_icon}
- APPLICATION_ID ${target_app_identifier}
- APPLICATION_NAMESPACE ${target_app_namespace}
- APPLICATION_VERSION ${target_version})
+ BASE_PATH "${CMAKE_CURRENT_FUNCTION_LIST_DIR}"
+ TARGET_NAME "${target_name}"
+ TARGET_ICON "${target_icon}"
+ APPLICATION_ID "${target_app_identifier}"
+ APPLICATION_NAMESPACE "${target_app_namespace}"
+ APPLICATION_VERSION "${target_version}")
_yup_message (STATUS "${target_name} - Copying SDL2 java activity to application")
_yup_fetch_sdl2()
@@ -76,6 +77,18 @@ function (yup_standalone_app)
return()
endif()
+ # ==== Find modules includes
+ set (module_include_dirs "")
+ foreach (module IN ITEMS ${YUP_ARG_MODULES})
+ _yup_message (STATUS "${target_name} - Including module ${module}")
+ get_target_property (module_path ${module} YUP_MODULE_PATH)
+ if (module_path AND EXISTS "${module_path}")
+ get_filename_component (module_path "${module_path}" DIRECTORY)
+ list (APPEND module_include_dirs "${module_path}")
+ endif()
+ endforeach()
+ list (REMOVE_DUPLICATES module_include_dirs)
+
# ==== Find dependencies
if (NOT "${target_console}" AND NOT YUP_PLATFORM_EMSCRIPTEN)
_yup_message (STATUS "${target_name} - Fetching SDL2 library")
@@ -108,11 +121,12 @@ function (yup_standalone_app)
endif()
target_compile_features (${target_name} PRIVATE cxx_std_${target_cxx_standard})
+ target_include_directories (${target_name} PRIVATE ${module_include_dirs})
# ==== Per platform configuration
if (YUP_PLATFORM_APPLE)
if (NOT "${target_console}" AND NOT "${target_wheel}")
- _yup_set_default (YUP_ARG_CUSTOM_PLIST "${CMAKE_SOURCE_DIR}/cmake/platforms/${YUP_PLATFORM}/Info.plist")
+ _yup_set_default (YUP_ARG_CUSTOM_PLIST "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/${YUP_PLATFORM}/Info.plist")
_yup_valid_identifier_string ("${target_app_identifier}" target_app_identifier)
_yup_message (STATUS "${target_name} - Converting application input icon to apple .icns format")
@@ -151,7 +165,7 @@ function (yup_standalone_app)
set_target_properties (${target_name} PROPERTIES SUFFIX ".html")
endif()
- _yup_set_default (YUP_ARG_CUSTOM_SHELL "${CMAKE_SOURCE_DIR}/cmake/platforms/${YUP_PLATFORM}/shell.html")
+ _yup_set_default (YUP_ARG_CUSTOM_SHELL "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/${YUP_PLATFORM}/shell.html")
_yup_set_default (YUP_ARG_INITIAL_MEMORY 33554432) # 32mb
_yup_set_default (YUP_ARG_PTHREAD_POOL_SIZE 8)
@@ -180,6 +194,7 @@ function (yup_standalone_app)
-sERROR_ON_UNDEFINED_SYMBOLS=1
-sSTACK_OVERFLOW_CHECK=2
-sFORCE_FILESYSTEM=1
+ -sEXIT_RUNTIME=1
-sNODERAWFS=0
-sWASMFS=1
-sFETCH=1
@@ -196,7 +211,7 @@ function (yup_standalone_app)
add_custom_command(
TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
- "${CMAKE_SOURCE_DIR}/cmake/platforms/${YUP_PLATFORM}/mini-coi.js"
+ "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/${YUP_PLATFORM}/mini-coi.js"
"${target_copy_dest}/mini-coi.js")
elseif (YUP_PLATFORM_LINUX)
@@ -228,4 +243,15 @@ function (yup_standalone_app)
${additional_libraries}
${YUP_ARG_MODULES})
+ if (YUP_ARG_SOURCES AND NOT YUP_TARGET_ANDROID)
+ target_sources (${target_name} PRIVATE ${YUP_ARG_SOURCES})
+ endif()
+
+ # ==== Post build steps, workaround for python*.dll
+ if ("yup::yup_python" IN_LIST YUP_ARG_MODULES AND YUP_PLATFORM_WINDOWS AND NOT YUP_ENABLE_STATIC_PYTHON_LIBS AND Python_RUNTIME_LIBRARY_RELEASE)
+ add_custom_command (TARGET ${target_name} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different "${Python_RUNTIME_LIBRARY_RELEASE}" "$"
+ COMMENT "Copying Python DLL next to executable")
+ endif()
+
endfunction()
diff --git a/cmake/yup_utilities.cmake b/cmake/yup_utilities.cmake
index 50056cddd..ee8c1e083 100644
--- a/cmake/yup_utilities.cmake
+++ b/cmake/yup_utilities.cmake
@@ -277,7 +277,7 @@ function (_yup_execute_process_or_fail)
OUTPUT_QUIET)
if (NOT result EQUAL 0)
- _yup_join_list_with_separator ("${command}" " " "" "" command_string)
+ _yup_join_list_with_separator ("${ARGN}" " " "" "" command_string)
message (FATAL_ERROR "Failed to execute command '${command_string}': ${error_message}")
endif()
endfunction()
diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt
index 6992ff3d0..8d0dd340e 100644
--- a/examples/graphics/CMakeLists.txt
+++ b/examples/graphics/CMakeLists.txt
@@ -48,7 +48,7 @@ endif()
# ==== Prepare target
set (additional_modules "")
-if (YUP_PLATFORM_DESKTOP)
+if (YUP_PLATFORM_DESKTOP AND NOT YUP_PLATFORM_WINDOWS)
set (additional_modules yup::yup_python)
endif()
@@ -81,6 +81,7 @@ yup_standalone_app (
libvorbis
libpng
libwebp
+ bungee_library
${additional_modules}
${link_libraries})
diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h
index dc3e92b1e..30e8b1c58 100644
--- a/examples/graphics/source/examples/Audio.h
+++ b/examples/graphics/source/examples/Audio.h
@@ -364,9 +364,9 @@ class AudioExample
slider->setRange (0.0f, 1.0f);
slider->setDefaultValue (0.0f);
- slider->onValueChanged = [this, i] (float value)
+ slider->onValueChanged = [this, i] (double value)
{
- harmonicSynth.setHarmonicAmplitude (i, value * 0.4f); // Scale down to prevent clipping
+ harmonicSynth.setHarmonicAmplitude (i, (float) value * 0.4f); // Scale down to prevent clipping
};
addAndMakeVisible (slider);
@@ -410,9 +410,9 @@ class AudioExample
volumeSlider->setRange ({ 0.0f, 1.0f });
volumeSlider->setDefaultValue (0.5f);
- volumeSlider->onValueChanged = [this] (float value)
+ volumeSlider->onValueChanged = [this] (double value)
{
- masterVolume = value;
+ masterVolume = (float) value;
};
volumeSlider->setValue (0.5f); // Set initial volume to 50%
addAndMakeVisible (*volumeSlider);
diff --git a/examples/graphics/source/examples/AudioFileDemo.h b/examples/graphics/source/examples/AudioFileDemo.h
index 91626d903..a90bed4fb 100644
--- a/examples/graphics/source/examples/AudioFileDemo.h
+++ b/examples/graphics/source/examples/AudioFileDemo.h
@@ -2,7 +2,7 @@
==============================================================================
This file is part of the YUP library.
- Copyright (c) 2025 - kunitoki@gmail.com
+ Copyright (c) 2026 - kunitoki@gmail.com
YUP is an open source library subject to open-source licensing.
@@ -21,12 +21,17 @@
#pragma once
+#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
+#include
#include
//==============================================================================
@@ -34,288 +39,451 @@
/**
Draws a multi-channel waveform with one horizontal lane per channel.
*/
-class AudioWaveformDisplay : public yup::Component
+class AudioFileWaveform : public yup::AudioViewComponent
{
public:
- AudioWaveformDisplay()
+ AudioFileWaveform (std::shared_ptr cacheToUse)
+ : yup::AudioViewComponent (std::move (cacheToUse))
{
addAndMakeVisible (playhead);
+ playhead.setVisible (false);
}
- /** Assigns the buffer to render and refreshes the waveform cache. */
- void setAudioBuffer (const yup::AudioBuffer* newBuffer)
- {
- audioBuffer = newBuffer;
-
- playhead.setLaneBounds (getWaveformBounds());
-
- rebuildCache();
- repaint();
- }
-
- /** Clears the waveform display back to its empty placeholder state. */
void clear()
{
- audioBuffer = nullptr;
+ AudioViewComponent::clear();
playheadSeconds = 0.0;
lengthSeconds = 0.0;
- channelPeaks.clear();
-
- updatePlayheadBounds();
-
- repaint();
+ updatePlayheadPosition();
}
- /** Updates the playhead marker position in seconds. */
+ /** Updates the playhead without repainting the full waveform. */
void setPlayhead (double newPlayheadSeconds, double newLengthSeconds)
{
playheadSeconds = newPlayheadSeconds;
lengthSeconds = newLengthSeconds;
-
- updatePlayheadBounds();
+ updatePlayheadPosition();
}
+protected:
void resized() override
{
- rebuildCache();
-
- playhead.setLaneBounds (getWaveformBounds());
-
- updatePlayheadBounds();
+ AudioViewComponent::resized();
+ updatePlayheadPosition();
}
- void paint (yup::Graphics& g) override
+private:
+ class PlayheadMarker : public yup::Component
{
- auto bounds = getLocalBounds().reduced (8);
- g.setFillColor (yup::Color (0xFF101010));
- g.fillAll();
+ public:
+ void paint (yup::Graphics& g) override
+ {
+ g.setFillColor (yup::Color (0xFFFFCC33));
+ g.fillRect (getLocalBounds());
+ }
+ };
- if (audioBuffer == nullptr || audioBuffer->getNumSamples() == 0)
+ void updatePlayheadPosition()
+ {
+ const double sampleRate = getSampleRate();
+ if (lengthSeconds <= 0.0 || sampleRate <= 0.0 || getTotalSamples() <= 0)
{
- g.setFillColor (yup::Colors::lightgray);
- auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (14.0f);
- g.fillFittedText ("Load an audio file to view its waveform.",
- font,
- bounds,
- yup::Justification::center);
+ playhead.setVisible (false);
return;
}
- auto labelArea = bounds.removeFromLeft (labelWidth);
- auto waveformArea = bounds;
- const int numChannels = static_cast (channelPeaks.size());
-
- if (numChannels == 0)
+ const auto waveformBounds = getWaveformBounds();
+ if (waveformBounds.getWidth() <= 0.0f)
+ {
+ playhead.setVisible (false);
return;
+ }
- const float laneHeight = waveformArea.getHeight() / static_cast (numChannels);
- auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f);
+ const double clamped = yup::jlimit (0.0, lengthSeconds, playheadSeconds);
+ const double samplePosition = clamped * sampleRate;
+ const auto viewRange = getViewRangeSamples();
- for (int channel = 0; channel < numChannels; ++channel)
+ if (viewRange.isEmpty()
+ || samplePosition < viewRange.getStart()
+ || samplePosition > viewRange.getEnd())
{
- yup::Rectangle lane (waveformArea.getX(),
- waveformArea.getY() + laneHeight * channel,
- waveformArea.getWidth(),
- laneHeight);
+ playhead.setVisible (false);
+ return;
+ }
- g.setFillColor (yup::Color (0xFF181818));
- g.fillRect (lane);
+ const float x = sampleToX (samplePosition, waveformBounds);
+ const float lineWidth = 2.0f;
+ playhead.setBounds (x - lineWidth * 0.5f,
+ waveformBounds.getY(),
+ lineWidth,
+ waveformBounds.getHeight());
+ playhead.setVisible (true);
+ playhead.repaint();
+ }
- g.setStrokeColor (yup::Color (0xFF2A2A2A));
- g.setStrokeWidth (1.0f);
- g.strokeRect (lane);
+ PlayheadMarker playhead;
+ double playheadSeconds = 0.0;
+ double lengthSeconds = 0.0;
- auto labelBounds = labelArea.withY (lane.getY()).withHeight (lane.getHeight());
- g.setFillColor (yup::Colors::white);
- g.fillFittedText ("Ch " + yup::String (channel + 1),
- font,
- labelBounds,
- yup::Justification::center);
+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioFileWaveform)
+};
- drawChannelWaveform (g, lane, channel);
- }
+//==============================================================================
+
+/**
+ Wraps an AudioSource and taps the audio stream to send to a KMeterState.
+*/
+class MeteringAudioSource : public yup::PositionableAudioSource
+{
+public:
+ MeteringAudioSource (yup::PositionableAudioSource* sourceToWrap, yup::KMeterState& meterState)
+ : source (sourceToWrap)
+ , meter (meterState)
+ {
}
-private:
- class PlayheadComponent : public yup::Component
+ void prepareToPlay (int samplesPerBlockExpected, double newSampleRate) override
{
- public:
- PlayheadComponent()
- {
- setOpaque (false);
- }
+ if (source != nullptr)
+ source->prepareToPlay (samplesPerBlockExpected, newSampleRate);
- void setPlayheadX (float newX)
- {
- playheadX = newX;
+ meter.prepare (newSampleRate, 2);
+ }
- updateBounds();
- }
+ void releaseResources() override
+ {
+ if (source != nullptr)
+ source->releaseResources();
+ }
- void setLaneBounds (const yup::Rectangle& newBounds)
+ void getNextAudioBlock (const yup::AudioSourceChannelInfo& bufferToFill) override
+ {
+ if (source != nullptr)
{
- laneBounds = newBounds;
+ source->getNextAudioBlock (bufferToFill);
- updateBounds();
- }
+ // Tap the audio and push to meter
+ const int numChannels = yup::jmin (bufferToFill.buffer->getNumChannels(), 2);
+ if (numChannels > 0 && bufferToFill.numSamples > 0)
+ {
+ const float* channels[2] = { nullptr, nullptr };
+ for (int i = 0; i < numChannels; ++i)
+ channels[i] = bufferToFill.buffer->getReadPointer (i, bufferToFill.startSample);
- private:
- void paint (yup::Graphics& g) override
+ meter.pushSamples (channels, numChannels, bufferToFill.numSamples);
+ meter.processPendingAudio();
+ }
+ }
+ else
{
- g.setFillColor (yup::Color (0xFFFFCC33));
- g.fillRect (getLocalBounds());
+ bufferToFill.clearActiveBufferRegion();
}
+ }
- void updateBounds()
- {
- if (laneBounds.getWidth() <= 0.0f || playheadX < 0.0f)
- {
- setVisible (false);
- return;
- }
+ void setNextReadPosition (yup::int64 newPosition) override
+ {
+ if (source != nullptr)
+ source->setNextReadPosition (newPosition);
+ }
- setVisible (true);
- const float snappedX = static_cast (static_cast (playheadX));
- setBounds (laneBounds.withX (laneBounds.getX() + snappedX).withWidth (1.0f).toNearestInt());
+ yup::int64 getNextReadPosition() const override
+ {
+ return source != nullptr ? source->getNextReadPosition() : 0;
+ }
- repaint();
- }
+ yup::int64 getTotalLength() const override
+ {
+ return source != nullptr ? source->getTotalLength() : 0;
+ }
- yup::Rectangle laneBounds;
- float playheadX = -1.0f;
- };
+ bool isLooping() const override
+ {
+ return source != nullptr ? source->isLooping() : false;
+ }
- struct ChannelPeaks
+ void setLooping (bool shouldLoop) override
{
- std::vector minValues;
- std::vector maxValues;
- };
+ if (source != nullptr)
+ source->setLooping (shouldLoop);
+ }
+
+private:
+ yup::PositionableAudioSource* source;
+ yup::KMeterState& meter;
- yup::Rectangle getWaveformBounds() const
+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MeteringAudioSource)
+};
+
+//==============================================================================
+/**
+ Wraps a PositionableAudioSource with time-stretching and pitch-shifting.
+*/
+class TimeStretchAudioSource : public yup::PositionableAudioSource
+{
+public:
+ TimeStretchAudioSource (yup::PositionableAudioSource* sourceToWrap, int numChannelsToUse)
+ : source (sourceToWrap)
+ , numChannels (numChannelsToUse)
{
- auto bounds = getLocalBounds().reduced (8);
- bounds.removeFromLeft (labelWidth);
- return bounds;
}
- void rebuildCache()
+ void prepareToPlay (int samplesPerBlockExpected, double newSampleRate) override
{
- channelPeaks.clear();
- if (audioBuffer == nullptr)
- return;
+ sampleRate = newSampleRate;
+ maxInputBlockSize = static_cast (std::ceil (static_cast (samplesPerBlockExpected) / minTimeRatio));
+ maxInputBlockSize = yup::jmax (maxInputBlockSize, samplesPerBlockExpected);
+ outputPointers.resize (static_cast (numChannels));
- const int numSamples = audioBuffer->getNumSamples();
- const int numChannels = audioBuffer->getNumChannels();
- auto waveformBounds = getWaveformBounds();
- const int waveformWidth = static_cast (waveformBounds.getWidth());
+ if (source != nullptr)
+ source->prepareToPlay (maxInputBlockSize, newSampleRate);
- if (numSamples <= 0 || numChannels <= 0 || waveformWidth <= 0)
- return;
+ yup::TimeStretchProcessor::ProcessSpec spec;
+ spec.inputSampleRate = newSampleRate;
+ spec.outputSampleRate = newSampleRate;
+ spec.maximumBlockSize = maxInputBlockSize;
+ spec.numChannels = numChannels;
+
+ const auto result = timeStretchProcessor.prepare (spec);
+ timeStretchAvailable = result.wasOk();
- const int columns = yup::jmax (1, yup::jmin (waveformWidth, numSamples));
- const int samplesPerColumn = yup::jmax (1, numSamples / columns);
+ if (timeStretchAvailable)
+ {
+ timeStretchProcessor.setTimeRatio (timeRatio);
+ timeStretchProcessor.setPitchRatio (pitchRatio);
+ setupInputProvider();
+ }
+ }
+
+ void releaseResources() override
+ {
+ if (source != nullptr)
+ source->releaseResources();
+ }
- channelPeaks.resize (static_cast (numChannels));
- for (int channel = 0; channel < numChannels; ++channel)
+ void getNextAudioBlock (const yup::AudioSourceChannelInfo& bufferToFill) override
+ {
+ if (source == nullptr)
{
- auto& peaks = channelPeaks[static_cast (channel)];
- peaks.minValues.assign (static_cast (columns), 0.0f);
- peaks.maxValues.assign (static_cast (columns), 0.0f);
+ bufferToFill.clearActiveBufferRegion();
+ return;
+ }
- for (int column = 0; column < columns; ++column)
- {
- const int startSample = column * samplesPerColumn;
- const int endSample = (column == columns - 1)
- ? numSamples
- : yup::jmin (numSamples, startSample + samplesPerColumn);
+ applyPendingParameters();
- float minValue = 1.0f;
- float maxValue = -1.0f;
+ const int outputFrames = bufferToFill.numSamples;
+ if (outputFrames <= 0)
+ return;
- for (int sample = startSample; sample < endSample; ++sample)
- {
- const float value = audioBuffer->getSample (channel, sample);
- minValue = yup::jmin (minValue, value);
- maxValue = yup::jmax (maxValue, value);
- }
+ if (! timeStretchAvailable)
+ {
+ source->getNextAudioBlock (bufferToFill);
+ outputPosition += outputFrames;
+ currentInputPosition.store (source->getNextReadPosition());
+ return;
+ }
+
+ const int channelsToProcess = yup::jmin (numChannels, bufferToFill.buffer->getNumChannels());
+ for (int channel = 0; channel < channelsToProcess; ++channel)
+ outputPointers[static_cast (channel)] = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
- peaks.minValues[static_cast (column)] = minValue;
- peaks.maxValues[static_cast (column)] = maxValue;
+ const auto result = timeStretchProcessor.process (nullptr, 0, outputPointers.data(), outputFrames);
+
+ if (result.failed())
+ {
+ bufferToFill.clearActiveBufferRegion();
+ }
+ else
+ {
+ const int renderedFrames = result.getValue();
+ if (renderedFrames < outputFrames)
+ {
+ const int framesToClear = outputFrames - renderedFrames;
+ for (int channel = 0; channel < channelsToProcess; ++channel)
+ bufferToFill.buffer->clear (channel,
+ bufferToFill.startSample + renderedFrames,
+ framesToClear);
}
}
+
+ outputPosition += outputFrames;
}
- void updatePlayheadBounds()
+ void setNextReadPosition (yup::int64 newPosition) override
{
- if (lengthSeconds <= 0.0)
- {
- playhead.setPlayheadX (-1.0f);
- return;
- }
+ const auto oldPosition = outputPosition;
+ outputPosition = newPosition;
- auto waveformBounds = getWaveformBounds();
- playhead.setLaneBounds (waveformBounds);
+ const auto inputPos = getInputPositionForOutput (newPosition);
- const double clamped = yup::jlimit (0.0, lengthSeconds, playheadSeconds);
- const float x = static_cast (clamped / lengthSeconds) * waveformBounds.getWidth();
- playhead.setPlayheadX (x);
+ if (source != nullptr)
+ source->setNextReadPosition (inputPos);
+
+ currentInputPosition.store (inputPos);
+
+ // Only reset if this is a discontinuous seek
+ if (timeStretchAvailable && std::abs (newPosition - oldPosition) > 64)
+ timeStretchProcessor.setInputPosition (inputPos);
+ }
+
+ yup::int64 getNextReadPosition() const override
+ {
+ return outputPosition;
+ }
+
+ yup::int64 getTotalLength() const override
+ {
+ if (source == nullptr)
+ return 0;
+
+ const auto ratio = timeStretchAvailable ? timeRatio.load() : 1.0;
+ return static_cast (std::round (static_cast (source->getTotalLength()) * ratio));
}
- void drawChannelWaveform (yup::Graphics& g, const yup::Rectangle& lane, int channelIndex)
+ bool isLooping() const override
{
- if (channelIndex < 0 || channelIndex >= static_cast (channelPeaks.size()))
+ return source != nullptr ? source->isLooping() : false;
+ }
+
+ void setLooping (bool shouldLoop) override
+ {
+ if (source != nullptr)
+ source->setLooping (shouldLoop);
+ }
+
+ void setTimeRatio (double newTimeRatio)
+ {
+ const auto clamped = yup::jlimit (minTimeRatio, maxTimeRatio, newTimeRatio);
+ if (timeRatio.load() == clamped)
return;
- const auto& peaks = channelPeaks[static_cast (channelIndex)];
- if (peaks.minValues.empty() || peaks.maxValues.empty())
+ timeRatio.store (clamped);
+ parametersDirty.store (true);
+ }
+
+ void setPitchRatio (double newPitchRatio)
+ {
+ const auto clamped = yup::jlimit (minPitchRatio, maxPitchRatio, newPitchRatio);
+ if (pitchRatio.load() == clamped)
return;
- const float centerY = lane.getCenterY();
- const float amplitude = lane.getHeight() * 0.45f;
- const float startX = lane.getX();
- const float stepX = lane.getWidth() / static_cast (peaks.minValues.size());
+ pitchRatio.store (clamped);
+ parametersDirty.store (true);
+ }
+
+ double getTimeRatio() const noexcept { return timeRatio.load(); }
+
+ double getPitchRatio() const noexcept { return pitchRatio.load(); }
+
+ yup::int64 getInputPosition() const noexcept { return currentInputPosition.load(); }
+
+private:
+ void setupInputProvider()
+ {
+ if (source == nullptr)
+ return;
- g.setStrokeColor (getChannelColor (channelIndex));
- g.setStrokeWidth (1.0f);
+ const int maxFrames = timeStretchProcessor.getMaxInputFrameCount();
+ tempBuffer.setSize (numChannels, maxFrames);
- for (size_t i = 0; i < peaks.minValues.size(); ++i)
+ timeStretchProcessor.setInputProvider ([this] (yup::int64 beginFrame,
+ int numFrames,
+ float* const* destChannels,
+ int channelStride,
+ int& muteHead,
+ int& muteTail)
{
- float x = startX + static_cast (i) * stepX;
- float minValue = peaks.minValues[i];
- float maxValue = peaks.maxValues[i];
+ (void) channelStride;
+ muteHead = 0;
+ muteTail = 0;
- float y1 = centerY - maxValue * amplitude;
- float y2 = centerY - minValue * amplitude;
+ // Track the center of the grain as the current input position
+ currentInputPosition.store (beginFrame + numFrames / 2);
- g.strokeLine ({ x, y1 }, { x, y2 });
- }
+ if (source == nullptr || numFrames <= 0)
+ {
+ muteHead = numFrames;
+ return;
+ }
- g.setStrokeColor (yup::Color (0xFF3A3A3A));
- g.setStrokeWidth (1.0f);
- g.strokeLine ({ lane.getX(), centerY }, { lane.getRight(), centerY });
+ const auto totalLength = source->getTotalLength();
+ const yup::int64 clampedBegin = yup::jlimit (0, totalLength, beginFrame);
+ const yup::int64 clampedEnd = yup::jlimit (0, totalLength, beginFrame + numFrames);
+
+ if (clampedBegin >= clampedEnd)
+ {
+ muteHead = numFrames;
+ for (int ch = 0; ch < numChannels; ++ch)
+ std::fill (destChannels[ch], destChannels[ch] + numFrames, 0.0f);
+ return;
+ }
+
+ muteHead = static_cast (clampedBegin - beginFrame);
+ muteTail = static_cast ((beginFrame + numFrames) - clampedEnd);
+ const int validFrames = static_cast (clampedEnd - clampedBegin);
+
+ source->setNextReadPosition (clampedBegin);
+ yup::AudioSourceChannelInfo info (&tempBuffer, 0, validFrames);
+ source->getNextAudioBlock (info);
+
+ for (int ch = 0; ch < numChannels; ++ch)
+ {
+ if (muteHead > 0)
+ std::fill (destChannels[ch], destChannels[ch] + muteHead, 0.0f);
+
+ std::copy (tempBuffer.getReadPointer (ch),
+ tempBuffer.getReadPointer (ch) + validFrames,
+ destChannels[ch] + muteHead);
+
+ if (muteTail > 0)
+ std::fill (destChannels[ch] + muteHead + validFrames,
+ destChannels[ch] + numFrames,
+ 0.0f);
+ }
+ });
}
- yup::Color getChannelColor (int channelIndex) const
+ void applyPendingParameters()
{
- static const yup::Color colors[] = {
- yup::Color (0xFF5BC0EB),
- yup::Color (0xFFFDE74C),
- yup::Color (0xFF9BC53D),
- yup::Color (0xFFE55934),
- yup::Color (0xFFFA7921),
- yup::Color (0xFF9D4EDD)
- };
+ if (! timeStretchAvailable)
+ return;
+
+ if (! parametersDirty.exchange (false))
+ return;
- const int colorIndex = channelIndex % (static_cast (sizeof (colors) / sizeof (colors[0])));
- return colors[colorIndex];
+ const auto newTimeRatio = timeRatio.load();
+ const auto newPitchRatio = pitchRatio.load();
+ timeStretchProcessor.setTimeRatio (newTimeRatio);
+ timeStretchProcessor.setPitchRatio (newPitchRatio);
}
- const yup::AudioBuffer* audioBuffer = nullptr;
- std::vector channelPeaks;
- double playheadSeconds = 0.0;
- double lengthSeconds = 0.0;
- const int labelWidth = 48;
- PlayheadComponent playhead;
+ yup::int64 getInputPositionForOutput (yup::int64 outputFrames) const
+ {
+ const auto ratio = timeStretchAvailable ? timeRatio.load() : 1.0;
+ return static_cast (std::floor (static_cast (outputFrames) / ratio));
+ }
+
+ yup::PositionableAudioSource* source = nullptr;
+ int numChannels = 0;
+ int maxInputBlockSize = 0;
+ double sampleRate = 0.0;
+ yup::int64 outputPosition = 0;
+ bool timeStretchAvailable = false;
+
+ std::atomic timeRatio { 1.0 };
+ std::atomic pitchRatio { 1.0 };
+ std::atomic parametersDirty { false };
+ std::atomic currentInputPosition { 0 };
+
+ yup::AudioBuffer tempBuffer;
+ std::vector outputPointers;
+ yup::TimeStretchProcessor timeStretchProcessor;
+
+ static constexpr double minTimeRatio = 0.5;
+ static constexpr double maxTimeRatio = 2.0;
+ static constexpr double minPitchRatio = 0.5;
+ static constexpr double maxPitchRatio = 2.0;
+
+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TimeStretchAudioSource)
};
//==============================================================================
@@ -324,32 +492,48 @@ class AudioWaveformDisplay : public yup::Component
Demonstrates loading, visualizing, playing, and exporting audio files.
*/
class AudioFileDemo : public yup::Component
- , public yup::Timer
{
public:
AudioFileDemo()
: Component ("AudioFileDemo")
+ , waveformCache (std::make_shared())
, loadButton ("Load Audio")
, playButton ("Play")
, stopButton ("Stop")
, saveButton ("Save As")
, loopButton ("Loop")
+ , labelsButton ("Labels")
+ , waveformDisplay (waveformCache)
+ , meterState (48000.0, 2)
+ , leftMeter (meterState, 0)
+ , rightMeter (meterState, 1)
+ , meteringSource (&transportSource, meterState)
{
formatManager.registerDefaultFormats (
yup::AudioFormatType::all & ~yup::AudioFormatType::coreAudio);
deviceManager.initialiseWithDefaultDevices (0, 2);
deviceManager.addAudioCallback (&sourcePlayer);
- sourcePlayer.setSource (&transportSource);
+ sourcePlayer.setSource (&meteringSource);
+
+ // Configure K-Meters
+ meterState.setScale (yup::KMeterState::Scale::k20);
+ leftMeter.setShowPeakHold (true);
+ rightMeter.setShowPeakHold (true);
+
+ // Configure the waveform cache
+ waveformCache->setThreadPool (&waveformThreadPool);
+
+ timeStretchSupported = yup::TimeStretchProcessor::isBackendAvailable (yup::TimeStretchProcessor::Backend::automatic);
setupUi();
- startTimerHz (30);
}
~AudioFileDemo() override
{
stopPlayback();
transportSource.setSource (nullptr);
+ meteringSource.setLooping (false);
sourcePlayer.setSource (nullptr);
deviceManager.removeAudioCallback (&sourcePlayer);
deviceManager.closeAudioDevice();
@@ -358,12 +542,15 @@ class AudioFileDemo : public yup::Component
void resized() override
{
auto bounds = getLocalBounds().reduced (8);
- auto header = bounds.removeFromTop (38);
+ auto header = bounds.removeFromTop (156);
const int buttonHeight = 28;
- const int buttonWidth = 110;
+ const int buttonWidth = 100;
+ const int smallButtonWidth = 60;
+ const int mediumButtonWidth = 85;
const int buttonMargin = 6;
+ // First row of buttons
auto buttonRow = header.removeFromTop (buttonHeight);
loadButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
buttonRow.removeFromLeft (buttonMargin);
@@ -374,12 +561,65 @@ class AudioFileDemo : public yup::Component
saveButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
buttonRow.removeFromLeft (buttonMargin);
loopButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
+ buttonRow.removeFromLeft (buttonMargin);
+ labelsButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
+
+ // Second row for K-scale buttons
+ header.removeFromTop (4);
+ auto scaleRow = header.removeFromTop (buttonHeight);
+ k20Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
+ scaleRow.removeFromLeft (buttonMargin);
+ k14Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
+ scaleRow.removeFromLeft (buttonMargin);
+ k12Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
+
+ // Third row for metering standard buttons
+ header.removeFromTop (4);
+ auto standardRow = header.removeFromTop (buttonHeight);
+ rmsButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
+ standardRow.removeFromLeft (buttonMargin);
+ ituButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
+ standardRow.removeFromLeft (buttonMargin);
+ ebuButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
+
+ // Fourth row for over counter mode buttons
+ header.removeFromTop (4);
+ auto modeRow = header.removeFromTop (buttonHeight);
+ contiguousButton.setBounds (modeRow.removeFromLeft (mediumButtonWidth));
+ modeRow.removeFromLeft (buttonMargin);
+ totalButton.setBounds (modeRow.removeFromLeft (mediumButtonWidth));
+
+ // Fifth row for time/pitch controls
+ header.removeFromTop (4);
+ auto stretchRow = header.removeFromTop (buttonHeight);
+ const int sliderLabelWidth = 70;
+ const int sliderWidth = 180;
+
+ timeLabel.setBounds (stretchRow.removeFromLeft (sliderLabelWidth));
+ stretchRow.removeFromLeft (buttonMargin);
+ timeStretchSlider.setBounds (stretchRow.removeFromLeft (sliderWidth));
+ stretchRow.removeFromLeft (buttonMargin * 2);
+ pitchLabel.setBounds (stretchRow.removeFromLeft (sliderLabelWidth));
+ stretchRow.removeFromLeft (buttonMargin);
+ pitchShiftSlider.setBounds (stretchRow.removeFromLeft (sliderWidth));
bounds.removeFromTop (6);
infoLabel.setBounds (bounds.removeFromTop (22));
statusLabel.setBounds (bounds.removeFromTop (22));
bounds.removeFromTop (6);
+ // Reserve space for K-Meters on the right
+ const int meterWidth = 60;
+ const int meterGap = 8;
+ const int meterSectionWidth = (meterWidth * 2) + (meterGap * 3);
+
+ auto meterArea = bounds.removeFromRight (meterSectionWidth);
+ leftMeter.setBounds (meterArea.removeFromLeft (meterWidth));
+ meterArea.removeFromLeft (meterGap);
+ rightMeter.setBounds (meterArea.removeFromLeft (meterWidth));
+
+ // Rest is for waveform
+ bounds.removeFromRight (meterGap);
waveformDisplay.setBounds (bounds);
}
@@ -389,9 +629,32 @@ class AudioFileDemo : public yup::Component
g.fillAll();
}
+ void refreshDisplay (double) override
+ {
+ if (! hasLoadedAudio)
+ return;
+
+ if (transportSource.hasStreamFinished())
+ stopPlayback();
+
+ // Get the actual input position being read from the time stretch source
+ double inputSeconds = 0.0;
+ if (timeStretchSource != nullptr && loadedSampleRate > 0.0)
+ {
+ const auto inputPos = timeStretchSource->getInputPosition();
+ inputSeconds = static_cast (inputPos) / loadedSampleRate;
+ }
+
+ waveformDisplay.setPlayhead (inputSeconds, audioLengthSeconds);
+ updatePlaybackStatus();
+ }
+
private:
void setupUi()
{
+ addAndMakeVisible (leftMeter);
+ addAndMakeVisible (rightMeter);
+
addAndMakeVisible (loadButton);
loadButton.onClick = [this]
{
@@ -434,6 +697,114 @@ class AudioFileDemo : public yup::Component
memorySource->setLooping (loopEnabled);
};
+ addAndMakeVisible (labelsButton);
+ labelsButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ labelsButton.onClick = [this]
+ {
+ waveformDisplay.setChannelLabelsVisible (labelsButton.getToggleState());
+ };
+
+ // K-Scale selection buttons (manual radio button behavior)
+ addAndMakeVisible (k20Button);
+ k20Button.setButtonText ("K-20");
+ k20Button.setToggleState (true, yup::NotificationType::dontSendNotification);
+ k20Button.onClick = [this]
+ {
+ k20Button.setToggleState (true, yup::NotificationType::dontSendNotification);
+ k14Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k12Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ meterState.setScale (yup::KMeterState::Scale::k20);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ addAndMakeVisible (k14Button);
+ k14Button.setButtonText ("K-14");
+ k14Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k14Button.onClick = [this]
+ {
+ k20Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k14Button.setToggleState (true, yup::NotificationType::dontSendNotification);
+ k12Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ meterState.setScale (yup::KMeterState::Scale::k14);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ addAndMakeVisible (k12Button);
+ k12Button.setButtonText ("K-12");
+ k12Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k12Button.onClick = [this]
+ {
+ k20Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k14Button.setToggleState (false, yup::NotificationType::dontSendNotification);
+ k12Button.setToggleState (true, yup::NotificationType::dontSendNotification);
+ meterState.setScale (yup::KMeterState::Scale::k12);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ // Metering standard selection buttons
+ addAndMakeVisible (rmsButton);
+ rmsButton.setButtonText ("RMS Flat");
+ rmsButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ rmsButton.onClick = [this]
+ {
+ rmsButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ ituButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ebuButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::rmsFlat);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ addAndMakeVisible (ituButton);
+ ituButton.setButtonText ("ITU BS.1770-4");
+ ituButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ituButton.onClick = [this]
+ {
+ rmsButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ituButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ ebuButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::ituBS1770_4);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ addAndMakeVisible (ebuButton);
+ ebuButton.setButtonText ("EBU R128");
+ ebuButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ebuButton.onClick = [this]
+ {
+ rmsButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ituButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ ebuButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::ebuR128);
+ leftMeter.repaint();
+ rightMeter.repaint();
+ };
+
+ // Over counter mode selection buttons
+ addAndMakeVisible (contiguousButton);
+ contiguousButton.setButtonText ("Contiguous");
+ contiguousButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ contiguousButton.onClick = [this]
+ {
+ contiguousButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ totalButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ meterState.setOverCounterMode (yup::KMeterState::OverCounterMode::contiguous);
+ };
+
+ addAndMakeVisible (totalButton);
+ totalButton.setButtonText ("Total");
+ totalButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ totalButton.onClick = [this]
+ {
+ contiguousButton.setToggleState (false, yup::NotificationType::dontSendNotification);
+ totalButton.setToggleState (true, yup::NotificationType::dontSendNotification);
+ meterState.setOverCounterMode (yup::KMeterState::OverCounterMode::total);
+ };
+
addAndMakeVisible (infoLabel);
infoLabel.setText ("No audio loaded.", yup::NotificationType::dontSendNotification);
infoLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white);
@@ -442,20 +813,51 @@ class AudioFileDemo : public yup::Component
statusLabel.setText ("Choose an audio file to begin.", yup::NotificationType::dontSendNotification);
statusLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::lightgray);
- addAndMakeVisible (waveformDisplay);
- }
+ addAndMakeVisible (timeLabel);
+ timeLabel.setText ("Time", yup::NotificationType::dontSendNotification);
+ timeLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white);
+
+ addAndMakeVisible (timeStretchSlider);
+ timeStretchSlider.setSliderType (yup::Slider::LinearBarHorizontal);
+ timeStretchSlider.setRange (0.5, 2.0, 0.01);
+ timeStretchSlider.setValue (1.0, yup::NotificationType::dontSendNotification);
+ timeStretchSlider.setDefaultValue (1.0);
+ timeStretchSlider.setNumDecimalPlacesToDisplay (2);
+ timeStretchSlider.setTextBoxStyle (yup::Slider::TextBoxRight, false, 60, 20);
+ timeStretchSlider.onValueChanged = [this] (double value)
+ {
+ timeStretchRatio = value;
+ if (timeStretchSource != nullptr)
+ timeStretchSource->setTimeRatio (timeStretchRatio);
+ updatePlaybackStatus();
+ };
- void timerCallback() override
- {
- if (! hasLoadedAudio)
- return;
+ addAndMakeVisible (pitchLabel);
+ pitchLabel.setText ("Pitch", yup::NotificationType::dontSendNotification);
+ pitchLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white);
+
+ addAndMakeVisible (pitchShiftSlider);
+ pitchShiftSlider.setSliderType (yup::Slider::LinearBarHorizontal);
+ pitchShiftSlider.setRange (0.5, 2.0, 0.01);
+ pitchShiftSlider.setValue (1.0, yup::NotificationType::dontSendNotification);
+ pitchShiftSlider.setDefaultValue (1.0);
+ pitchShiftSlider.setNumDecimalPlacesToDisplay (2);
+ pitchShiftSlider.setTextBoxStyle (yup::Slider::TextBoxRight, false, 60, 20);
+ pitchShiftSlider.onValueChanged = [this] (double value)
+ {
+ pitchShiftRatio = value;
+ if (timeStretchSource != nullptr)
+ timeStretchSource->setPitchRatio (pitchShiftRatio);
+ };
- if (transportSource.hasStreamFinished())
- stopPlayback();
+ timeStretchSlider.setEnabled (timeStretchSupported);
+ pitchShiftSlider.setEnabled (timeStretchSupported);
+ timeLabel.setEnabled (timeStretchSupported);
+ pitchLabel.setEnabled (timeStretchSupported);
- waveformDisplay.setPlayhead (transportSource.getCurrentPosition(),
- audioLengthSeconds);
- updatePlaybackStatus();
+ addAndMakeVisible (waveformDisplay);
+ waveformDisplay.setSelectable (true);
+ waveformDisplay.setChannelLabelsVisible (labelsButton.getToggleState());
}
void updateStatus (const yup::String& newStatus)
@@ -465,7 +867,7 @@ class AudioFileDemo : public yup::Component
void updatePlaybackStatus()
{
- const double lengthSeconds = audioLengthSeconds;
+ const double lengthSeconds = audioLengthSeconds * (timeStretchRatio > 0.0 ? timeStretchRatio : 1.0);
const double positionSeconds = transportSource.getCurrentPosition();
yup::String positionText = formatTime (positionSeconds) + " / " + formatTime (lengthSeconds);
@@ -503,6 +905,8 @@ class AudioFileDemo : public yup::Component
transportSource.stop();
transportSource.setPosition (0.0);
playButton.setButtonText ("Play");
+ waveformDisplay.setPlayhead (0.0, audioLengthSeconds);
+ updatePlaybackStatus();
}
void loadAudioFile (const yup::File& file)
@@ -535,9 +939,13 @@ class AudioFileDemo : public yup::Component
transportSource.stop();
transportSource.setSource (nullptr);
memorySource = std::make_unique (audioBuffer, false, loopEnabled);
- transportSource.setSource (memorySource.get(), 0, nullptr, loadedSampleRate, numChannels);
+ timeStretchSource = std::make_unique (memorySource.get(), numChannels);
+ timeStretchSource->setTimeRatio (timeStretchRatio);
+ timeStretchSource->setPitchRatio (pitchShiftRatio);
+ transportSource.setSource (timeStretchSource.get(), 0, nullptr, loadedSampleRate, numChannels);
- waveformDisplay.setAudioBuffer (&audioBuffer);
+ waveformDisplay.setSource (&audioBuffer, loadedSampleRate);
+ waveformDisplay.setPlayhead (0.0, audioLengthSeconds);
updateStatus ("Loaded " + file.getFileName() + " | " + yup::String (numChannels)
+ " ch | " + yup::String (loadedSampleRate, 1) + " Hz | "
+ formatTime (audioLengthSeconds));
@@ -610,6 +1018,7 @@ class AudioFileDemo : public yup::Component
yup::String getAudioFileFilter() const
{
+ // TODO - Add methods to audioformatmanager to get supported formats dynamically
return "*.wav;*.aiff;*.aif;*.flac;*.mp3;*.opus;*.m4a;*.wma;*.ogg";
}
@@ -618,21 +1027,48 @@ class AudioFileDemo : public yup::Component
yup::AudioSourcePlayer sourcePlayer;
yup::AudioTransportSource transportSource;
std::unique_ptr memorySource;
+ std::unique_ptr timeStretchSource;
yup::AudioBuffer audioBuffer;
+ yup::ThreadPool waveformThreadPool;
+ std::shared_ptr waveformCache;
+
yup::TextButton loadButton;
yup::TextButton playButton;
yup::TextButton stopButton;
yup::TextButton saveButton;
yup::ToggleButton loopButton;
+ yup::ToggleButton labelsButton;
+ yup::ToggleButton k20Button;
+ yup::ToggleButton k14Button;
+ yup::ToggleButton k12Button;
+ yup::ToggleButton rmsButton;
+ yup::ToggleButton ituButton;
+ yup::ToggleButton ebuButton;
+ yup::ToggleButton contiguousButton;
+ yup::ToggleButton totalButton;
+
+ yup::Label timeLabel;
+ yup::Label pitchLabel;
+ yup::Slider timeStretchSlider { yup::Slider::LinearBarHorizontal, "Time Stretch" };
+ yup::Slider pitchShiftSlider { yup::Slider::LinearBarHorizontal, "Pitch Shift" };
yup::Label infoLabel;
yup::Label statusLabel;
- AudioWaveformDisplay waveformDisplay;
+ AudioFileWaveform waveformDisplay;
+
+ // K-Meter components
+ yup::KMeterState meterState;
+ yup::KMeterComponent leftMeter;
+ yup::KMeterComponent rightMeter;
+ MeteringAudioSource meteringSource;
yup::String currentFileName { "No audio loaded" };
double loadedSampleRate = 0.0;
double audioLengthSeconds = 0.0;
+ double timeStretchRatio = 1.0;
+ double pitchShiftRatio = 1.0;
bool hasLoadedAudio = false;
bool loopEnabled = false;
+ bool timeStretchSupported = false;
};
diff --git a/examples/graphics/source/examples/ConvolutionDemo.h b/examples/graphics/source/examples/ConvolutionDemo.h
index fe6b03c57..210c33ec5 100644
--- a/examples/graphics/source/examples/ConvolutionDemo.h
+++ b/examples/graphics/source/examples/ConvolutionDemo.h
@@ -353,9 +353,9 @@ class ConvolutionDemo
wetGainSlider.setRange (0.0, 2.0);
wetGainSlider.setValue (1.0);
- wetGainSlider.onValueChanged = [this] (float value)
+ wetGainSlider.onValueChanged = [this] (double value)
{
- wetGain.setTargetValue (value);
+ wetGain.setTargetValue ((float) value);
};
addAndMakeVisible (wetGainSlider);
@@ -366,9 +366,9 @@ class ConvolutionDemo
dryGainSlider.setRange (0.0, 2.0);
dryGainSlider.setValue (0.3);
- dryGainSlider.onValueChanged = [this] (float value)
+ dryGainSlider.onValueChanged = [this] (double value)
{
- dryGain.setTargetValue (value);
+ dryGain.setTargetValue ((float) value);
};
addAndMakeVisible (dryGainSlider);
@@ -422,6 +422,9 @@ class ConvolutionDemo
// Create waveform data points
const size_t numPoints = std::min (static_cast (getWidth()), length);
+ if (numPoints == 0)
+ return;
+
const size_t stride = length / numPoints;
std::vector> waveformData;
diff --git a/examples/graphics/source/examples/CrossoverDemo.h b/examples/graphics/source/examples/CrossoverDemo.h
index 15f7cc56e..13e4dbd52 100644
--- a/examples/graphics/source/examples/CrossoverDemo.h
+++ b/examples/graphics/source/examples/CrossoverDemo.h
@@ -309,9 +309,9 @@ class CrossoverDemo : public yup::Component
freqSlider.setSkewFactorFromMidpoint (1000.0);
freqSlider.setValue (1000.0);
//freqSlider.setTextValueSuffix (" Hz");
- freqSlider.onValueChanged = [this] (float value)
+ freqSlider.onValueChanged = [this] (double value)
{
- crossoverFreq.setTargetValue (value);
+ crossoverFreq.setTargetValue ((float) value);
setCrossoverFrequency (value);
};
addAndMakeVisible (freqSlider);
@@ -326,9 +326,9 @@ class CrossoverDemo : public yup::Component
lowGainSlider.setRange (0.0, 2.0);
lowGainSlider.setValue (1.0);
//lowGainSlider.setTextValueSuffix (" x");
- lowGainSlider.onValueChanged = [this] (float value)
+ lowGainSlider.onValueChanged = [this] (double value)
{
- lowGain.setTargetValue (value);
+ lowGain.setTargetValue ((float) value);
};
addAndMakeVisible (lowGainSlider);
@@ -342,9 +342,9 @@ class CrossoverDemo : public yup::Component
highGainSlider.setRange (0.0, 2.0);
highGainSlider.setValue (1.0);
//highGainSlider.setTextValueSuffix (" x");
- highGainSlider.onValueChanged = [this] (float value)
+ highGainSlider.onValueChanged = [this] (double value)
{
- highGain.setTargetValue (value);
+ highGain.setTargetValue ((float) value);
};
addAndMakeVisible (highGainSlider);
diff --git a/examples/graphics/source/examples/FilterDemo.h b/examples/graphics/source/examples/FilterDemo.h
index 1733782bf..488fc7483 100644
--- a/examples/graphics/source/examples/FilterDemo.h
+++ b/examples/graphics/source/examples/FilterDemo.h
@@ -44,7 +44,7 @@ class PhaseResponseDisplay : public yup::Component
auto bounds = getLocalBounds();
// Background
- g.setFillColor (yup::Color (0xFF1E1E1E));
+ g.setFillColor (yup::Color (0xff1e1e1e));
g.fillRect (bounds);
// Reserve space for labels
@@ -165,7 +165,7 @@ class GroupDelayDisplay : public yup::Component
auto bounds = getLocalBounds();
// Background
- g.setFillColor (yup::Color (0xFF1E1E1E));
+ g.setFillColor (yup::Color (0xff1e1e1e));
g.fillRect (bounds);
// Reserve space for labels
@@ -281,7 +281,7 @@ class StepResponseDisplay : public yup::Component
auto bounds = getLocalBounds();
// Background
- g.setFillColor (yup::Color (0xFF1E1E1E));
+ g.setFillColor (yup::Color (0xff1e1e1e));
g.fillRect (bounds);
// Reserve space for labels
@@ -403,7 +403,7 @@ class PolesZerosDisplay : public yup::Component
auto bounds = getLocalBounds();
// Background
- g.setFillColor (yup::Color (0xFF1E1E1E));
+ g.setFillColor (yup::Color (0xff1e1e1e));
g.fillRect (bounds);
// Reserve space for labels
@@ -559,7 +559,7 @@ class FrequencyResponsePlot : public yup::Component
auto bounds = getLocalBounds();
// Background
- g.setFillColor (yup::Color (0xff1a1a1a));
+ g.setFillColor (yup::Color (0xff1e1e1e));
g.fillAll();
// Reserve space for labels
diff --git a/examples/graphics/source/examples/ScrollBarDemo.h b/examples/graphics/source/examples/ScrollBarDemo.h
new file mode 100644
index 000000000..2dce37333
--- /dev/null
+++ b/examples/graphics/source/examples/ScrollBarDemo.h
@@ -0,0 +1,337 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2025 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#pragma once
+
+class ScrollBarDemo : public yup::Component
+{
+public:
+ ScrollBarDemo()
+ : Component ("ScrollBarDemo")
+ {
+ // Set want to grab focus
+ setWantsKeyboardFocus (true);
+
+ // Create a large virtual canvas (2000x2000)
+ canvasWidth = 2000.0f;
+ canvasHeight = 2000.0f;
+
+ // Create vertical scrollbar
+ verticalScrollBar = std::make_unique (yup::ScrollBar::Orientation::vertical);
+ verticalScrollBar->setVisibilityMode (yup::ScrollBar::VisibilityMode::alwaysVisible);
+ verticalScrollBar->setRangeLimits (0.0, canvasHeight);
+ verticalScrollBar->onScrollPositionChanged = [this] (double newPosition)
+ {
+ scrollY = static_cast (newPosition);
+ repaint();
+ };
+ addAndMakeVisible (verticalScrollBar.get());
+
+ // Create horizontal scrollbar
+ horizontalScrollBar = std::make_unique (yup::ScrollBar::Orientation::horizontal);
+ horizontalScrollBar->setVisibilityMode (yup::ScrollBar::VisibilityMode::alwaysVisible);
+ horizontalScrollBar->setRangeLimits (0.0, canvasWidth);
+ horizontalScrollBar->onScrollPositionChanged = [this] (double newPosition)
+ {
+ scrollX = static_cast (newPosition);
+ repaint();
+ };
+ addAndMakeVisible (horizontalScrollBar.get());
+ }
+
+ void resized() override
+ {
+ auto bounds = getLocalBounds();
+ auto scrollBarSize = 15.0f;
+
+ // Set the scrollbar width to match our layout
+ verticalScrollBar->setScrollBarWidth (scrollBarSize);
+ horizontalScrollBar->setScrollBarWidth (scrollBarSize);
+
+ // Position vertical scrollbar on the right (full height minus horizontal scrollbar)
+ verticalScrollBar->setBounds (
+ bounds.getWidth() - scrollBarSize,
+ 0.0f,
+ scrollBarSize,
+ bounds.getHeight() - scrollBarSize);
+
+ // Position horizontal scrollbar at the bottom (full width minus vertical scrollbar)
+ horizontalScrollBar->setBounds (
+ 0.0f,
+ bounds.getHeight() - scrollBarSize,
+ bounds.getWidth() - scrollBarSize,
+ scrollBarSize);
+
+ // Calculate viewport size (excluding scrollbar areas)
+ auto viewportWidth = bounds.getWidth() - scrollBarSize;
+ auto viewportHeight = bounds.getHeight() - scrollBarSize;
+
+ // Clamp scroll positions to valid range
+ scrollX = yup::jlimit (0.0f, canvasWidth - viewportWidth, scrollX);
+ scrollY = yup::jlimit (0.0f, canvasHeight - viewportHeight, scrollY);
+
+ // Update scrollbar ranges - the range represents the visible portion
+ verticalScrollBar->setCurrentRange (scrollY, scrollY + viewportHeight);
+ horizontalScrollBar->setCurrentRange (scrollX, scrollX + viewportWidth);
+ }
+
+ void paint (yup::Graphics& g) override
+ {
+ auto bounds = getLocalBounds();
+ auto scrollBarSize = 15.0f;
+ auto viewportWidth = bounds.getWidth() - scrollBarSize;
+ auto viewportHeight = bounds.getHeight() - scrollBarSize;
+
+ // Draw background for entire component (including scrollbar areas)
+ g.setFillColor (yup::Color (0xff1a1a1a));
+ g.fillAll();
+
+ // Define the viewport (content area without scrollbars)
+ yup::Rectangle viewport (0.0f, 0.0f, viewportWidth, viewportHeight);
+
+ // Calculate visible region in canvas coordinates
+ float visibleLeft = scrollX;
+ float visibleTop = scrollY;
+ float visibleRight = scrollX + viewportWidth;
+ float visibleBottom = scrollY + viewportHeight;
+
+ // Draw content with clipping and translation
+ {
+ auto state = g.saveState();
+
+ // Translate to viewport origin (no scrolling applied yet)
+ auto currentTransform = g.getTransform();
+ g.setTransform (currentTransform.translated (-scrollX, -scrollY));
+
+ // Clip path ?
+ auto bounds = getBounds();
+ g.setClipPath (yup::Rectangle (getLeft() + visibleLeft, getTop() + visibleTop, viewportWidth, viewportHeight));
+
+ // Draw canvas background (only visible portion)
+ g.setFillColor (yup::Color (0xff2a2a2a));
+ g.fillRect (visibleLeft, visibleTop, viewportWidth, viewportHeight);
+
+ // Draw grid lines (only in visible area)
+ g.setStrokeColor (yup::Colors::gray.withAlpha (0.2f));
+ g.setStrokeWidth (1.0f);
+
+ // Vertical grid lines - only draw visible ones
+ float startX = std::floor (visibleLeft / 100.0f) * 100.0f;
+ for (float x = startX; x <= visibleRight; x += 100.0f)
+ g.strokeLine (x, visibleTop, x, visibleBottom);
+
+ // Horizontal grid lines - only draw visible ones
+ float startY = std::floor (visibleTop / 100.0f) * 100.0f;
+ for (float y = startY; y <= visibleBottom; y += 100.0f)
+ g.strokeLine (visibleLeft, y, visibleRight, y);
+
+ // Draw major grid lines (only visible ones)
+ g.setStrokeColor (yup::Colors::gray.withAlpha (0.4f));
+ g.setStrokeWidth (2.0f);
+
+ startX = std::floor (visibleLeft / 500.0f) * 500.0f;
+ for (float x = startX; x <= visibleRight; x += 500.0f)
+ g.strokeLine (x, visibleTop, x, visibleBottom);
+
+ startY = std::floor (visibleTop / 500.0f) * 500.0f;
+ for (float y = startY; y <= visibleBottom; y += 500.0f)
+ g.strokeLine (visibleLeft, y, visibleRight, y);
+
+ // Draw colorful shapes (only visible ones)
+ drawShapes (g, visibleLeft, visibleTop, visibleRight, visibleBottom);
+
+ // Draw coordinate labels (only visible ones)
+ drawCoordinateLabels (g, visibleLeft, visibleTop, visibleRight, visibleBottom);
+ }
+
+ // Draw viewport border
+ g.setStrokeColor (yup::Colors::white.withAlpha (0.3f));
+ g.setStrokeWidth (1.0f);
+ g.strokeRect (viewport);
+
+ // Draw scroll position indicator (always on top)
+ drawScrollIndicator (g, viewportWidth, viewportHeight);
+ }
+
+ void mouseDown (const yup::MouseEvent& event) override
+ {
+ takeKeyboardFocus();
+ }
+
+ void mouseWheel (const yup::MouseEvent& event, const yup::MouseWheelData& wheelData) override
+ {
+ // Handle mouse wheel scrolling
+ auto bounds = getLocalBounds();
+ auto scrollBarSize = 15.0f;
+ auto viewportWidth = bounds.getWidth() - scrollBarSize;
+ auto viewportHeight = bounds.getHeight() - scrollBarSize;
+
+ // Vertical scrolling
+ if (std::abs (wheelData.getDeltaY()) > std::abs (wheelData.getDeltaX()))
+ {
+ auto newScrollY = scrollY - wheelData.getDeltaY() * 20.0f;
+ newScrollY = yup::jlimit (0.0f, canvasHeight - viewportHeight, newScrollY);
+ verticalScrollBar->setCurrentRangeStart (newScrollY);
+ }
+ // Horizontal scrolling
+ else
+ {
+ auto newScrollX = scrollX - wheelData.getDeltaX() * 20.0f;
+ newScrollX = yup::jlimit (0.0f, canvasWidth - viewportWidth, newScrollX);
+ horizontalScrollBar->setCurrentRangeStart (newScrollX);
+ }
+ }
+
+private:
+ void drawShapes (yup::Graphics& g, float visibleLeft, float visibleTop, float visibleRight, float visibleBottom)
+ {
+ // Draw a pattern of colored shapes across the canvas (only visible ones)
+ for (int row = 0; row < 4; ++row)
+ {
+ for (int col = 0; col < 4; ++col)
+ {
+ float x = 250.0f + col * 500.0f;
+ float y = 250.0f + row * 500.0f;
+ float size = 150.0f;
+
+ // Skip shapes outside visible area (with some margin)
+ if (x + size / 2 < visibleLeft - 50.0f || x - size / 2 > visibleRight + 50.0f || y + size / 2 < visibleTop - 50.0f || y - size / 2 > visibleBottom + 50.0f)
+ {
+ continue;
+ }
+
+ // Alternate between different shapes and colors
+ int shapeType = (row * 4 + col) % 4;
+
+ switch (shapeType)
+ {
+ case 0: // Red circle
+ g.setFillColor (yup::Colors::red.withAlpha (0.7f));
+ g.fillEllipse (x - size / 2, y - size / 2, size, size);
+ break;
+
+ case 1: // Green rectangle
+ g.setFillColor (yup::Colors::green.withAlpha (0.7f));
+ g.fillRect (x - size / 2, y - size / 2, size, size);
+ break;
+
+ case 2: // Blue rounded rectangle
+ g.setFillColor (yup::Colors::blue.withAlpha (0.7f));
+ g.fillRoundedRect (x - size / 2, y - size / 2, size, size, 20.0f);
+ break;
+
+ case 3: // Yellow triangle (using path)
+ {
+ yup::Path triangle;
+ triangle.moveTo (x, y - size / 2);
+ triangle.lineTo (x + size / 2, y + size / 2);
+ triangle.lineTo (x - size / 2, y + size / 2);
+ triangle.closeSubPath();
+
+ g.setFillColor (yup::Colors::yellow.withAlpha (0.7f));
+ g.fillPath (triangle);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ void drawCoordinateLabels (yup::Graphics& g, float visibleLeft, float visibleTop, float visibleRight, float visibleBottom)
+ {
+ auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont();
+
+ // Draw coordinate labels at major grid intersections (only visible ones)
+ float startX = std::floor (visibleLeft / 500.0f) * 500.0f;
+ float startY = std::floor (visibleTop / 500.0f) * 500.0f;
+
+ for (float x = startX; x <= visibleRight && x <= canvasWidth; x += 500.0f)
+ {
+ for (float y = startY; y <= visibleBottom && y <= canvasHeight; y += 500.0f)
+ {
+ yup::String label = "(" + yup::String (static_cast (x)) + ", " + yup::String (static_cast (y)) + ")";
+
+ yup::StyledText text;
+ {
+ auto modifier = text.startUpdate();
+ modifier.setMaxSize ({ 120.0f, 20.0f });
+ modifier.setHorizontalAlign (yup::StyledText::left);
+ modifier.appendText (label, font);
+ }
+
+ g.setFillColor (yup::Colors::white);
+ g.fillFittedText (text, yup::Rectangle (x + 10.0f, y + 10.0f, 120.0f, 20.0f));
+ }
+ }
+ }
+
+ void drawScrollIndicator (yup::Graphics& g, float viewportWidth, float viewportHeight)
+ {
+ auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont();
+
+ // Draw scroll position in top-left corner
+ yup::String info = "Scroll: (" + yup::String (static_cast (scrollX)) + ", " + yup::String (static_cast (scrollY)) + ")";
+
+ yup::StyledText scrollText;
+ {
+ auto modifier = scrollText.startUpdate();
+ modifier.setMaxSize ({ 150.0f, 20.0f });
+ modifier.setHorizontalAlign (yup::StyledText::left);
+ modifier.appendText (info, font);
+ }
+
+ // Draw background for text
+ g.setFillColor (yup::Colors::black.withAlpha (0.7f));
+ g.fillRoundedRect (10.0f, 10.0f, 160.0f, 30.0f, 5.0f);
+
+ // Draw text
+ g.setFillColor (yup::Colors::white);
+ g.fillFittedText (scrollText, yup::Rectangle (15.0f, 15.0f, 150.0f, 20.0f));
+
+ // Draw canvas info in top-right corner
+ yup::String canvasInfo = "Canvas: " + yup::String (static_cast (canvasWidth)) + "x" + yup::String (static_cast (canvasHeight));
+
+ yup::StyledText canvasText;
+ {
+ auto modifier = canvasText.startUpdate();
+ modifier.setMaxSize ({ 150.0f, 20.0f });
+ modifier.setHorizontalAlign (yup::StyledText::left);
+ modifier.appendText (canvasInfo, font);
+ }
+
+ g.setFillColor (yup::Colors::black.withAlpha (0.7f));
+ g.fillRoundedRect (viewportWidth - 170.0f, 10.0f, 160.0f, 30.0f, 5.0f);
+
+ g.setFillColor (yup::Colors::white);
+ g.fillFittedText (canvasText, yup::Rectangle (viewportWidth - 165.0f, 15.0f, 150.0f, 20.0f));
+ }
+
+private:
+ std::unique_ptr verticalScrollBar;
+ std::unique_ptr horizontalScrollBar;
+
+ float canvasWidth = 2000.0f;
+ float canvasHeight = 2000.0f;
+ float scrollX = 0.0f;
+ float scrollY = 0.0f;
+
+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScrollBarDemo)
+};
diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h
index 08c76f66d..2f03c93e4 100644
--- a/examples/graphics/source/examples/Svg.h
+++ b/examples/graphics/source/examples/Svg.h
@@ -21,9 +21,6 @@
#pragma once
-namespace yup
-{
-
class SvgDemo : public yup::Component
{
public:
@@ -46,7 +43,7 @@ class SvgDemo : public yup::Component
parseSvgFile (currentSvgFileIndex);
}
- void paint (Graphics& g) override
+ void paint (yup::Graphics& g) override
{
g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray));
g.fillAll();
@@ -92,8 +89,6 @@ class SvgDemo : public yup::Component
}
yup::Drawable drawable;
- Array svgFiles;
+ yup::Array svgFiles;
int currentSvgFileIndex = 0;
};
-
-} // namespace yup
diff --git a/examples/graphics/source/examples/Widgets.h b/examples/graphics/source/examples/Widgets.h
index 620c7d59d..21a9988e2 100644
--- a/examples/graphics/source/examples/Widgets.h
+++ b/examples/graphics/source/examples/Widgets.h
@@ -116,6 +116,38 @@ class WidgetsDemo : public yup::Component
textEditor->setText ("Type some text here...", yup::dontSendNotification);
textEditor->setMultiLine (true);
addAndMakeVisible (textEditor.get());
+
+ // Progress Bar (normal mode - linked to slider)
+ progressBar = std::make_unique ("progressBar");
+ progressBar->setProgress (0.5, yup::dontSendNotification);
+ progressBar->onProgressChanged = [this] (double value)
+ {
+ if (value >= 0.0)
+ updateStatus ("Progress: " + yup::String (value * 100.0, 0) + "%");
+ };
+ addAndMakeVisible (progressBar.get());
+
+ // Progress Bar Label
+ progressBarLabel = std::make_unique ("progressBarLabel");
+ progressBarLabel->setText ("Progress Bar (linked to slider):", yup::dontSendNotification);
+ addAndMakeVisible (progressBarLabel.get());
+
+ // Indeterminate Progress Bar
+ indeterminateProgressBar = std::make_unique ("indeterminateProgressBar");
+ indeterminateProgressBar->setProgress (-1.0, yup::dontSendNotification);
+ addAndMakeVisible (indeterminateProgressBar.get());
+
+ // Indeterminate Progress Bar Label
+ indeterminateLabel = std::make_unique ("indeterminateLabel");
+ indeterminateLabel->setText ("Indeterminate Progress Bar:", yup::dontSendNotification);
+ addAndMakeVisible (indeterminateLabel.get());
+
+ // Update slider to control progress bar
+ slider->onValueChanged = [this] (double value)
+ {
+ updateStatus ("Slider value: " + yup::String (value, 1));
+ progressBar->setProgress (value / 100.0, yup::dontSendNotification);
+ };
}
void setupLayout()
@@ -162,11 +194,22 @@ class WidgetsDemo : public yup::Component
textEditor->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (bounds.getWidth() - 2 * margin), 100.0f));
y += 110;
- // Viewport
- //viewport->setBounds (yup::Rectangle (static_cast(margin), static_cast(y), static_cast(inputWidth), 150.0f));
-
// Slider
slider->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (inputWidth / 2), static_cast (inputWidth / 2)));
+ y += static_cast (inputWidth / 2) + spacing * 2;
+
+ // Progress Bar (normal mode)
+ progressBarLabel->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (bounds.getWidth() - 2 * margin), 20.0f));
+ y += 25;
+
+ progressBar->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (bounds.getWidth() - 2 * margin), static_cast (componentHeight)));
+ y += componentHeight + spacing * 2;
+
+ // Indeterminate Progress Bar
+ indeterminateLabel->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (bounds.getWidth() - 2 * margin), 20.0f));
+ y += 25;
+
+ indeterminateProgressBar->setBounds (yup::Rectangle (static_cast (margin), static_cast (y), static_cast (bounds.getWidth() - 2 * margin), static_cast (componentHeight)));
}
void paint (yup::Graphics& g) override
@@ -211,6 +254,10 @@ class WidgetsDemo : public yup::Component
std::unique_ptr contentLabel;
std::unique_ptr slider;
std::unique_ptr textEditor;
+ std::unique_ptr progressBar;
+ std::unique_ptr progressBarLabel;
+ std::unique_ptr indeterminateProgressBar;
+ std::unique_ptr indeterminateLabel;
YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WidgetsDemo)
};
diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp
index fbce909d9..a943ca89a 100644
--- a/examples/graphics/source/main.cpp
+++ b/examples/graphics/source/main.cpp
@@ -25,7 +25,6 @@
#include
#include
#include
-
#if YUP_MODULE_AVAILABLE_yup_python
#include
#endif
@@ -40,28 +39,62 @@
#include "examples/Artboard.h"
#include "examples/Audio.h"
#include "examples/AudioFileDemo.h"
-#include "examples/CrossoverDemo.h"
-#include "examples/ConvolutionDemo.h"
#include "examples/ColorLab.h"
+#include "examples/ConvolutionDemo.h"
+#include "examples/CrossoverDemo.h"
+#include "examples/FileChooser.h"
#include "examples/FilterDemo.h"
#include "examples/LayoutFonts.h"
-#include "examples/FileChooser.h"
#include "examples/OpaqueDemo.h"
#include "examples/Paths.h"
#include "examples/PopupMenu.h"
+#include "examples/ScrollBarDemo.h"
#include "examples/SliderDemo.h"
#include "examples/SpectrumAnalyzer.h"
-#include "examples/TextEditor.h"
#include "examples/Svg.h"
+#include "examples/TextEditor.h"
#include "examples/VariableFonts.h"
#include "examples/Widgets.h"
-
#if YUP_MODULE_AVAILABLE_yup_python
#include "examples/Python.h"
#endif
//==============================================================================
+class DemoListModel : public yup::ListBoxModel
+{
+public:
+ DemoListModel (yup::Array names)
+ : demoNames (std::move (names))
+ {
+ }
+
+ int getNumRows() override
+ {
+ return demoNames.size();
+ }
+
+ yup::String getRowText (int rowIndex) override
+ {
+ if (rowIndex >= 0 && rowIndex < demoNames.size())
+ return demoNames[rowIndex];
+ return {};
+ }
+
+ void selectedRowsChanged (const yup::Array& selectedRows) override
+ {
+ if (onSelectionChanged && ! selectedRows.isEmpty())
+ onSelectionChanged (selectedRows[0]);
+ }
+
+ std::function onSelectionChanged;
+
+private:
+ yup::Array demoNames;
+};
+
+//==============================================================================
+
class CustomWindow
: public yup::DocumentWindow
, public yup::Timer
@@ -108,40 +141,43 @@ class CustomWindow
registerDemo ("Audio", counter++);
registerDemo ("Audio File", counter++);
- registerDemo ("FFT Analyzer", counter++);
- registerDemo ("Filter Demo", counter++);
- registerDemo ("Crossover Demo", counter++);
- registerDemo ("Convolution Demo", counter++);
registerDemo ("Color Lab", counter++);
+ registerDemo ("Convolution Demo", counter++);
+ registerDemo ("Crossover Demo", counter++);
+ registerDemo ("File Chooser", counter++);
+ registerDemo ("Filter Demo", counter++);
registerDemo ("Layout Fonts", counter++);
- registerDemo ("Variable Fonts", counter++);
+ registerDemo ("Opaque Demo", counter++);
registerDemo ("Paths", counter++);
- registerDemo ("Text Editor", counter++);
registerDemo ("Popup Menu", counter++);
- registerDemo ("File Chooser", counter++);
+ registerDemo ("ScrollBar", counter++);
registerDemo ("Sliders", counter++);
+ registerDemo ("FFT Analyzer", counter++);
+ registerDemo ("Text Editor", counter++);
+ registerDemo ("Variable Fonts", counter++);
registerDemo ("Widgets", counter++);
registerDemo ("Artboard", counter++, [] (auto& artboard)
{
jassert (artboard.loadArtboard());
});
- registerDemo ("Opaque Demo", counter++);
+ registerDemo ("SVG", counter++);
#if YUP_MODULE_AVAILABLE_yup_python
registerDemo ("Python", counter++);
#endif
+ // Create the ListBox with the demo names
+ listModel = std::make_unique (demoNames);
+ listModel->onSelectionChanged = [this] (int index)
{
- auto button = std::make_unique ("SVG");
- button->onClick = [this, number = counter++]
- {
- selectComponent (number);
- };
- addAndMakeVisible (button.get());
- buttons.add (std::move (button));
+ selectComponent (index);
+ };
- components.add (std::make_unique());
- addChildComponent (components.getLast());
- }
+ listBox = std::make_unique();
+ listBox->setModel (listModel.get());
+ listBox->setRowHeight (30);
+ listBox->setRowWidth (200);
+ listBox->selectRow (0, false, yup::dontSendNotification);
+ addAndMakeVisible (listBox.get());
selectComponent (0);
@@ -155,34 +191,46 @@ class CustomWindow
void resized() override
{
constexpr auto margin = 5;
- constexpr auto buttonsPerRow = 6;
+ constexpr auto listBoxWidth = 200;
+ constexpr auto listBoxHeight = 40;
auto bounds = getLocalBounds().reduced (margin);
- auto initialBounds = bounds;
- auto buttonBounds = initialBounds;
+ auto width = bounds.getWidth();
+ auto height = bounds.getHeight();
+
+ // Landscape orientation (width > height): vertical ListBox on the left
+ if (width > height)
+ {
+ listBox->setOrientation (yup::ListBox::Orientation::vertical);
+ listBox->setRowHeight (30);
+ listBox->setVerticalScrollBarVisibility (yup::ScrollBar::VisibilityMode::autoHide);
+ listBox->setHorizontalScrollBarVisibility (yup::ScrollBar::VisibilityMode::alwaysHidden);
- const auto totalMargin = margin * (buttons.size() - 1);
- const auto buttonWidth = (bounds.getWidth() - totalMargin) / buttonsPerRow;
+ auto listBoxBounds = bounds.removeFromLeft (listBoxWidth);
+ listBox->setBounds (listBoxBounds);
- int buttonsInRow = 0;
- for (auto& button : buttons)
+ // Add margin between ListBox and demo components
+ bounds.removeFromLeft (margin);
+ }
+ // Portrait orientation (width <= height): horizontal ListBox on top
+ else
{
- if (buttonsInRow == 0)
- buttonBounds = initialBounds.removeFromTop (30);
+ listBox->setOrientation (yup::ListBox::Orientation::horizontal);
+ listBox->setRowWidth (80);
+ listBox->setRowHeight (listBoxHeight);
+ listBox->setVerticalScrollBarVisibility (yup::ScrollBar::VisibilityMode::alwaysHidden);
+ listBox->setHorizontalScrollBarVisibility (yup::ScrollBar::VisibilityMode::autoHide);
- button->setBounds (buttonBounds.removeFromLeft (buttonWidth));
- buttonBounds.removeFromLeft (margin);
+ auto listBoxBounds = bounds.removeFromTop (listBoxHeight);
+ listBox->setBounds (listBoxBounds);
- if (++buttonsInRow == buttonsPerRow)
- {
- initialBounds.removeFromTop (margin);
- buttonsInRow = 0;
- }
+ // Add margin between ListBox and demo components
+ bounds.removeFromTop (margin);
}
- initialBounds.removeFromTop (margin);
+ // Demo components take the remaining space
for (auto& component : components)
- component->setBounds (initialBounds);
+ component->setBounds (bounds);
}
void paint (yup::Graphics& g) override
@@ -248,13 +296,7 @@ class CustomWindow
template
void registerDemo (const yup::String& name, int counter, std::function setup = nullptr)
{
- auto button = std::make_unique (name);
- button->onClick = [this, counter]
- {
- selectComponent (counter);
- };
- addAndMakeVisible (button.get());
- buttons.add (std::move (button));
+ demoNames.add (name);
auto demo = std::make_unique();
auto& demoInstance = *demo.get();
@@ -283,11 +325,12 @@ class CustomWindow
setTitle (title);
}
- yup::OwnedArray buttons;
+ yup::Array demoNames;
+ std::unique_ptr listModel;
+ std::unique_ptr listBox;
yup::OwnedArray components;
yup::Font font;
-
yup::Image image;
};
@@ -325,7 +368,7 @@ struct Application : yup::YUPApplication
window->centreWithSize ({ 1080, 2400 });
// window->setFullScreen(true);
#else
- window->centreWithSize ({ 600, 800 });
+ window->centreWithSize ({ 1024, 768 });
#endif
window->setVisible (true);
diff --git a/justfile b/justfile
index 470e59cec..b907286a9 100644
--- a/justfile
+++ b/justfile
@@ -32,8 +32,8 @@ ninja PROFILING="OFF":
[doc("generate and open project in Windows using Visual Studio")]
win PROFILING="OFF":
- cmake -G "Visual Studio 17 2022" -B build -DYUP_ENABLE_PROFILING={{PROFILING}}
- -start build/yup.sln
+ cmake -G "Visual Studio 18 2026" -B build -DYUP_ENABLE_PROFILING={{PROFILING}}
+ -start build/yup.slnx
[doc("generate project in Linux using Ninja")]
linux PROFILING="OFF":
@@ -57,7 +57,7 @@ android:
[doc("generate and open project for Android using Android Studio (windows)")]
[windows]
android:
- cmake -G "Visual Studio 17 2022" -B build -DYUP_TARGET_ANDROID=ON
+ cmake -G "Visual Studio 18 2026" -B build -DYUP_TARGET_ANDROID=ON
[doc("generate and open project for Android using Android Studio (linux)")]
[linux]
diff --git a/logo.png b/logo.png
new file mode 100644
index 000000000..a0bf35599
Binary files /dev/null and b/logo.png differ
diff --git a/modules/yup_audio_basics/yup_audio_basics.cpp b/modules/yup_audio_basics/yup_audio_basics.cpp
index c63d78458..714d58f9f 100644
--- a/modules/yup_audio_basics/yup_audio_basics.cpp
+++ b/modules/yup_audio_basics/yup_audio_basics.cpp
@@ -49,10 +49,11 @@
#include "yup_audio_basics.h"
#if YUP_MAC || YUP_IOS
+#include
+
#include "native/yup_AudioWorkgroup_apple.h"
#endif
-#include "buffers/yup_FloatVectorOperations.cpp"
#include "buffers/yup_AudioChannelSet.cpp"
#include "buffers/yup_AudioProcessLoadMeasurer.cpp"
#include "utilities/yup_IIRFilter.cpp"
diff --git a/modules/yup_audio_basics/yup_audio_basics.h b/modules/yup_audio_basics/yup_audio_basics.h
index b0fc2e6d9..da67ac2bb 100644
--- a/modules/yup_audio_basics/yup_audio_basics.h
+++ b/modules/yup_audio_basics/yup_audio_basics.h
@@ -50,8 +50,7 @@
website: https://github.com/kunitoki/yup
license: ISC
- dependencies: yup_core
- appleFrameworks: Accelerate
+ dependencies: yup_core yup_simd
END_YUP_MODULE_DECLARATION
@@ -62,86 +61,18 @@
#define YUP_AUDIO_BASICS_H_INCLUDED
#include
+#include
//==============================================================================
#undef Complex // apparently some C libraries actually define these symbols (!)
#undef Factor
-//==============================================================================
-#ifndef YUP_USE_SSE_INTRINSICS
-#if defined(__SSE__)
-#define YUP_USE_SSE_INTRINSICS 1
-#endif
-#endif
-
-#ifndef YUP_USE_AVX_INTRINSICS
-#if defined(__AVX2__)
-#define YUP_USE_AVX_INTRINSICS 1
-#endif
-#endif
-
-#ifndef YUP_USE_FMA_INTRINSICS
-#if defined(__FMA__)
-#define YUP_USE_FMA_INTRINSICS 1
-#endif
-#endif
-
-#if ! YUP_INTEL
-#undef YUP_USE_SSE_INTRINSICS
-#undef YUP_USE_AVX_INTRINSICS
-#undef YUP_USE_FMA_INTRINSICS
-#endif
-
-#if __ARM_NEON__ && ! (YUP_USE_VDSP_FRAMEWORK || defined(YUP_USE_ARM_NEON))
-#define YUP_USE_ARM_NEON 1
-#endif
-
-#if TARGET_IPHONE_SIMULATOR
-#ifdef YUP_USE_ARM_NEON
-#undef YUP_USE_ARM_NEON
-#endif
-#define YUP_USE_ARM_NEON 0
-#endif
-
-//==============================================================================
-#if YUP_USE_AVX_INTRINSICS || YUP_USE_FMA_INTRINSICS
-#include
-#endif
-
-#if YUP_USE_SSE_INTRINSICS
-#include
-#endif
-
-#if YUP_USE_ARM_NEON
-#if JUCE_64BIT && JUCE_WINDOWS
-#include
-#else
-#include
-#endif
-#endif
-
-#if (YUP_MAC || YUP_IOS) && __has_include()
-#ifndef YUP_USE_VDSP_FRAMEWORK
-#define YUP_USE_VDSP_FRAMEWORK 1
-#endif
-
-#if YUP_USE_VDSP_FRAMEWORK
-#include
-#endif
-
-#elif YUP_USE_VDSP_FRAMEWORK
-#undef YUP_USE_VDSP_FRAMEWORK
-#endif
-
//==============================================================================
#include
#include
//==============================================================================
#include "buffers/yup_AudioDataConverters.h"
-YUP_BEGIN_IGNORE_WARNINGS_MSVC (4661)
-#include "buffers/yup_FloatVectorOperations.h"
-YUP_END_IGNORE_WARNINGS_MSVC
#include "buffers/yup_AudioSampleBuffer.h"
#include "buffers/yup_AudioChannelSet.h"
#include "buffers/yup_AudioProcessLoadMeasurer.h"
diff --git a/modules/yup_audio_formats/formats/yup_WaveAudioFormat.cpp b/modules/yup_audio_formats/formats/yup_WaveAudioFormat.cpp
index 9bd8dd21d..d9ba6e22f 100644
--- a/modules/yup_audio_formats/formats/yup_WaveAudioFormat.cpp
+++ b/modules/yup_audio_formats/formats/yup_WaveAudioFormat.cpp
@@ -487,10 +487,12 @@ Array WaveAudioFormat::getFileExtensions (Mode handleMode) const
{
switch (handleMode)
{
- case Mode::forReading:
- return { ".wav", ".wave", ".bwf" };
case Mode::forWriting:
return { ".wav" };
+
+ case Mode::forReading:
+ default:
+ return { ".wav", ".wave", ".bwf" };
}
}
diff --git a/modules/yup_audio_formats/formats/yup_WindowsMediaAudioFormat.cpp b/modules/yup_audio_formats/formats/yup_WindowsMediaAudioFormat.cpp
index 60b09e904..72bfcd2d3 100644
--- a/modules/yup_audio_formats/formats/yup_WindowsMediaAudioFormat.cpp
+++ b/modules/yup_audio_formats/formats/yup_WindowsMediaAudioFormat.cpp
@@ -248,8 +248,10 @@ class MediaFoundationInputByteStream final : public IMFByteStream
}
asyncResult->SetStatus (hr);
+
+ // Use MFInvokeCallback to queue the callback on a work queue thread
if (pCallback != nullptr)
- pCallback->Invoke (asyncResult);
+ MFInvokeCallback (asyncResult);
asyncResult->Release();
asyncState->Release();
@@ -527,8 +529,10 @@ class MediaFoundationOutputByteStream final : public IMFByteStream
}
asyncResult->SetStatus (hr);
+
+ // Use MFInvokeCallback to queue the callback on a work queue thread
if (pCallback != nullptr)
- pCallback->Invoke (asyncResult);
+ MFInvokeCallback (asyncResult);
asyncResult->Release();
asyncState->Release();
diff --git a/modules/yup_audio_formats/yup_audio_formats.h b/modules/yup_audio_formats/yup_audio_formats.h
index 952ab8546..d7e45faba 100644
--- a/modules/yup_audio_formats/yup_audio_formats.h
+++ b/modules/yup_audio_formats/yup_audio_formats.h
@@ -32,7 +32,7 @@
website: https://github.com/kunitoki/yup
license: ISC
- dependencies: yup_audio_basics
+ dependencies: yup_audio_basics yup_simd
appleFrameworks: AudioToolbox CoreAudio CoreFoundation
END_YUP_MODULE_DECLARATION
@@ -44,6 +44,7 @@
#define YUP_AUDIO_FORMATS_H_INCLUDED
#include
+#include
//==============================================================================
/** Config: YUP_AUDIO_FORMAT_WAVE
diff --git a/modules/yup_audio_gui/displays/yup_AudioViewComponent.cpp b/modules/yup_audio_gui/displays/yup_AudioViewComponent.cpp
new file mode 100644
index 000000000..8e4aa2ea3
--- /dev/null
+++ b/modules/yup_audio_gui/displays/yup_AudioViewComponent.cpp
@@ -0,0 +1,571 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2026 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+namespace yup
+{
+
+//==============================================================================
+AudioViewComponent::AudioViewComponent (std::shared_ptr cacheToUse)
+{
+ ownedThumbnail = std::make_unique (cacheToUse);
+ attachThumbnail (*ownedThumbnail);
+}
+
+AudioViewComponent::AudioViewComponent (AudioThumbnail& thumbnailToUse)
+{
+ attachThumbnail (thumbnailToUse);
+}
+
+AudioViewComponent::~AudioViewComponent()
+{
+ if (thumbnail != nullptr)
+ thumbnail->removeListener (this);
+}
+
+void AudioViewComponent::setSource (const AudioBuffer* newBuffer, double newSampleRate)
+{
+ thumbnail->setSource (newBuffer, newSampleRate);
+}
+
+void AudioViewComponent::setSource (std::unique_ptr reader, double newSampleRate)
+{
+ thumbnail->setSource (std::move (reader), newSampleRate);
+}
+
+void AudioViewComponent::clear()
+{
+ thumbnail->clear();
+ viewRangeSamples = {};
+ zoomFactor = 1.0;
+ updateScrollBar();
+ repaint();
+}
+
+void AudioViewComponent::setZoomFactor (double newZoomFactor)
+{
+ const int totalSamples = getTotalSamples();
+ if (totalSamples <= 0)
+ {
+ zoomFactor = 1.0;
+ viewRangeSamples = {};
+ updateScrollBar();
+ repaint();
+ return;
+ }
+
+ // Limit maximum zoom to prevent viewing less than 100 samples (or total if smaller)
+ const double maxZoom = jmin (static_cast (totalSamples), static_cast (totalSamples) / 100.0);
+ zoomFactor = jlimit (1.0, maxZoom, newZoomFactor);
+
+ const double viewLength = jmax (100.0, static_cast (totalSamples) / zoomFactor);
+ auto newRange = Range::withStartAndLength (viewRangeSamples.getStart(), viewLength);
+
+ setViewRangeSamplesInternal (newRange, true);
+}
+
+void AudioViewComponent::setViewRangeSamples (Range newRange)
+{
+ setViewRangeSamplesInternal (newRange, true);
+}
+
+void AudioViewComponent::scrollToSample (double newStartSample)
+{
+ setViewRangeSamplesInternal (Range::withStartAndLength (newStartSample,
+ viewRangeSamples.getLength()),
+ true);
+}
+
+void AudioViewComponent::setLabelWidth (int newLabelWidth)
+{
+ labelWidth = jmax (0, newLabelWidth);
+ resized();
+ repaint();
+}
+
+void AudioViewComponent::setChannelLabelsVisible (bool shouldShow) noexcept
+{
+ if (showChannelLabels == shouldShow)
+ return;
+
+ showChannelLabels = shouldShow;
+ resized();
+ repaint();
+}
+
+void AudioViewComponent::setSelectable (bool shouldBeSelectable) noexcept
+{
+ if (selectable == shouldBeSelectable)
+ return;
+
+ selectable = shouldBeSelectable;
+ setWantsKeyboardFocus (selectable);
+
+ if (! selectable)
+ leaveKeyboardFocus();
+}
+
+int AudioViewComponent::getTotalSamples() const noexcept
+{
+ return thumbnail->getTotalSamples();
+}
+
+int AudioViewComponent::getNumChannels() const noexcept
+{
+ return thumbnail->getNumChannels();
+}
+
+double AudioViewComponent::getSampleRate() const noexcept
+{
+ return thumbnail->getSampleRate();
+}
+
+double AudioViewComponent::timeToSample (double seconds) const noexcept
+{
+ const double sampleRate = getSampleRate();
+ return sampleRate > 0.0 ? seconds * sampleRate : 0.0;
+}
+
+double AudioViewComponent::sampleToTime (double sample) const noexcept
+{
+ const double sampleRate = getSampleRate();
+ return sampleRate > 0.0 ? sample / sampleRate : 0.0;
+}
+
+float AudioViewComponent::sampleToX (double sample, const Rectangle& waveformBounds) const noexcept
+{
+ if (waveformBounds.getWidth() <= 0.0f)
+ return waveformBounds.getX();
+
+ const auto range = viewRangeSamples.isEmpty()
+ ? Range::withStartAndLength (0.0, 1.0)
+ : viewRangeSamples;
+ const double clamped = jlimit (range.getStart(), range.getEnd(), sample);
+ const double proportion = (clamped - range.getStart()) / range.getLength();
+ return waveformBounds.getX() + static_cast (proportion * waveformBounds.getWidth());
+}
+
+double AudioViewComponent::xToSample (float x, const Rectangle& waveformBounds) const noexcept
+{
+ if (waveformBounds.getWidth() <= 0.0f)
+ return viewRangeSamples.getStart();
+
+ const auto range = viewRangeSamples.isEmpty()
+ ? Range::withStartAndLength (0.0, 1.0)
+ : viewRangeSamples;
+ const float clampedX = jlimit (waveformBounds.getX(), waveformBounds.getRight(), x);
+ const double proportion = (clampedX - waveformBounds.getX()) / waveformBounds.getWidth();
+ return range.getStart() + proportion * range.getLength();
+}
+
+Rectangle AudioViewComponent::getWaveformBounds() const
+{
+ auto bounds = getLocalBounds().reduced (8);
+ if (horizontalScrollBar != nullptr && horizontalScrollBar->isVisible())
+ bounds.removeFromBottom (static_cast (horizontalScrollBar->getScrollBarWidth()));
+ if (progressBar != nullptr && progressBar->isVisible())
+ bounds.removeFromBottom (progressBarHeight);
+ if (showChannelLabels && labelWidth > 0)
+ bounds.removeFromLeft (labelWidth);
+ return bounds;
+}
+
+void AudioViewComponent::paint (Graphics& g)
+{
+ paintBackground (g, getLocalBounds());
+
+ auto bounds = getLocalBounds().reduced (8);
+ if (horizontalScrollBar != nullptr && horizontalScrollBar->isVisible())
+ bounds.removeFromBottom (static_cast (horizontalScrollBar->getScrollBarWidth()));
+ if (progressBar != nullptr && progressBar->isVisible())
+ bounds.removeFromBottom (progressBarHeight);
+
+ const bool shouldShowLabels = showChannelLabels && labelWidth > 0;
+ auto labelArea = shouldShowLabels ? bounds.removeFromLeft (labelWidth) : Rectangle();
+ auto waveformArea = bounds;
+
+ const int numChannels = thumbnail->getNumChannels();
+ const int totalSamples = thumbnail->getTotalSamples();
+
+ if (totalSamples <= 0 || numChannels <= 0)
+ {
+ paintPlaceholder (g, waveformArea);
+ return;
+ }
+
+ auto profile = thumbnail->getPeakProfile();
+ if (profile == nullptr || ! profile->isValid())
+ {
+ paintPlaceholder (g, waveformArea);
+ return;
+ }
+
+ const float laneHeight = waveformArea.getHeight() / static_cast (numChannels);
+ auto font = ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f);
+
+ // Calculate visible sample range
+ const Range visibleSamples = viewRangeSamples.isEmpty()
+ ? Range (0.0, static_cast (totalSamples))
+ : viewRangeSamples;
+
+ // Paint each channel
+ for (int channel = 0; channel < numChannels; ++channel)
+ {
+ Rectangle lane (waveformArea.getX(),
+ waveformArea.getY() + laneHeight * channel,
+ waveformArea.getWidth(),
+ laneHeight);
+
+ // Draw lane background
+ g.setFillColor (Color (0xFF181818));
+ g.fillRect (lane);
+
+ g.setStrokeColor (Color (0xFF2A2A2A));
+ g.setStrokeWidth (1.0f);
+ g.strokeRect (lane);
+
+ // Draw channel label
+ if (shouldShowLabels)
+ {
+ auto labelBounds = labelArea.withY (lane.getY()).withHeight (lane.getHeight());
+ g.setFillColor (Colors::white);
+ g.fillFittedText (getChannelLabel (channel),
+ font,
+ labelBounds,
+ Justification::center);
+ }
+
+ // Paint waveform using new simplified API
+ thumbnail->paintChannel (g, lane, channel, visibleSamples, waveformArea.getWidth());
+ }
+
+ paintOverlay (g, waveformArea);
+}
+
+void AudioViewComponent::resized()
+{
+ updateScrollBar();
+ updateLayout();
+ repaint();
+}
+
+void AudioViewComponent::mouseDown (const MouseEvent& event)
+{
+ if (selectable)
+ takeKeyboardFocus();
+
+ Component::mouseDown (event);
+}
+
+void AudioViewComponent::mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData)
+{
+ if (! selectable)
+ {
+ Component::mouseWheel (event, wheelData);
+ return;
+ }
+
+ if (getTotalSamples() <= 0)
+ return;
+
+ const float deltaX = wheelData.getDeltaX();
+ const float deltaY = wheelData.getDeltaY();
+
+ if (deltaY != 0.0f)
+ {
+ const float magnitude = deltaY >= 0.0f ? deltaY : -deltaY;
+ const double zoomStep = 1.0 + static_cast (magnitude) * 0.25;
+ const double zoomMultiplier = deltaY > 0.0f ? zoomStep : 1.0 / zoomStep;
+ const auto waveformBounds = getWaveformBounds();
+ const double anchorSample = xToSample (event.getPosition().getX(), waveformBounds);
+ zoomAroundSample (zoomMultiplier, anchorSample);
+ }
+
+ if (deltaX != 0.0f)
+ {
+ const double scrollAmount = viewRangeSamples.getLength() * static_cast (deltaX) * 0.15;
+ scrollBySamples (scrollAmount);
+ }
+}
+
+void AudioViewComponent::keyDown (const KeyPress& keys, const Point&)
+{
+ if (! selectable)
+ return;
+
+ if (getTotalSamples() <= 0)
+ return;
+
+ const int key = keys.getKey();
+
+ if (key == KeyPress::leftKey || key == KeyPress::rightKey)
+ {
+ const double direction = key == KeyPress::leftKey ? -1.0 : 1.0;
+ const double scrollAmount = viewRangeSamples.getLength() * 0.1 * direction;
+ scrollBySamples (scrollAmount);
+ return;
+ }
+
+ if (key == KeyPress::upKey || key == KeyPress::downKey)
+ {
+ const double direction = key == KeyPress::upKey ? 1.0 : -1.0;
+ const double zoomMultiplier = direction > 0.0 ? 1.15 : (1.0 / 1.15);
+ const double anchorSample = viewRangeSamples.getStart()
+ + viewRangeSamples.getLength() * 0.5;
+ zoomAroundSample (zoomMultiplier, anchorSample);
+ }
+}
+
+void AudioViewComponent::paintBackground (Graphics& g, const Rectangle&)
+{
+ g.setFillColor (Color (0xFF101010));
+ g.fillAll();
+}
+
+void AudioViewComponent::paintPlaceholder (Graphics& g, const Rectangle& bounds)
+{
+ const bool hasSource = (thumbnail->getTotalSamples() > 0 && thumbnail->getNumChannels() > 0);
+ const auto placeholderText = hasSource
+ ? String ("Analyzing waveform...")
+ : String ("Load an audio file to view its waveform.");
+
+ g.setFillColor (Colors::lightgray);
+ auto font = ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (14.0f);
+ g.fillFittedText (placeholderText, font, bounds, Justification::center);
+}
+
+void AudioViewComponent::paintOverlay (Graphics&, const Rectangle&)
+{
+}
+
+String AudioViewComponent::getChannelLabel (int channelIndex) const
+{
+ return "Ch " + String (channelIndex + 1);
+}
+
+void AudioViewComponent::attachThumbnail (AudioThumbnail& thumbnailToUse)
+{
+ thumbnail = &thumbnailToUse;
+ thumbnail->addListener (this);
+
+ horizontalScrollBar = std::make_unique (ScrollBar::Orientation::horizontal);
+ horizontalScrollBar->setVisibilityMode (ScrollBar::VisibilityMode::autoHide);
+ horizontalScrollBar->onScrollPositionChanged = [this] (double)
+ {
+ handleScrollBarMoved();
+ };
+ addAndMakeVisible (*horizontalScrollBar);
+
+ progressBar = std::make_unique ("AudioThumbnailProgress");
+ progressBar->setVisible (false);
+ addAndMakeVisible (*progressBar);
+
+ updateProgressBar (thumbnail->getProgress(), thumbnail->isProgressVisible());
+ ensureViewRangeIsValid();
+ updateScrollBar();
+}
+
+void AudioViewComponent::updateLayout()
+{
+ auto bounds = getLocalBounds().reduced (8);
+ if (horizontalScrollBar != nullptr && horizontalScrollBar->isVisible())
+ {
+ auto height = static_cast (horizontalScrollBar->getScrollBarWidth());
+ horizontalScrollBar->setBounds (bounds.removeFromBottom (height));
+ }
+ else if (horizontalScrollBar != nullptr)
+ {
+ horizontalScrollBar->setBounds (bounds.removeFromBottom (0));
+ }
+
+ if (progressBar != nullptr && progressBar->isVisible())
+ progressBar->setBounds (bounds.removeFromBottom (progressBarHeight));
+ else if (progressBar != nullptr)
+ progressBar->setBounds (bounds.removeFromBottom (0));
+}
+
+void AudioViewComponent::updateScrollBar()
+{
+ if (horizontalScrollBar == nullptr)
+ return;
+
+ const double totalSamples = static_cast (getTotalSamples());
+ const double viewStart = viewRangeSamples.isEmpty() ? 0.0 : viewRangeSamples.getStart();
+ const double viewEnd = viewRangeSamples.isEmpty()
+ ? jmax (1.0, totalSamples)
+ : viewRangeSamples.getEnd();
+
+ ignoreScrollBarCallback = true;
+ horizontalScrollBar->setRangeLimits (0.0, jmax (1.0, totalSamples));
+ horizontalScrollBar->setCurrentRange (viewStart, viewEnd);
+ ignoreScrollBarCallback = false;
+}
+
+void AudioViewComponent::handleScrollBarMoved()
+{
+ if (ignoreScrollBarCallback || horizontalScrollBar == nullptr)
+ return;
+
+ const double newStart = horizontalScrollBar->getCurrentRangeStart();
+ setViewRangeSamplesInternal (Range::withStartAndLength (newStart,
+ viewRangeSamples.getLength()),
+ false);
+}
+
+void AudioViewComponent::setViewRangeSamplesInternal (Range range, bool notifyScrollBar)
+{
+ const double totalSamples = static_cast (getTotalSamples());
+ if (totalSamples <= 0.0)
+ {
+ viewRangeSamples = {};
+ zoomFactor = 1.0;
+ if (notifyScrollBar)
+ updateScrollBar();
+ repaint();
+ return;
+ }
+
+ // Clamp view range to valid sample bounds
+ double clampedStart = jlimit (0.0, totalSamples - 1.0, range.getStart());
+ double clampedLength = range.getLength();
+
+ // Ensure the view doesn't extend past the end
+ if (clampedStart + clampedLength > totalSamples)
+ clampedLength = totalSamples - clampedStart;
+
+ // Ensure minimum length (at least 10 samples or total samples if smaller)
+ const double minViewLength = jmin (10.0, totalSamples);
+ clampedLength = jmax (minViewLength, clampedLength);
+
+ // Final check: if start + length would exceed total, adjust start
+ if (clampedStart + clampedLength > totalSamples)
+ clampedStart = jmax (0.0, totalSamples - clampedLength);
+
+ viewRangeSamples = Range::withStartAndLength (clampedStart, clampedLength);
+
+ zoomFactor = (totalSamples > 0.0 && viewRangeSamples.getLength() > 0.0)
+ ? totalSamples / viewRangeSamples.getLength()
+ : 1.0;
+
+ if (notifyScrollBar)
+ updateScrollBar();
+
+ repaint();
+}
+
+void AudioViewComponent::scrollBySamples (double deltaSamples)
+{
+ const int totalSamples = getTotalSamples();
+ if (totalSamples <= 0)
+ return;
+
+ const double viewLength = viewRangeSamples.isEmpty()
+ ? static_cast (totalSamples)
+ : viewRangeSamples.getLength();
+ const double viewStart = viewRangeSamples.isEmpty() ? 0.0 : viewRangeSamples.getStart();
+ const double newStart = viewStart + deltaSamples;
+
+ // Prevent scrolling beyond boundaries
+ if (deltaSamples < 0.0 && newStart <= 0.0)
+ {
+ // At the beginning, don't scroll left
+ setViewRangeSamplesInternal (Range::withStartAndLength (0.0, viewLength), true);
+ return;
+ }
+
+ if (deltaSamples > 0.0 && newStart + viewLength >= static_cast (totalSamples))
+ {
+ // At the end, don't scroll right
+ setViewRangeSamplesInternal (Range::withStartAndLength (
+ static_cast (totalSamples) - viewLength, viewLength),
+ true);
+ return;
+ }
+
+ setViewRangeSamplesInternal (Range::withStartAndLength (newStart, viewLength), true);
+}
+
+void AudioViewComponent::zoomAroundSample (double zoomMultiplier, double anchorSample)
+{
+ const double totalSamples = static_cast (getTotalSamples());
+ if (totalSamples <= 0.0)
+ return;
+
+ const double currentZoom = zoomFactor;
+ // Limit maximum zoom to prevent viewing less than 100 samples (or total if smaller)
+ const double maxZoom = jmin (totalSamples, totalSamples / 100.0);
+ const double newZoom = jlimit (1.0, maxZoom, currentZoom * zoomMultiplier);
+ const double newViewLength = jmax (100.0, totalSamples / newZoom);
+
+ const double oldViewLength = viewRangeSamples.isEmpty()
+ ? totalSamples
+ : viewRangeSamples.getLength();
+ const double oldViewStart = viewRangeSamples.isEmpty() ? 0.0 : viewRangeSamples.getStart();
+ const double clampedAnchor = jlimit (0.0, totalSamples - 1.0, anchorSample);
+ const double anchorRatio = oldViewLength > 0.0
+ ? jlimit (0.0, 1.0, (clampedAnchor - oldViewStart) / oldViewLength)
+ : 0.5;
+ const double newStart = clampedAnchor - newViewLength * anchorRatio;
+
+ setViewRangeSamplesInternal (Range::withStartAndLength (newStart, newViewLength), true);
+}
+
+void AudioViewComponent::ensureViewRangeIsValid()
+{
+ const int totalSamples = getTotalSamples();
+ if (totalSamples <= 0)
+ {
+ viewRangeSamples = {};
+ zoomFactor = 1.0;
+ updateScrollBar();
+ return;
+ }
+
+ const auto defaultRange = Range::withStartAndLength (0.0, static_cast (totalSamples));
+ const auto rangeToUse = viewRangeSamples.isEmpty() ? defaultRange : viewRangeSamples;
+ setViewRangeSamplesInternal (rangeToUse, true);
+}
+
+void AudioViewComponent::updateProgressBar (double progress, bool isVisible)
+{
+ if (progressBar == nullptr)
+ return;
+
+ if (progressBar->isVisible() != isVisible)
+ {
+ progressBar->setVisible (isVisible);
+ updateLayout();
+ }
+
+ progressBar->setProgress (progress, sendNotificationSync);
+}
+
+void AudioViewComponent::thumbnailChanged (AudioThumbnail&)
+{
+ ensureViewRangeIsValid();
+ repaint();
+}
+
+void AudioViewComponent::thumbnailProgressChanged (AudioThumbnail&, double progress, bool isVisible)
+{
+ updateProgressBar (progress, isVisible);
+}
+
+} // namespace yup
diff --git a/modules/yup_audio_gui/displays/yup_AudioViewComponent.h b/modules/yup_audio_gui/displays/yup_AudioViewComponent.h
new file mode 100644
index 000000000..1661170d7
--- /dev/null
+++ b/modules/yup_audio_gui/displays/yup_AudioViewComponent.h
@@ -0,0 +1,184 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2026 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#pragma once
+
+namespace yup
+{
+
+//==============================================================================
+/**
+ View component that renders an AudioThumbnail with zooming, scrolling, and progress display.
+*/
+class YUP_API AudioViewComponent : public Component
+ , private AudioThumbnail::Listener
+{
+public:
+ //==============================================================================
+ /** Creates a view with an owned AudioThumbnail.
+
+ @param cacheToUse Optional cache to share across multiple views (creates default if nullptr)
+ */
+ explicit AudioViewComponent (std::shared_ptr cacheToUse = nullptr);
+
+ /** Creates a view for an externally owned AudioThumbnail. */
+ explicit AudioViewComponent (AudioThumbnail& thumbnailToUse);
+
+ /** Destructor. */
+ ~AudioViewComponent() override;
+
+ //==============================================================================
+ /** Assigns the buffer to render and refreshes the peak cache. */
+ void setSource (const AudioBuffer* newBuffer, double newSampleRate = 0.0);
+
+ /** Assigns an audio file to render and refreshes the peak cache. */
+ void setSource (std::unique_ptr reader, double newSampleRate = 0.0);
+
+ /** Clears the waveform display and cache. */
+ void clear();
+
+ //==============================================================================
+ /** Sets the zoom factor.
+
+ A zoom factor of 1.0 fits the whole buffer in view. Higher values zoom in.
+ */
+ void setZoomFactor (double newZoomFactor);
+
+ /** Returns the current zoom factor. */
+ double getZoomFactor() const noexcept { return zoomFactor; }
+
+ /** Sets the visible sample range. */
+ void setViewRangeSamples (Range newRange);
+
+ /** Returns the currently visible sample range. */
+ Range getViewRangeSamples() const noexcept { return viewRangeSamples; }
+
+ /** Scrolls to a new start sample using the current view length. */
+ void scrollToSample (double newStartSample);
+
+ //==============================================================================
+ /** Sets the channel label width in pixels. */
+ void setLabelWidth (int newLabelWidth);
+
+ /** Returns the channel label width in pixels. */
+ int getLabelWidth() const noexcept { return labelWidth; }
+
+ /** Shows or hides the channel labels. */
+ void setChannelLabelsVisible (bool shouldShow) noexcept;
+
+ /** Returns true if the channel labels are visible. */
+ bool isChannelLabelsVisible() const noexcept { return showChannelLabels; }
+
+ /** Enables mouse/keyboard interaction for zooming and scrolling. */
+ void setSelectable (bool shouldBeSelectable) noexcept;
+
+ /** Returns true if mouse/keyboard interaction is enabled. */
+ bool isSelectable() const noexcept { return selectable; }
+
+ //==============================================================================
+ /** Returns the horizontal scrollbar used for scrolling. */
+ ScrollBar* getHorizontalScrollBar() const noexcept { return horizontalScrollBar.get(); }
+
+ /** Returns the progress bar used during loading/analysis. */
+ ProgressBar* getProgressBar() const noexcept { return progressBar.get(); }
+
+ //==============================================================================
+ /** Returns the total sample count of the assigned source. */
+ int getTotalSamples() const noexcept;
+
+ /** Returns the total channel count of the assigned source. */
+ int getNumChannels() const noexcept;
+
+ /** Returns the sample rate associated with the buffer. */
+ double getSampleRate() const noexcept;
+
+ /** Converts a time in seconds to a sample position. */
+ double timeToSample (double seconds) const noexcept;
+
+ /** Converts a sample position to time in seconds. */
+ double sampleToTime (double sample) const noexcept;
+
+ /** Converts a sample position to an X coordinate within the waveform bounds. */
+ float sampleToX (double sample, const Rectangle& waveformBounds) const noexcept;
+
+ /** Converts an X coordinate within the waveform bounds to a sample position. */
+ double xToSample (float x, const Rectangle& waveformBounds) const noexcept;
+
+ /** Returns the bounds of the waveform area (excluding labels and scrollbar). */
+ Rectangle getWaveformBounds() const;
+
+ //==============================================================================
+ /** @internal */
+ void paint (Graphics& g) override;
+ /** @internal */
+ void resized() override;
+ /** @internal */
+ void mouseDown (const MouseEvent& event) override;
+ /** @internal */
+ void mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) override;
+ /** @internal */
+ void keyDown (const KeyPress& keys, const Point& position) override;
+
+protected:
+ //==============================================================================
+ /** Paints the component background. */
+ virtual void paintBackground (Graphics& g, const Rectangle