diff --git a/.gitignore b/.gitignore index cac3fab..fecdb38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Build products /build/ -/build-juce-gui/ /cmake-build-*/ /CMakeUserPresets.json /*.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 411f6cf..5e9be23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,6 @@ find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter) find_package(PkgConfig QUIET) option(XTC_WITH_JACK "Discover the optional JACK backend" ON) option(XTC_WITH_LADSPA "Enable LADSPA when its system header is available" ON) -option(XTC_BUILD_JUCE_GUI "Build the optional JUCE workstation frontend" OFF) option(XTC_BUILD_GUI "Build the FLTK application when FLTK is available" ON) option(XTC_ENABLE_LIVE_TESTS "Register tests requiring an existing JACK server" OFF) option(XTC_BUILD_JUCE "Build optional externally supplied JUCE examples" OFF) @@ -123,7 +122,6 @@ foreach(preview quality builtin) endforeach() include(cmake/Tests.cmake) include(cmake/Gui.cmake) -include(cmake/JuceGui.cmake) include(cmake/Developer.cmake) include(cmake/Install.cmake) if(XTC_BUILD_STEINBERG) diff --git a/README.md b/README.md index 4b4c1a3..f9e199d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,6 @@ - **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph. - **C core:** project model, graph, DSP and audio engine are implemented in C. -- **Optional JUCE GUI:** build `xtc-juce` with `-DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE`. Keep or disable FLTK independently with `XTC_BUILD_GUI`. See [the JUCE frontend](src/gui/JUCE/README.md) for build/run instructions and SDK licensing. - **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API. The sequencer coordinates nested ruler, track, header, clip and velocity widgets; see [GUI ownership](docs/ARCHITECTURE.md#sequencer-widget-ownership). - **Linux first:** keep backend-specific details behind an interface. - **Small increments:** build and test each milestone before adding features. @@ -88,7 +87,7 @@ The JACK backend has passed software-loopback testing; physical-device playback ## Build -Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The default optional GUI needs C++17 and FLTK; a separately selectable JUCE frontend uses an external JUCE SDK; JACK and LADSPA development files enable their optional integrations. +Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The optional GUI needs C++17 and FLTK; JACK and LADSPA development files enable their optional integrations. ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @@ -108,7 +107,7 @@ make clean # clean products; keep the CMake cache Override `BUILD_DIR`, `BUILD_TYPE` or `JOBS` as needed, for example `make BUILD_DIR=build/release BUILD_TYPE=Release JOBS=8`. Pass additional options with `CMAKE_ARGS` and `CTEST_ARGS`. CMake remains the authoritative build system. -This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration. +This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration. ## Run diff --git a/cmake/Install.cmake b/cmake/Install.cmake index 4520d7f..f206289 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -1,6 +1,5 @@ # Retain the audited owned-file inventory and symlink-safe staging implementation. set(XTC_INSTALL_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "Directory containing built install inputs") -option(XTC_INSTALL_JUCE_GUI "Include the optional JUCE workstation in installation" ${XTC_BUILD_JUCE_GUI}) option(XTC_INSTALL_GUI "Include the GUI in installation" ${XTC_GUI_AVAILABLE}) set(XTC_INSTALL_LICENSEDIR "${CMAKE_INSTALL_DATAROOTDIR}/licenses/xtc" CACHE STRING "License install directory") set(XTC_INSTALL_EXAMPLESDIR "${CMAKE_INSTALL_DATAROOTDIR}/xtc/examples" CACHE STRING "Example install directory") diff --git a/cmake/InstallDriver.cmake.in b/cmake/InstallDriver.cmake.in index 5021dbe..0d98e1c 100644 --- a/cmake/InstallDriver.cmake.in +++ b/cmake/InstallDriver.cmake.in @@ -22,9 +22,6 @@ else() if("@XTC_INSTALL_GUI@") list(APPEND extra --gui) endif() - if("@XTC_INSTALL_JUCE_GUI@") - list(APPEND extra --juce-gui) - endif() set(jack) if("@JACK_FOUND@" AND "@XTC_WITH_JACK@") set(jack jack) diff --git a/cmake/JuceGui.cmake b/cmake/JuceGui.cmake deleted file mode 100644 index dd016c1..0000000 --- a/cmake/JuceGui.cmake +++ /dev/null @@ -1,91 +0,0 @@ -if(NOT XTC_BUILD_JUCE_GUI) - return() -endif() -enable_language(CXX) -set(XTC_JUCE_ROOT "" CACHE PATH "External JUCE source checkout") -if(NOT TARGET juce::juce_gui_extra) - if(NOT EXISTS "${XTC_JUCE_ROOT}/modules/juce_gui_extra/juce_gui_extra.h") - message(FATAL_ERROR "XTC_BUILD_JUCE_GUI requires XTC_JUCE_ROOT pointing to an external JUCE SDK checkout") - endif() - set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE) - add_subdirectory("${XTC_JUCE_ROOT}" juce-gui-sdk EXCLUDE_FROM_ALL) -endif() -message(STATUS "Optional JUCE frontend: JUCE is AGPLv3/commercial; review the SDK licence before distributing") -set(xtc_juce_sources main_window.cpp graph_widget.cpp sequencer_widget.cpp - sequencer_commands.cpp sequencer_track_widget.cpp sequencer_clip_widget.cpp - sequencer_velocity_widget.cpp sequencer_ruler_widget.cpp sequencer_instruments.cpp - sequencer_waveforms.cpp piano_roll.cpp mixer_widget.cpp plugin_dialog.cpp sample_editor.cpp ui.cpp) -list(TRANSFORM xtc_juce_sources PREPEND "${PROJECT_SOURCE_DIR}/src/gui/JUCE/") -add_library(xtc-juce-gui STATIC ${xtc_juce_sources}) -xtc_warnings(xtc-juce-gui) -target_compile_definitions(xtc-juce-gui PUBLIC JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_MODAL_LOOPS_PERMITTED=1 JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1 JUCE_STANDALONE_APPLICATION=1 - "$<$:DEBUG=1;_DEBUG=1>" "$<$>:_NDEBUG=1>") -target_link_libraries(xtc-juce-gui PUBLIC xtc-core PRIVATE juce::juce_gui_extra) -target_include_directories(xtc-juce-gui SYSTEM PUBLIC "${XTC_JUCE_ROOT}/modules") -juce_add_gui_app(xtc-juce PRODUCT_NAME "xtc JUCE" COMPANY_NAME xtc BUNDLE_ID org.xtc.workstation) -set_target_properties(xtc-juce PROPERTIES OUTPUT_NAME xtc-juce RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") -target_sources(xtc-juce PRIVATE src/gui/JUCE/main.cpp) -target_link_libraries(xtc-juce PRIVATE xtc-juce-gui) -xtc_warnings(xtc-juce) -add_custom_target(gui-juce DEPENDS xtc-juce) -if(BUILD_TESTING) - find_program(XTC_JUCE_XVFB_RUN xvfb-run) - # Exercise the same C boundary, editing workflows and fault injection as FLTK. - # Adapt toolkit names/initialisation and show the inspector before native focus. - file(READ "${PROJECT_SOURCE_DIR}/tests/integration/test_gui.cpp" juce_test) - string(REGEX REPLACE "#include ]+>" "#include \"ui.hpp\"" juce_test "${juce_test}") - string(REPLACE "../../src/gui/" "${PROJECT_SOURCE_DIR}/src/gui/JUCE/" juce_test "${juce_test}") - string(REPLACE "Fl_" "Ui" juce_test "${juce_test}") - string(REPLACE "Fl::" "Ui::" juce_test "${juce_test}") - string(REPLACE "FL_" "UI_" juce_test "${juce_test}") - string(REPLACE "int main(int argc, char **argv)\n{" "int main(int argc, char **argv)\n{\n UiInitialiser initialise;" juce_test "${juce_test}") - # JUCE requires a visible component before native keyboard focus is requested. - string(REPLACE "if(gain){Ui::focus(&gain->input);" "if(gain){window.show_track_inspector(true);Ui::focus(&gain->input);" juce_test "${juce_test}") - # Native TextEditor commits on blur; unwind that additional edit before - # the shared test checks undo of its earlier explicit gain edit. - string(REPLACE "Ui::focus(nullptr);}" "Ui::focus(nullptr);pump();xtc_track_info blurred{};xtc_project_get_track(seq->project(),0,&blurred);if(blurred.gain==.123f)CHECK(window.undo()==XTC_OK);}" juce_test "${juce_test}") - file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/test_juce_gui.cpp" CONTENT "${juce_test}") - add_executable(test_juce_gui "${CMAKE_CURRENT_BINARY_DIR}/test_juce_gui.cpp") - target_include_directories(test_juce_gui PRIVATE "${PROJECT_SOURCE_DIR}/src/gui/JUCE") - target_link_libraries(test_juce_gui PRIVATE xtc-juce-gui) - xtc_wrap(test_juce_gui xtc_history_reserve) - xtc_warnings(test_juce_gui) - if(XTC_JUCE_XVFB_RUN) - foreach(pair "editing|" "sequencer|SEQUENCER_COMPONENTS" "midi|MIDI_WORKFLOWS" "samples|SAMPLE_EDITOR" "creative|CREATIVE" "next-daw|NEXT_DAW") - string(REPLACE "|" ";" pair "${pair}") - list(GET pair 0 name) - list(GET pair 1 env) - add_test(NAME juce-gui-${name} COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-gui-${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "gui;juce" RUN_SERIAL TRUE) - if(env) - set_property(TEST juce-gui-${name} PROPERTY ENVIRONMENT "XTC_TEST_${env}_ONLY=1") - endif() - endforeach() - endif() -endif() -if(BUILD_TESTING AND JACK_FOUND AND XTC_WITH_JACK) - add_executable(test_juce_playback tests/integration/test_juce_playback.cpp) - target_link_libraries(test_juce_playback PRIVATE xtc-juce-gui) - xtc_warnings(test_juce_playback) - if(XTC_ENABLE_LIVE_TESTS AND XTC_JUCE_XVFB_RUN) - add_test(NAME juce-playback COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-playback PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "live;juce" RUN_SERIAL TRUE) - endif() -endif() -if(BUILD_TESTING AND XTC_JUCE_XVFB_RUN) - function(xtc_juce_gui_variant name) - add_test(NAME juce-gui-${name} COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-gui-${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "gui;juce" RUN_SERIAL TRUE ENVIRONMENT "${ARGN}") - endfunction() - xtc_juce_gui_variant(modern-editing "XTC_TEST_MODERN_EDITING_ONLY=1") - xtc_juce_gui_variant(midi-labels "XTC_TEST_MIDI_LABELS_ONLY=1") - set_property(TEST juce-gui-next-daw APPEND PROPERTY ENVIRONMENT XTC_TEST_MODERN_EDITING_ONLY=1) - xtc_juce_gui_variant(modern-plugins "XTC_TEST_CLAP=$" "XTC_TEST_VST2=$" "XTC_TEST_VST3=$") - xtc_juce_gui_variant(instruments "XTC_TEST_SYNTH_CLAP=$" "XTC_TEST_SYNTH_VST2=$" "XTC_TEST_SYNTH_VST3=$") - if(XTC_LADSPA_AVAILABLE) - xtc_juce_gui_variant(plugins "XTC_TEST_PLUGIN=$") - xtc_juce_gui_variant(track-fx "XTC_TEST_TRACK_FX_ONLY=1" "XTC_TEST_PLUGIN=$" "XTC_TEST_CLAP=$" "XTC_TEST_VST2=$" "XTC_TEST_VST3=$") - endif() -endif() -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/tests/integration/test_gui.cpp") diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d020866..a7e3d3c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,8 +11,7 @@ the graph editor is an advanced view of the same project. ## Layers -1. **C++ GUI:** input, drawing, windows, view state. FLTK is the default; - `src/gui/JUCE` is an independently optional JUCE frontend using the same C API. +1. **FLTK/C++ GUI:** input, drawing, windows, view state. 2. **C core:** project model, commands, parameters, persistence, validation. 3. **C engine:** prepared processing graph, DSP, transport timing, event delivery. @@ -219,16 +218,3 @@ files; unsupported native saves preserve files. See NATIVE_API.md and decision 0008. Release tooling lives outside the runtime, with source manifests, deterministic exports and staged installation/consumer checks. See RELEASING.md for reproducibility scope. - -## JUCE frontend ownership - -The optional `xtc-juce` application ports the complete retained GUI command -structure under `src/gui/JUCE`, without linking FLTK. `ui.*` supplies JUCE -components, native controls, graphics, event dispatch, timers, scrolling and -dialogs while preserving the original C-facing command boundaries. The master -sequencer owns JUCE ruler/track groups; tracks own header, instrument, clip and -velocity components. Group ownership, borrowed indexes, immutable copied C -metadata and deferred deletion follow the same rules as the FLTK frontend. -JUCE renders overlays after child components and routes callbacks on the message -thread. The audio backend and callbacks remain in C. See the -[JUCE source map](../src/gui/JUCE/README.md#components-and-maintenance). diff --git a/docs/BUILD.md b/docs/BUILD.md index 3142795..a982f76 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -79,7 +79,6 @@ lists, dependencies, SDK and packaging rules in CMake. | Cache option | Default | Meaning | | --- | --- | --- | | `BUILD_TESTING` | `ON` | Build/register the test suite | -| `XTC_BUILD_JUCE_GUI` | `OFF` | Build the JUCE workstation frontend (`xtc-juce`), independently of FLTK; requires `XTC_JUCE_ROOT` | | `XTC_BUILD_GUI` | `ON` | Discover FLTK and build the application when available | | `XTC_WITH_JACK` | `ON` | Discover/build the JACK/PipeWire backend | | `XTC_WITH_LADSPA` | `ON` | Enable LADSPA when the system header exists | @@ -192,21 +191,3 @@ Clean products with `cmake --build build --target clean`; to discard the cache, remove only the selected build directory. Never configure in the source tree. Historical validation entries in ROADMAP describe the build used at that time; Phase 108 records the CMake migration and its verified results. - -## Optional JUCE workstation frontend - -See [src/gui/JUCE/README.md](../src/gui/JUCE/README.md) for the component map and -licence requirements. The SDK is supplied by the user; no download occurs. - -```sh -cmake -S . -B build/juce-gui -DCMAKE_BUILD_TYPE=Debug \ - -DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE -cmake --build build/juce-gui --parallel 4 --target xtc-juce test_juce_gui -./build/juce-gui/xtc-juce examples/projects/playback-test.xtc -ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1 -``` - -`XTC_BUILD_GUI=ON` builds both frontends. `XTC_BUILD_JUCE` continues to mean -JUCE plugin/processor examples and can be enabled independently or alongside -the workstation frontend. A missing SDK is a configuration error only when a -JUCE option is enabled. The existing default FLTK build is unchanged. diff --git a/docs/GUI.md b/docs/GUI.md index 51db44e..21b098b 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -1,10 +1,6 @@ # GUI Guidelines -The default frontend uses FLTK/C++ for windows, menus, dialogs, and the event loop. -An optional JUCE port is available under `src/gui/JUCE`; build `xtc-juce` with -`XTC_BUILD_JUCE_GUI=ON` and an external `XTC_JUCE_ROOT`. Both frontends can be -built together and use the same project files and C API. See -[JUCE build and component details](../src/gui/JUCE/README.md). Keep all audio +Use FLTK/C++ for windows, menus, dialogs, and the event loop. Keep all audio engine and project-domain logic in C. Main views: diff --git a/docs/JUCE.md b/docs/JUCE.md index 8d0b4c7..9e8b76a 100644 --- a/docs/JUCE.md +++ b/docs/JUCE.md @@ -1,8 +1,7 @@ -# JUCE integration +# JUCE integration (Phase 107) -JUCE supplies an [optional workstation frontend](../src/gui/JUCE/README.md) -(`XTC_BUILD_JUCE_GUI`) as well as runnable plugin examples (`XTC_BUILD_JUCE`) -alongside xtc's C99 engine and FLTK application. The two options are independent. It is an external SDK dependency, not vendored, fetched, linked by +JUCE is used by optional, runnable examples alongside xtc's C99 engine and FLTK +application. It is an external SDK dependency, not vendored, fetched, linked by normal builds or automatically installed. The integration uses: | JUCE capability | Implemented use | diff --git a/docs/LICENSES.md b/docs/LICENSES.md index b9b5875..a683894 100644 --- a/docs/LICENSES.md +++ b/docs/LICENSES.md @@ -79,11 +79,3 @@ repository code; no Steinberg VST2 SDK or third-party plugin is bundled. Hosting an installed binary is distinct from SDK, trademark and plugin redistribution rights. Review those separately for any proposed distribution. Source archives retain upstream notices; binary installs also ship both header licenses. - -## Optional JUCE workstation frontend - -`XTC_BUILD_JUCE_GUI` links the user-supplied JUCE SDK under its AGPLv3 or -commercial licence. It is off by default and separate from the MIT C core and -FLTK frontend. No SDK source is copied into this repository or downloaded by -normal configuration. Review the supplied SDK's licence and its graphics/text -dependencies when distributing `xtc-juce`. See [the frontend guide](../src/gui/JUCE/README.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 23e0067..2da2677 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -3739,47 +3739,6 @@ Play/Stop/Space/stereo check also passed. See the integration-test guide for com --- -## Phase 113 — Optional JUCE workstation frontend - -### Tasks and acceptance criteria - -- [x] Preserve the FLTK sources and default build; add an independently optional - `XTC_BUILD_JUCE_GUI` workstation under `src/gui/JUCE`. -- [x] Require a user-supplied SDK through `XTC_JUCE_ROOT`, with an explicit error - when enabled without the SDK and no SDK discovery/download in normal builds. -- [x] Port the main window, sequencer/master and child views, graph, mixer, - automation, piano roll, velocity editing, sample editor, plugin dialogs, - insert chains, preferences, searchable commands and support widgets. -- [x] Use JUCE components, controls, graphics, native events, dialogs and timers; - keep project/history/DSP/backend/plugin APIs in C and preserve `.xtc` files. -- [x] Allow JUCE-only, FLTK-only, both and core-only configurations; share the - externally supplied SDK with the independently optional plugin examples. -- [x] Include the JUCE binary in the owned install/uninstall inventory when selected. -- [x] Pass the retained GUI regression workflows against JUCE, including failure - rollback, child ownership, gestures, preferences and MIDI device fallback. -- [x] Verify native JUCE transport events and finite stereo playback with the - bundled test song and missing saved MIDI devices. -- [x] Validate the original FLTK build/tests and document final build results. - -### Implementation notes - -The frontend retains the original C-facing command structure and filenames. -`ui.*` maps the retained coordinates and callback conventions to JUCE, with no -FLTK headers or runtime dependency. Native controls may look different. Both -frontends continue to use the existing JACK/PipeWire backend. See the -[frontend source and build guide](../src/gui/JUCE/README.md). -JUCE is an optional external AGPLv3/commercial SDK; normal builds do not link it. -Qualification is for Linux, rather than a new cross-platform audio backend. -The original 57 FLTK/core tests, core-only build, missing-SDK diagnostic, shared -SDK configuration and staged install/uninstall checks passed. The frontend was -built with warnings enabled against external JUCE 8.0.15. Native Play/Stop/Space, -expanded track rendering and finite stereo playback passed with missing saved -MIDI devices. All 12 JUCE GUI workflows passed, with focused reruns after the -native focus/blur adjustments. The [workstation capture](../screenshots/juce-workstation.png) -comes from this real-backend regression, with no physical routes changed. - ---- - ## Recommended Execution Order Reorder the phases when the audit reveals a blocking dependency. Do not mark a diff --git a/examples/juce/CMakeLists.txt b/examples/juce/CMakeLists.txt index a3974e7..6bd1342 100644 --- a/examples/juce/CMakeLists.txt +++ b/examples/juce/CMakeLists.txt @@ -11,9 +11,7 @@ endif() message(STATUS "JUCE is AGPLv3/commercial: review the external SDK LICENSE.md before distributing these binaries") set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE) -if(NOT TARGET juce::juce_audio_utils) - add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL) -endif() +add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL) get_filename_component(XTC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) if(NOT TARGET xtc-core) set(XTC_BUILD_GUI OFF CACHE BOOL "" FORCE) diff --git a/screenshots/ALL.md b/screenshots/ALL.md index 702987f..e7fee09 100644 --- a/screenshots/ALL.md +++ b/screenshots/ALL.md @@ -5,15 +5,6 @@ See [capture notes and reproduction instructions](README.md) for image provenanc --- -## The optional JUCE workstation - -![JUCE workstation playing the bundled test project](juce-workstation.png) - -The JUCE frontend plays the bundled test song with an expanded velocity lane; -missing saved MIDI devices appear as a warning while stereo playback continues. - ---- - ## The xtc workspace at a glance ![The xtc workspace at a glance](overview.png) diff --git a/screenshots/README.md b/screenshots/README.md index 4b39f4b..a224ddf 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -32,23 +32,6 @@ APIs and GUI history/preparation paths. Original projects, media and user configuration are not overwritten. Black margins are the virtual desktop; floating views show the actual detached windows together. -## Optional JUCE workstation (Phase 113) - -![JUCE workstation playing the bundled test project](juce-workstation.png) - -Rendered directly from the actual JUCE component hierarchy, with the first -track expanded and playback running. The test deliberately configures missing -MIDI devices and captures finite stereo output through private JACK probe ports. -It preserves user preferences and physical routes. A running JACK-compatible -server is required. After building the optional frontend and tests: - -```sh -XTC_JUCE_SCREENSHOT="$PWD/screenshots/juce-workstation.png" \ - xvfb-run -a -s '-screen 0 1440x960x24' ./build/juce-gui/test_juce_playback -``` - -See the [JUCE frontend guide](../src/gui/JUCE/README.md) for SDK and build options. - ## Continuous audio velocity editing (Phase 110) ![Audio velocity envelope beneath a waveform track](audio-velocity-expanded.png) diff --git a/screenshots/juce-workstation.png b/screenshots/juce-workstation.png deleted file mode 100644 index 2b62f5d..0000000 Binary files a/screenshots/juce-workstation.png and /dev/null differ diff --git a/src/gui/JUCE/README.md b/src/gui/JUCE/README.md deleted file mode 100644 index c4656b7..0000000 --- a/src/gui/JUCE/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# JUCE workstation frontend - -This directory ports the workstation GUI to JUCE. The sibling FLTK sources are -retained. Both frontends call the same C99 project, history, DSP, plugin and JACK -APIs and open the same `.xtc` files. No FLTK headers or library are required by -this frontend. - -## Build and run - -Supply your own JUCE SDK source checkout. CMake does not download the SDK: - -```sh -cmake -S . -B build/juce-gui -DCMAKE_BUILD_TYPE=Debug \ - -DXTC_BUILD_JUCE_GUI=ON -DXTC_BUILD_GUI=OFF \ - -DXTC_JUCE_ROOT=/absolute/path/to/JUCE -cmake --build build/juce-gui --parallel 4 --target xtc-juce -./build/juce-gui/xtc-juce examples/projects/playback-test.xtc -``` - -Leave `XTC_BUILD_GUI=ON` to build both `xtc` (FLTK) and `xtc-juce`. Set both GUI -options off for a core-only build. `XTC_BUILD_JUCE` independently enables the -Soft Keys plugin examples; it is not required for this application. Both JUCE -options can share one SDK checkout in the same build. - -`cmake --install build/juce-gui` includes `xtc-juce` through the existing owned -file inventory; set `XTC_INSTALL_JUCE_GUI=OFF` to exclude it. `uninstall` removes -the same owned binary. Standard binary packages remain core/FLTK packages. - -The qualified platform is Linux with JUCE 8.0.15 and the existing JACK/PipeWire -backend. This port does not replace JACK with JUCE AudioDeviceManager or change -the C engine's platform support. Playback needs a running JACK-compatible server; -MIDI devices are optional. Route the stereo outputs to speakers to listen. - -JUCE is **AGPLv3/commercial**, supplied externally. Review its SDK licence before -redistributing linked binaries. The SDK increases compile time and requires its -normal Linux graphics/windowing development dependencies. Normal FLTK and core -builds neither discover nor link JUCE. - -## Components and maintenance - -The matching filenames preserve the existing command and model boundaries: - -| Files | Responsibility | -| --- | --- | -| `main.cpp`, `main_window.*` | Application, menus, transport, project lifecycle, history and backend control | -| `sequencer_widget.*` | Master selection, coordinates, commands and view reconciliation | -| `sequencer_{track,clip,velocity,ruler}_widget.*` | Actual JUCE child components for tracks, headers, clips, velocity and ruler | -| `sequencer_commands.cpp`, `sequencer_instruments.cpp`, `sequencer_waveforms.cpp` | Editing commands, instrument choices and waveform caches | -| `graph_widget.*`, `mixer_widget.*`, `piano_roll.*`, `sample_editor.*` | Graph editing, mixer/automation, note editing and destructive sample-copy editing | -| `preferences_dialog.hpp`, `plugin_dialog.*`, `plugin_library.hpp`, `track_inserts.hpp`, `command_palette.hpp` | Preferences, plugin discovery/controls, insert chains and searchable commands | -| `theme.hpp`, `snap.hpp`, `velocity_lane.hpp`, `insert_meter.hpp` | Appearance, snapping, controller lane and meters | -| `ui.*` | JUCE component adapter: ownership, retained coordinates, drawing, events, controls, menus, dialogs, scrolling and timers | - -`UiWidget` derives from `juce::Component`; the sequencer hierarchy is a JUCE -component hierarchy. Groups own their workstation children; JUCE also tracks -native child controls. Explicit removal detaches without deleting. Deferred -removal uses a JUCE safe pointer and the message queue. JUCE text editors, -buttons, sliders, list boxes, scrollbars, popup menus and file choosers provide -the platform interaction. Custom canvases draw with `juce::Graphics`; overlays -paint after child components. The rotary pan control uses JUCE's rotary slider -and double-click reset. GUI operations and timers stay on the message thread; -audio callbacks continue to run only C code. - -This is a retained-command port rather than a redesign of the workstation. -When changing a GUI workflow, update both frontend implementations. The common -GUI regression source is compiled against each toolkit, preventing separate -test suites from drifting. The JUCE variant shows the track inspector before -requesting native keyboard focus, since hidden components cannot receive it, and -accounts for JUCE text editors committing an additional undoable edit on blur. The JUCE adapter intentionally retains the existing -coordinate and callback conventions; it does not emulate or link the FLTK -runtime. Frontend-specific appearance and native dialogs differ. - -See [build and validation guidance](../../../docs/BUILD.md) and -[GUI architecture](../../../docs/ARCHITECTURE.md). diff --git a/src/gui/JUCE/command_palette.hpp b/src/gui/JUCE/command_palette.hpp deleted file mode 100644 index d4c1c27..0000000 --- a/src/gui/JUCE/command_palette.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef XTC_COMMAND_PALETTE_HPP -#define XTC_COMMAND_PALETTE_HPP -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -/* UI command search only; execute the chosen action after this modal is gone. */ -class CommandPalette : public UiDouble_Window -{ - std::vector commands_, visible_; - UiInput *search_; - UiHold_Browser *list_; - std::string selected_; - static std::string lower(std::string value) - { - for (auto &c : value) - c = static_cast(std::tolower(static_cast(c))); - return value; - } - void filter() - { - list_->clear(); - visible_.clear(); - const auto query = lower(search_->value()); - for (const auto &command : commands_) - if (lower(command).find(query) != std::string::npos) { - visible_.push_back(command); - list_->add(command.c_str()); - } - if (!visible_.empty()) - list_->select(1); - } - void accept() - { - int i = list_->value(); - if (i > 0 && static_cast(i) <= visible_.size()) { - selected_ = visible_[static_cast(i - 1)]; - hide(); - } - } - - public: - explicit CommandPalette(std::vector commands) - : UiDouble_Window(700, 440, "xtc — Commands"), commands_(std::move(commands)) - { - set_modal(); - begin(); - search_ = new UiInput(136, 18, 544, 32, "Search commands"); - search_->when(UI_WHEN_CHANGED); - search_->callback([](UiWidget *, void *p) { static_cast(p)->filter(); }, this); - list_ = new UiHold_Browser(20, 64, 660, 302); - list_->format_char(0); - list_->callback( - [](UiWidget *, void *p) { - if (Ui::event_clicks()) - static_cast(p)->accept(); - }, - this); - auto *cancel = new UiButton(450, 390, 100, 30, "Cancel"); - cancel->callback([](UiWidget *, void *p) { static_cast(p)->hide(); }, this); - auto *run = new UiButton(564, 390, 116, 30, "Run command"); - run->callback([](UiWidget *, void *p) { static_cast(p)->accept(); }, this); - callback([](UiWidget *, void *p) { static_cast(p)->hide(); }, this); - end(); - xtc_ui::style(this); - filter(); - } - int handle(int event) override - { - if (event == UI_KEYDOWN) { - int key = Ui::event_key(); - if (key == UI_Enter || key == UI_KP_Enter) { - accept(); - return 1; - } - if (key == UI_Escape) { - hide(); - return 1; - } - if ((key == UI_Up || key == UI_Down) && !visible_.empty()) { - list_->select( - std::clamp(list_->value() + (key == UI_Up ? -1 : 1), 1, static_cast(visible_.size()))); - return 1; - } - } - return UiDouble_Window::handle(event); - } - std::string run() - { - show(); - search_->take_focus(); - while (shown()) - Ui::wait(); - return selected_; - } -}; -#endif diff --git a/src/gui/JUCE/graph_widget.cpp b/src/gui/JUCE/graph_widget.cpp deleted file mode 100644 index 8642d6b..0000000 --- a/src/gui/JUCE/graph_widget.cpp +++ /dev/null @@ -1,645 +0,0 @@ -#include "theme.hpp" -#include "graph_widget.hpp" -#include "xtc/xtc_plugin.h" -#include "ui.hpp" -#include -#include -#include - -namespace { -constexpr int node_width = 150, node_height = 74; - -/** - * @brief Returns the display label for a node type. - * - * @param type Node type to describe. - * @return Borrowed static string; never NULL, falling back to "Unknown". - */ -const char *node_name(xtc_node_type type) -{ - switch (type) { - case XTC_NODE_OSCILLATOR: - return "Oscillator"; - case XTC_NODE_GAIN: - return "Gain / bus"; - case XTC_NODE_METER: - return "Meter"; - case XTC_NODE_PLUGIN: - return "Effect plugin"; - case XTC_NODE_NATIVE: - return "Native processor"; - case XTC_NODE_INSTRUMENT: - return "MIDI mix"; - case XTC_NODE_AUDIO: - return "Audio mix"; - case XTC_NODE_OUTPUT: - return "Output sum"; - } - return "Unknown"; -} -} // namespace - -/** - * @brief Constructs the widget and initializes its cached layout from the graph. - * - * The graph, handler, context, and feedback box are stored without taking - * ownership. Layout population is delegated to set_graph(). - * - * @param left Left coordinate in JUCE screen space. - * @param top Top coordinate in JUCE screen space. - * @param width Widget width in pixels. - * @param height Widget height in pixels. - * @param graph Borrowed graph snapshot; must outlive the widget's use of it. - * @param handler Edit callback invoked for every graph mutation. - * @param context Opaque pointer passed back to @p handler; may be NULL. - * @param feedback_box Borrowed label used to report edit results. - */ -GraphWidget::GraphWidget(int left, int top, int width, int height, const xtc_graph *graph, EditHandler handler, - void *context, UiBox *feedback_box) - : UiWidget(left, top, width, height), graph_(graph), handler_(handler), context_(context), feedback_(feedback_box) -{ - set_graph(graph); -} - -/** - * @brief Replaces the graph snapshot and rebuilds cached node positions. - * - * Existing positions are preserved by node ID. New nodes get grid slots that do - * not overlap previously known nodes. The selection is dropped when the selected - * node or wire is no longer present. The widget does not take ownership of the - * graph. - * - * @param graph Borrowed graph that must outlive the widget's use of it. - */ -void GraphWidget::set_graph(const xtc_graph *graph) -{ - graph_ = graph; - routing_count_ = 0; - (void)xtc_graph_analyze(graph_, routing_.data(), routing_.size(), &routing_count_); - size_t nodes = 0U, edges = 0U; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) { - count_ = 0U; - redraw(); - return; - } - auto old = positions_; - size_t old_count = count_; - count_ = 0U; - for (size_t i = 0U; i < nodes && i < positions_.size(); ++i) { - xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) - continue; - Position position; - position.id = node.id; - bool found = false; - for (size_t j = 0U; j < old_count; ++j) - if (old[j].id == node.id) { - position = old[j]; - found = true; - break; - } - if (!found) { - for (size_t slot = 0U; slot <= positions_.size(); ++slot) { - position.left = 40 + static_cast(slot % 8U) * 210; - position.top = 40 + static_cast(slot / 8U) * 115; - bool occupied = false; - for (size_t j = 0U; j < old_count; ++j) - if (std::abs(old[j].left - position.left) < node_width + 12 && - std::abs(old[j].top - position.top) < node_height + 12) - occupied = true; - for (size_t j = 0U; j < count_; ++j) - if (std::abs(positions_[j].left - position.left) < node_width + 12 && - std::abs(positions_[j].top - position.top) < node_height + 12) - occupied = true; - if (!occupied) - break; - } - } - positions_[count_++] = position; - } - int left, top; - if (!node_position(selected_node_, left, top)) - selected_node_ = SIZE_MAX; - if (edge_selected_ && !select_connection(selected_edge_.source_node, selected_edge_.destination_node)) - edge_selected_ = false; - grow_canvas(); - redraw(); -} - -/** - * @brief Looks up the cached position of a node. - * - * @param id Stable node identifier. - * @param left Receives the left position in widget-local coordinates. - * @param top Receives the top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::node_position(size_t id, int &left, int &top) const -{ - for (size_t i = 0U; i < count_; ++i) - if (positions_[i].id == id) { - left = positions_[i].left; - top = positions_[i].top; - return true; - } - return false; -} - -/** - * @brief Moves a node within the widget, clamped to the visible bounds. - * - * The node keeps a 12-pixel margin from the widget edges. - * - * @param id Stable node identifier. - * @param left Requested left position in widget-local coordinates. - * @param top Requested top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::move_node(size_t id, int left, int top) -{ - for (size_t i = 0U; i < count_; ++i) - if (positions_[i].id == id) { - positions_[i].left = std::max(12, std::min(left, w() - node_width - 12)); - positions_[i].top = std::max(12, std::min(top, h() - node_height - 12)); - redraw(); - return true; - } - return false; -} - -/** - * @brief Selects a node by ID and clears any wire selection. - * - * @param id Stable node identifier. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::select_node(size_t id) -{ - int left, top; - if (!node_position(id, left, top)) - return false; - selected_node_ = id; - edge_selected_ = false; - describe_selection(); - redraw(); - return true; -} - -/** - * @brief Selects a wire between two nodes and clears the node selection. - * - * @param source Source node identifier. - * @param destination Destination node identifier. - * @return True when a matching connection exists, false otherwise. - */ -bool GraphWidget::select_connection(size_t source, size_t destination) -{ - size_t nodes, edges; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) - return false; - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - if (xtc_graph_get_connection(graph_, i, &edge) == XTC_OK && edge.source_node == source && - edge.destination_node == destination) { - selected_edge_ = edge; - edge_selected_ = true; - selected_node_ = SIZE_MAX; - describe_selection(); - redraw(); - return true; - } - } - return false; -} - -/** - * @brief Shows the outcome of an edit in the feedback label. - * - * @param result Result returned by the edit handler. - * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. - */ -void GraphWidget::feedback(xtc_result result, const char *success) -{ - char text[256]; - if (result == XTC_OK) - std::snprintf(text, sizeof(text), "%s", success); - else if (result == XTC_ERR_CAPACITY) - std::snprintf( - text, sizeof(text), - "Graph unchanged: capacity exceeded (node limit or pending edit). Retry after the next audio block."); - else if (result == XTC_ERR_GRAPH_CYCLE) - std::snprintf(text, sizeof(text), "Graph unchanged: %s. Feedback loops are unsupported.", xtc_result_string(result)); - else - std::snprintf(text, sizeof(text), "Graph unchanged: %s. Connect an audio output (right) to an input (left); duplicate wires are rejected.", xtc_result_string(result)); - feedback_->copy_label(text); -} - -/** - * @brief Requests creation of a node through the edit handler. - * - * Selects the new node when the edit succeeds. - * - * @param type Kind of node to add. - * @return Result reported by the edit handler. - */ -xtc_result GraphWidget::add_node(xtc_node_type type) -{ - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_ADD_NODE; - edit.data.add.type = type; - edit.data.add.value = type == XTC_NODE_OSCILLATOR ? 440.0f : type == XTC_NODE_GAIN ? 0.1f : 0.0f; - size_t added = SIZE_MAX; - xtc_result result = handler_(context_, edit, &added); - if (result == XTC_OK) - select_node(added); - feedback(result, "Node added. Drag its title to move; drag an output port to an input to connect."); - return result; -} - -/** - * @brief Requests a connection between two ports through the edit handler. - * - * @param connection Connection describing the source and destination node/port pairs. - * @return Result reported by the edit handler. - */ -xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) -{ - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_CONNECT; - edit.data.connection = connection; - xtc_result result = handler_(context_, edit, nullptr); - feedback(result, "Connection added. Click a wire and press Delete to disconnect."); - return result; -} - -/** - * @brief Requests removal of the selected wire or node through the edit handler. - * - * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when - * nothing is selected. - */ -xtc_result GraphWidget::delete_selected() -{ - xtc_graph_edit edit{}; - if (edge_selected_) { - edit.kind = XTC_EDIT_DISCONNECT; - edit.data.connection = selected_edge_; - } else if (selected_node_ != SIZE_MAX) { - edit.kind = XTC_EDIT_REMOVE_NODE; - edit.data.node_id = selected_node_; - } else { - feedback_->copy_label("Select a node or wire first."); - return XTC_ERR_INVALID_STATE; - } - xtc_result result = handler_(context_, edit, nullptr); - feedback(result, "Selection deleted. Unwired inputs use external audio; select a node to inspect routing."); - return result; -} - -/** - * @brief Computes the widget-local center of a node port. - * - * @param id Stable node identifier. - * @param port Port index; 0 selects the input and 1 the output. - * @param left Receives the port center x coordinate. - * @param top Receives the port center y coordinate. - * @return True when the node and port exist, false otherwise. - */ -bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const -{ - xtc_port_info info{}; - if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top)) - return false; - left += info.direction == XTC_PORT_OUTPUT ? node_width : 0; - top += 48; - return true; -} - -/** - * @brief Finds the port nearest to a widget-local point. - * - * Searches nodes in reverse draw order and accepts points within 9 pixels of a - * port center. - * - * @param left Widget-local x coordinate to test. - * @param top Widget-local y coordinate to test. - * @param node Receives the matching node ID when true is returned. - * @param port Receives the matching port index (0 input, 1 output) when true is returned. - * @return True when a port is within range, false otherwise. - */ -bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const -{ - for (size_t i = count_; i > 0U; --i) - for (size_t p = 0U; p < 2U; ++p) { - int px, py; - if (port_position(positions_[i - 1U].id, p, px, py) && std::abs(px - left) <= 9 && - std::abs(py - top) <= 9) { - node = positions_[i - 1U].id; - port = p; - return true; - } - } - return false; -} - -/** - * @brief Draws the background, wires, nodes, ports, and any pending wire. - */ -void GraphWidget::draw() -{ - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(25, 28, 33)); - ui_rectf(x(), y(), w(), h()); - size_t nodes = 0U, edges = 0U; - (void)xtc_graph_get_counts(graph_, &nodes, &edges); - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - int sx, sy, dx, dy; - if (xtc_graph_get_connection(graph_, i, &edge) != XTC_OK || !port_position(edge.source_node, 1U, sx, sy) || - !port_position(edge.destination_node, 0U, dx, dy)) - continue; - bool selected = (edge_selected_ && selected_edge_.source_node == edge.source_node && - selected_edge_.destination_node == edge.destination_node) || - selected_node_ == edge.source_node || selected_node_ == edge.destination_node; - xtc_routing_info route{}; - bool audible_path = routing(edge.destination_node, route) && route.reaches_output; - ui_color(selected ? xtc_ui::accent() : audible_path ? ui_rgb_color(100, 200, 190) : ui_rgb_color(110, 115, 125)); - ui_line_style(UI_SOLID, selected ? 3 : 2); - ui_line(x() + sx, y() + sy, x() + dx, y() + dy); - double length = std::hypot(dx-sx, dy-sy); - if (length > 20) { - double ux = (dx-sx)/length, uy = (dy-sy)/length; - int ax = x() + (sx+dx)/2, ay = y() + (sy+dy)/2; - ui_line(ax, ay, ax-static_cast(ux*9-uy*5), ay-static_cast(uy*9+ux*5)); - ui_line(ax, ay, ax-static_cast(ux*9+uy*5), ay-static_cast(uy*9-ux*5)); - } - } - ui_line_style(0); - for (size_t i = 0U; i < count_; ++i) { - xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) - continue; - int left = x() + positions_[i].left, top = y() + positions_[i].top; - ui_color(node.type == XTC_NODE_OUTPUT ? ui_rgb_color(100, 50, 55) - : node.type == XTC_NODE_GAIN ? ui_rgb_color(75, 55, 95) - : node.type == XTC_NODE_OSCILLATOR || node.type == XTC_NODE_AUDIO || node.type == XTC_NODE_INSTRUMENT - ? ui_rgb_color(35, 80, 75) : ui_rgb_color(49, 65, 85)); - ui_rectf(left, top, node_width, node_height); - ui_color(selected_node_ == node.id ? xtc_ui::accent() : ui_rgb_color(115, 130, 145)); - ui_rect(left, top, node_width, node_height); - ui_color(UI_WHITE); - ui_font(UI_HELVETICA_BOLD, 12); - char text[96]; - xtc_plugin_spec title_plugin{}; - if(node.type==XTC_NODE_PLUGIN && xtc_graph_get_plugin(graph_,node.id,&title_plugin)==XTC_OK) - std::snprintf(text,sizeof(text),"%s effect #%zu",xtc_plugin_format_name(title_plugin.format),node.id); - else std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id); - ui_push_clip(left+6, top+2, node_width-12, 40); - ui_draw(text, left + 10, top + 20); - ui_font(UI_HELVETICA, 12); - if (node.type == XTC_NODE_OSCILLATOR) - std::snprintf(text, sizeof(text), "%.0f Hz", static_cast(node.value)); - else if (node.type == XTC_NODE_GAIN) - std::snprintf(text, sizeof(text), "x %.2f", static_cast(node.value)); - else if (node.type == XTC_NODE_PLUGIN) { - xtc_plugin_spec plugin{}; - if (xtc_graph_get_plugin(graph_,node.id,&plugin) == XTC_OK) - std::snprintf(text,sizeof(text),"%.20s",plugin.label); - } else - std::snprintf(text, sizeof(text), "%s", - node.type == XTC_NODE_METER ? "Peak pass-through" - : node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source" - : node.type == XTC_NODE_AUDIO ? "Timeline mix source" - : "Summed engine output"); - ui_draw(text, left + 10, top + 37); - ui_pop_clip(); - xtc_routing_info route{}; - if (routing(node.id, route) && !route.reaches_output) { - ui_color(xtc_ui::accent()); - ui_draw("! no output path", left + 10, top + 72); - } - for (size_t port = 0U; port < 2U; ++port) { - int px, py; - if (!port_position(node.id, port, px, py)) - continue; - ui_color(ui_rgb_color(110, 220, 185)); - ui_pie(x() + px - 5, y() + py - 5, 10, 10, 0, 360); - ui_color(UI_WHITE); - ui_draw(port == 0U ? (route.external_input ? "ext in" : "sum in") : "audio out", left + (port == 0U ? 10 : 90), top + 56); - } - } - if (wiring_) { - int px, py; - if (port_position(wire_node_, wire_port_, px, py)) { - ui_color(xtc_ui::accent()); - ui_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_); - } - } - ui_pop_clip(); -} - -/** - * @brief Handles mouse and keyboard events for selection, dragging, and wiring. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, 0 for unsupported left-button input, - * or the result of UiWidget::handle() for unhandled events. - */ -bool GraphWidget::select_at(int left, int top) -{ - for (size_t i = count_; i > 0; --i) { - const auto &pos = positions_[i-1]; - if (left >= pos.left && left <= pos.left+node_width && top >= pos.top && top <= pos.top+node_height) - return select_node(pos.id); - } - size_t nodes = 0, edges = 0; - xtc_graph_get_counts(graph_,&nodes,&edges); - for (size_t i = 0; i < edges; ++i) { - xtc_connection_info edge{}; int sx, sy, dx, dy; - xtc_graph_get_connection(graph_,i,&edge); - if (!port_position(edge.source_node,1,sx,sy) || !port_position(edge.destination_node,0,dx,dy)) continue; - double vx = dx-sx, vy = dy-sy, length = vx*vx+vy*vy; - double t = length == 0 ? 0 : std::clamp(((left-sx)*vx+(top-sy)*vy)/length,0.0,1.0); - if (std::hypot(left-sx-t*vx,top-sy-t*vy) <= 6) return select_connection(edge.source_node,edge.destination_node); - } - selected_node_ = SIZE_MAX; edge_selected_ = false; describe_selection(); redraw(); return false; -} - -int GraphWidget::handle(int event) -{ - const int left = Ui::event_x() - x(), top = Ui::event_y() - y(); - switch (event) { - case UI_FOCUS: - case UI_UNFOCUS: - return 1; - case UI_PUSH: { - if (Ui::event_button() == UI_RIGHT_MOUSE) { - take_focus(); select_at(left,top); - UiMenu_Item items[5]{}; - items[0].text = "Inspect selection"; - items[1].text = "Arrange by signal flow (L)"; - items[2].text = "Insert unity gain in selected wire (I)"; - items[3].text = "Delete selection"; - const auto *picked = items->popup(Ui::event_x(), Ui::event_y()); - if (picked == items) describe_selection(); - else if (picked == items+1) auto_layout(); - else if (picked == items+2) insert_gain(); - else if (picked == items+3) delete_selected(); - return 1; - } - if (Ui::event_button() != UI_LEFT_MOUSE) return 0; - take_focus(); - mouse_x_ = left; - mouse_y_ = top; - dragging_ = false; - wiring_ = false; - if (hit_port(left, top, wire_node_, wire_port_)) { - wiring_ = true; - redraw(); - return 1; - } - for (size_t i = count_; i > 0U; --i) { - const Position &pos = positions_[i - 1U]; - if (left >= pos.left && left <= pos.left + node_width && top >= pos.top && top <= pos.top + node_height) { - select_node(pos.id); - dragging_ = true; - drag_x_ = left - pos.left; - drag_y_ = top - pos.top; - return 1; - } - } - size_t nodes, edges; - (void)xtc_graph_get_counts(graph_, &nodes, &edges); - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - int sx, sy, dx, dy; - (void)xtc_graph_get_connection(graph_, i, &edge); - if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) - continue; - double vx = dx - sx, vy = dy - sy; - double length = vx * vx + vy * vy; - double t = - length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length)); - if (std::hypot(left - sx - t * vx, top - sy - t * vy) <= 6.0) { - select_connection(edge.source_node, edge.destination_node); - return 1; - } - } - selected_node_ = SIZE_MAX; - edge_selected_ = false; - redraw(); - return 1; - } - case UI_DRAG: - mouse_x_ = left; - mouse_y_ = top; - if (dragging_) - move_node(selected_node_, left - drag_x_, top - drag_y_); - if (wiring_) - redraw(); - return 1; - case UI_RELEASE: - dragging_ = false; - if (wiring_) { - size_t node, port; - wiring_ = false; - if (hit_port(left, top, node, port)) - connect_ports({wire_node_, wire_port_, node, port}); - else - feedback_->copy_label("Connection cancelled. Drag an output port to an input port."); - redraw(); - } - return 1; - case UI_KEYDOWN: - if (Ui::event_key() == 'l' && !Ui::event_state(UI_CTRL | UI_ALT)) { auto_layout(); return 1; } - if (Ui::event_key() == 'i' && !Ui::event_state(UI_CTRL | UI_ALT)) { insert_gain(); return 1; } - if ((Ui::event_key() == UI_Left || Ui::event_key() == UI_Right) && count_) { - size_t selected = 0; - for (size_t i = 0; i < count_; ++i) if (positions_[i].id == selected_node_) { - selected = Ui::event_key() == UI_Left ? (i+count_-1)%count_ : (i+1)%count_; break; - } - select_node(positions_[selected].id); - return 1; - } - if (Ui::event_key() == UI_Delete || Ui::event_key() == UI_BackSpace) { - delete_selected(); - return 1; - } - if (Ui::event_key() == UI_Escape) { - wiring_ = dragging_ = false; - redraw(); - return 1; - } - break; - default: - break; - } - return UiWidget::handle(event); -} - -bool GraphWidget::routing(size_t id, xtc_routing_info &info) const -{ - for (size_t i = 0; i < routing_count_; ++i) if (routing_[i].id == id) { info = routing_[i]; return true; } - return false; -} - -void GraphWidget::grow_canvas() -{ - int width = 2000, height = 4000; - for (size_t i = 0; i < count_; ++i) { - width = std::max(width, positions_[i].left + node_width + 40); - height = std::max(height, positions_[i].top + node_height + 40); - } - size(width, height); - if (parent()) parent()->redraw(); -} - -bool GraphWidget::auto_layout() -{ - if (routing_count_ != count_) return false; - std::array rows{}; - for (size_t i = 0; i < count_; ++i) { - xtc_routing_info info{}; - if (!routing(positions_[i].id, info) || info.level >= rows.size()) return false; - positions_[i].left = 40 + static_cast(info.level) * 210; - positions_[i].top = 40 + static_cast(rows[info.level]++) * 115; - } - wiring_ = dragging_ = false; - grow_canvas(); - feedback_->copy_label("Arranged left to right by signal flow. Layout is session-only; no audio or routing changes."); - redraw(); - return true; -} - -xtc_result GraphWidget::insert_gain() -{ - if (!edge_selected_) { - feedback_->copy_label("Select a wire first, then insert a unity gain/bus node."); - return XTC_ERR_INVALID_STATE; - } - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_INSERT_GAIN; - edit.data.connection = selected_edge_; - size_t added = SIZE_MAX; - xtc_result result = handler_(context_, edit, &added); - if (result == XTC_OK) select_node(added); - feedback(result, "Unity gain inserted into wire as one edit. Inputs sum; fan-out copies the signal without normalization."); - return result; -} - -void GraphWidget::describe_selection() -{ - char text[320]; - if (edge_selected_) { - std::snprintf(text, sizeof(text), "Wire: #%zu audio out -> #%zu sum in. Delete disconnects; I inserts a unity gain. Destination sums its inputs.", - selected_edge_.source_node, selected_edge_.destination_node); - } else { - xtc_routing_info info{}; - if (!routing(selected_node_, info)) { feedback_->copy_label("Select a node or wire to inspect routing."); return; } - std::snprintf(text, sizeof(text), "Node #%zu: %zu incoming, %zu outgoing | %s | %s. Ports are audio lanes; stereo uses paired independent processing.", - info.id, info.inputs, info.outputs, - info.reaches_output ? "path to engine output" : "NO output path", - info.external_input ? "unwired input uses external audio" : "wired sum or source"); - } - feedback_->copy_label(text); -} diff --git a/src/gui/JUCE/graph_widget.hpp b/src/gui/JUCE/graph_widget.hpp deleted file mode 100644 index aefa6a8..0000000 --- a/src/gui/JUCE/graph_widget.hpp +++ /dev/null @@ -1,212 +0,0 @@ -#ifndef XTC_GRAPH_WIDGET_HPP -#define XTC_GRAPH_WIDGET_HPP -#include "ui.hpp" -#include -#include "xtc/xtc_engine.h" - -class UiBox; - -/* GUI positions/selection only. Domain snapshots and edits stay in the C API. */ -class GraphWidget : public UiWidget { - public: - /** - * @brief Applies a graph edit using an opaque context and optional added-node output. - * - * The handler receives the context, edit command, and output identifier in that order. - * @return XTC_OK on success, or an edit error. - */ - using EditHandler = xtc_result (*)(void *, const xtc_graph_edit &, size_t *); - /** - * @brief Constructs a graph widget bound to a graph and an edit callback. - * - * The widget stores the graph, handler, context, and feedback box without - * taking ownership of any of them. - * - * @param x Left coordinate in JUCE screen space. - * @param y Top coordinate in JUCE screen space. - * @param width Widget width in pixels. - * @param height Widget height in pixels. - * @param graph Borrowed graph snapshot; must outlive the widget's use of it. - * @param handler Edit callback invoked for every graph mutation. - * @param context Opaque pointer passed back to @p handler; may be NULL. - * @param feedback Borrowed label used to report edit results; must outlive the widget. - */ - GraphWidget(int x, int y, int width, int height, const xtc_graph *graph, EditHandler handler, void *context, - UiBox *feedback); - - /** - * @brief Clears cached node positions, selection, and interaction state. - * - * Does not change the borrowed graph pointer or the graph's own layout. - */ - void reset_layout() - { - count_ = 0; - selected_node_ = SIZE_MAX; - edge_selected_ = dragging_ = wiring_ = false; - } - - /** - * @brief Replaces the graph snapshot and rebuilds cached node positions. - * - * Existing positions are preserved by node ID; new nodes are placed on a grid - * that avoids overlapping previously known nodes. Selections that no longer - * exist are cleared. The widget does not take ownership of @p graph. - * - * @param graph Borrowed graph that must outlive the widget's use of it. - */ - void set_graph(const xtc_graph *graph); - - /** - * @brief Returns the graph snapshot currently displayed. - * - * The returned pointer is borrowed and valid for GUI thread use only. - * - * @return Borrowed graph pointer; may be NULL when none was supplied. - */ - size_t selected_node() const { return selected_node_; } - - const xtc_graph *graph() const - { - return graph_; - } // Borrowed, GUI thread only. - - /** - * @brief Requests creation of a node through the edit handler. - * - * Selects the new node when the edit succeeds. - * - * @param type Kind of node to add. - * @return Result reported by the edit handler. - */ - xtc_result add_node(xtc_node_type type); - xtc_result insert_gain(); - bool selected_connection(xtc_connection_info &out) const { if (!edge_selected_) return false; out = selected_edge_; return true; } - bool auto_layout(); // Presentation only, no engine publication or undo step. - bool routing(size_t id, xtc_routing_info &info) const; - - - /** - * @brief Requests a connection between two ports through the edit handler. - * - * @param connection Connection describing the source and destination node/port pairs. - * @return Result reported by the edit handler. - */ - xtc_result connect_ports(const xtc_connection_info &connection); - - /** - * @brief Requests removal of the selected wire or node through the edit handler. - * - * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when - * nothing is selected. - */ - xtc_result delete_selected(); - - /** - * @brief Selects a node by ID and clears any wire selection. - * - * @param id Stable node identifier. - * @return True when the node has a cached position, false otherwise. - */ - bool select_node(size_t id); - bool select_at(int left, int top); // Canvas-local coordinates; selection only. - - /** - * @brief Selects a wire between two nodes and clears the node selection. - * - * @param source Source node identifier. - * @param destination Destination node identifier. - * @return True when a matching connection exists, false otherwise. - */ - bool select_connection(size_t source, size_t destination); - - /** - * @brief Moves a node within the widget, clamped to the visible bounds. - * - * @param id Stable node identifier. - * @param left Requested left position in widget-local coordinates. - * @param top Requested top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ - bool move_node(size_t id, int left, int top); - - /** - * @brief Looks up the cached position of a node. - * - * @param id Stable node identifier. - * @param left Receives the left position in widget-local coordinates. - * @param top Receives the top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ - bool node_position(size_t id, int &left, int &top) const; - - /** - * @brief Handles mouse and keyboard events for selection, dragging, and wiring. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, 0 for unsupported left-button input, - * or the result of UiWidget::handle() for unhandled events. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the background, wires, nodes, ports, and any pending wire. - */ - void draw() override; - - private: - struct Position { - size_t id = SIZE_MAX; - int left = 0, top = 0; - }; - - std::array positions_{}; - size_t count_ = 0U, routing_count_ = 0U; - std::array routing_{}; - void describe_selection(); - void grow_canvas(); - const xtc_graph *graph_; - EditHandler handler_; - void *context_; - UiBox *feedback_; - size_t selected_node_ = SIZE_MAX; - xtc_connection_info selected_edge_{}; - bool edge_selected_ = false; - bool dragging_ = false, wiring_ = false; - size_t wire_node_ = SIZE_MAX, wire_port_ = 0U; - int drag_x_ = 0, drag_y_ = 0, mouse_x_ = 0, mouse_y_ = 0; - /** - * @brief Shows the outcome of an edit in the feedback label. - * - * @param result Result returned by the edit handler. - * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. - */ - void feedback(xtc_result result, const char *success); - - /** - * @brief Finds the port nearest to a widget-local point. - * - * Searches nodes in reverse draw order and accepts points within 9 pixels of a - * port center. - * - * @param left Widget-local x coordinate to test. - * @param top Widget-local y coordinate to test. - * @param node Receives the matching node ID when true is returned. - * @param port Receives the matching port index (0 input, 1 output) when true is returned. - * @return True when a port is within range, false otherwise. - */ - bool hit_port(int left, int top, size_t &node, size_t &port) const; - - /** - * @brief Computes the widget-local center of a node port. - * - * @param id Stable node identifier. - * @param port Port index; 0 selects the input and 1 the output. - * @param left Receives the port center x coordinate. - * @param top Receives the port center y coordinate. - * @return True when the node and port exist, false otherwise. - */ - bool port_position(size_t id, size_t port, int &left, int &top) const; -}; -#endif diff --git a/src/gui/JUCE/insert_meter.hpp b/src/gui/JUCE/insert_meter.hpp deleted file mode 100644 index 6148376..0000000 --- a/src/gui/JUCE/insert_meter.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef XTC_INSERT_METER_HPP -#define XTC_INSERT_METER_HPP -#include "xtc/xtc_backend.h" -#include "xtc/xtc_mixer.h" -#include "theme.hpp" -#include -#include -#include -/* UI-only display fed by the existing single backend snapshot reader. */ -class InsertMeter : public UiButton { - xtc_peak_meter left_{-90,0},right_{-90,0}; - uint64_t track_=UINT64_MAX; - public: - InsertMeter(int x,int y,int w,int h):UiButton(x,y,w,h) { - tooltip("Post-track-fader, pre-bus stereo sample peaks. Click to clear the sticky clip indicator."); - callback([](UiWidget *w,void *){static_cast(w)->clear_clips();}); - } - void clear_clips(){left_.clipped=right_.clipped=0;redraw();} - float left_db() const {return left_.db;} - float right_db() const {return right_.db;} - bool clipped() const {return left_.clipped || right_.clipped;} - void feed(const xtc_meter_snapshot &snapshot,uint64_t track,bool active,double seconds) { - if(track_!=track){track_=track;xtc_peak_meter_reset(&left_);xtc_peak_meter_reset(&right_);} - bool found=false;float l=0,r=0; - for(size_t i=0;i=0 ? l : 0,seconds); - (void)xtc_peak_meter_update(&right_,std::isfinite(r) && r>=0 ? r : 0,seconds); - } - redraw(); - } - void draw() override { - draw_box();char text[120];std::snprintf(text,sizeof(text),"Post-fader L %.1f R %.1f dBFS%s",static_cast(left_.db),static_cast(right_.db),clipped() ? " CLIP" : ""); - ui_font(UI_HELVETICA,11);ui_color(clipped() ? ui_rgb_color(245,132,142) : xtc_ui::muted());ui_draw(text,x()+8,y()+12); - int width=std::max(0,(w()-24)/2); - for(unsigned int side=0;side<2;++side) { - int left=x()+8+static_cast(side)*(width+8);float db=side ? right_.db : left_.db; - ui_color(xtc_ui::background());ui_rectf(left,y()+h()-7,width,4); - ui_color(clipped() ? ui_rgb_color(245,132,142) : xtc_ui::accent()); - ui_rectf(left,y()+h()-7,static_cast(static_cast(width)*std::min(1.f,std::max(0.f,(db+90)/90))),4); - } - } -}; -#endif diff --git a/src/gui/JUCE/main.cpp b/src/gui/JUCE/main.cpp deleted file mode 100644 index 695b2f7..0000000 --- a/src/gui/JUCE/main.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "ui.hpp" -#include -#include -#include -#include "main_window.hpp" -#include "xtc/xtc_version.h" -#include "xtc/xtc.h" -#include - -/** - * @brief Runs the JUCE application and reports command-line or startup failures. - * - * @param argc Number of command-line arguments including the program name. - * @param argv Command-line argument vector. - * @return EXIT_SUCCESS after the event loop exits, or EXIT_FAILURE for invalid usage or startup failure. - */ -int main(int argc, char **argv) -{ - if (argc == 2 && !std::strcmp(argv[1], "--version")) { - std::printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); - return EXIT_SUCCESS; - } - if (argc == 2 && !std::strcmp(argv[1], "--diagnose")) { - unsigned int features = xtc_build_features(); - std::printf("xtc %s; project formats 1..%u; JACK %s; LADSPA %s; CLAP/VST2/VST3 mono effects/instruments compiled\nFor limits and recovery commands run xtc-check --diagnose/--help.\n", - XTC_VERSION_STRING,XTC_PROJECT_FORMAT_VERSION,features & XTC_FEATURE_JACK ? "compiled" : "disabled", - features & XTC_FEATURE_LADSPA ? "compiled" : "disabled"); - return EXIT_SUCCESS; - } - if (argc == 2 && !std::strcmp(argv[1], "--help")) { - std::puts( - "Usage: xtc-juce [PROJECT]\n--version, --diagnose and --help work without a display or audio server."); - return EXIT_SUCCESS; - } - const char *project_path = nullptr; - if (argc == 2 && argv[1][0] != '-') { - project_path = argv[1]; - argc = 1; - argv[1] = nullptr; - } - try { - UiInitialiser initialise; - MainWindow window; - if (project_path) { - xtc_result result = window.open_project(project_path); - if (result != XTC_OK) { - std::fprintf(stderr, "Cannot open project: %s\n", xtc_result_string(result)); - return EXIT_FAILURE; - } - } - window.show(argc, argv); - return Ui::run(); - } catch (const std::exception &error) { - std::fprintf(stderr, "GUI startup/runtime failure: %s\n", error.what()); - } catch (...) { - std::fputs("Unexpected GUI startup/runtime failure.\n", stderr); - } - return EXIT_FAILURE; -} diff --git a/src/gui/JUCE/main_window.cpp b/src/gui/JUCE/main_window.cpp deleted file mode 100644 index 5e7f9e6..0000000 --- a/src/gui/JUCE/main_window.cpp +++ /dev/null @@ -1,2856 +0,0 @@ -#include "main_window.hpp" -#include "xtc/xtc_midi_file.h" -#include "graph_widget.hpp" -#include "sequencer_widget.hpp" -#include "piano_roll.hpp" -#include "mixer_widget.hpp" -#include "plugin_dialog.hpp" -#include "sample_editor.hpp" -#include "theme.hpp" -#include "snap.hpp" -#include "velocity_lane.hpp" -#include "preferences_dialog.hpp" -#include "command_palette.hpp" -#include "ui.hpp" -#include "plugin_library.hpp" -#include "track_inserts.hpp" -#include "xtc/xtc_version.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -class PanelWindow : public UiDouble_Window { - MainWindow *owner_; - public: - PanelWindow(int width, int height, const char *title, MainWindow *owner) - : UiDouble_Window(width,height,title), owner_(owner) {} - void resize(int x,int y,int width,int height) override { - UiDouble_Window::resize(x,y,width,height); - if(owner_ && owner_->transport_window()==this)owner_->layout_transport(); - } - int handle(int event) override { - if (event == UI_SHORTCUT && owner_->panel_shortcut(event)) return 1; - return UiDouble_Window::handle(event); - } -}; -void restore_panel(UiWindow *window,int x,int y,int width,int height,int min_width,int min_height) -{ - int sx,sy,sw,sh;Ui::screen_work_area(sx,sy,sw,sh,Ui::screen_num(x,y,width,height)); - width=std::min(std::max(width,min_width),sw);height=std::min(std::max(height,min_height),sh); - x=std::clamp(x,sx,sx+sw-width);y=std::clamp(y,sy,sy+sh-height); - window->resize(x,y,width,height); -} -bool editing_number(UiValue_Input *input) -{ - return Ui::focus() == input || Ui::focus() == &input->input || Ui::pushed() == input || - Ui::pushed() == &input->input; -} -/** - * @brief Updates a box label only when the text differs. - * - * Avoids redundant relayout and redraw when status text is refreshed - * repeatedly. The box takes a copy of the text. - * - * @param box Box whose label is updated; must not be NULL. - * @param text New label text; must not be NULL. - */ -void set_label(UiBox *box, const char *text) -{ - if (box->label() == nullptr || std::strcmp(box->label(), text) != 0) - box->copy_label(text); -} -/* Paint progress without dispatching input/reentering edit commands. */ -class BusyOperation { - UiWindow &window_; - public: - BusyOperation(UiWindow &window, UiBox *status, const char *message) : window_(window) { - set_label(status,message); window_.cursor(UI_CURSOR_WAIT); Ui::flush(); - } - ~BusyOperation() { window_.cursor(UI_CURSOR_DEFAULT); } -}; -} // namespace - -/** - * @brief Constructs the window, its widgets, and the initial project. - * - * Creates an empty project and undo history, opens the audio backend, and - * schedules the periodic status timer. Throws std::runtime_error when the - * initial project or history cannot be created. - * - * @param prompt_unsaved Whether open, new, and close actions ask before - * discarding unsaved changes. - */ -MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) - : UiDouble_Window(1280, 800, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved) -{ - xtc_ui::initialize(); - xtc_settings_defaults(&settings_); - char config_path[4096]; - if(configuration) configuration_=configuration; - else if(xtc_settings_path(config_path,sizeof(config_path))==XTC_OK) configuration_=config_path; - xtc_result config_result=configuration_.empty() ? XTC_ERR_NOT_FOUND : xtc_settings_load(&settings_,configuration_.c_str()); - - configuration_valid_=config_result==XTC_OK || config_result==XTC_ERR_NOT_FOUND; - xtc_project *initial = nullptr; - xtc_result result = xtc_project_create(&initial); - if (result != XTC_OK) - throw std::runtime_error(xtc_result_string(result)); - project_defaults(initial); - project_.reset(initial); - xtc_history *history = nullptr; - result = xtc_history_create(initial, &history); - if (result != XTC_OK) - throw std::runtime_error(xtc_result_string(result)); - history_.reset(history); - begin(); - auto *menu = new UiMenu_Bar(0, 0, 1280, 28); - menu_ = menu; - menu->add("File/New", UI_CTRL + 'n', file_callback, this); - menu->add("File/Open", UI_CTRL + 'o', file_callback, this); - menu->add("File/Save", UI_CTRL + 's', file_callback, this); - menu->add("File/Import MIDI...",0,[](UiWidget *,void *c){static_cast(c)->import_midi_dialog();},this); - menu->add("File/Export selected clip as MIDI...",0,[](UiWidget *,void *c){static_cast(c)->export_midi_dialog();},this); - menu->add("File/Export WAV",0,[](UiWidget *,void *context){static_cast(context)->export_audio();},this); - menu->add("File/Save As", UI_CTRL + UI_SHIFT + 's', file_callback, this); - menu->add("File/Import WAV", UI_CTRL + 'i', audio_file_callback, this); - menu->add("File/Relink selected audio", 0, audio_file_callback, this); - menu->add("Edit/Preferences",UI_CTRL+',',[](UiWidget *,void *c){static_cast(c)->show_preferences();},this); - menu->add("Edit/Undo", UI_CTRL + 'z', history_callback, this); - menu->add("Edit/Redo", UI_CTRL + UI_SHIFT + 'z', history_callback, this); - menu->add("Edit/Redo alternate", UI_CTRL + 'y', history_callback, this); - menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this); - menu->add("Audio/Panic", UI_F + 9, panic_callback, this); - menu->add("Edit/Mixer comparison/Capture A",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(0,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this); - menu->add("Edit/Mixer comparison/Recall A",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(0,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this); - menu->add("Edit/Mixer comparison/Capture B",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(1,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this); - menu->add("Edit/Mixer comparison/Recall B",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(1,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this); - menu->add("View/Previous marker",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->navigate_marker(-1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this); - menu->add("View/Next marker",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->navigate_marker(1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this); - menu->add("View/Loop current section",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->loop_section();if(r!=XTC_OK)w->report_error("Section loop unchanged",r);},this); - menu->add("File/Collect project media...",0,[](UiWidget *,void *c){auto *w=static_cast(c);const char *path=ui_input("New absolute project folder (must not exist)","");if(path){auto r=w->collect_project(path);if(r!=XTC_OK)w->report_error("Project collection failed",r);}},this); - menu->add("View/Commands",UI_CTRL+'p',[](UiWidget *,void *c){static_cast(c)->show_commands();},this); - menu->add("View/Fit song",UI_CTRL+'0',[](UiWidget *,void *c){static_cast(c)->fit_song();},this); - menu->add("View/Piano roll", UI_CTRL + '4', piano_callback, this); - menu->add("Audio/Record WAV", UI_SHIFT + UI_F + 5, audio_file_callback, this); - menu->add("Track/Load instrument plugin", 0, instrument_callback, this); - menu->add("Track/Use built-in instrument", 0, instrument_callback, this); - menu->add("Track/Live MIDI to selected instrument", 0, instrument_callback, this); - menu->add("Track/Live MIDI to built-in sine", 0, instrument_callback, this); - menu->add("Edit/Selected instrument controls",0,instrument_callback,this); - menu->add("Edit/Selected instrument automation",0,instrument_callback,this); - menu->add("File/Load sampler WAV",0,instrument_callback,this); - menu->add("File/Save instrument preset",0,instrument_callback,this); - menu->add("File/Load instrument preset",0,instrument_callback,this); - menu->add("Edit/Insert internal delay on wire", 0, plugin_callback, this); - menu->add("Edit/Insert internal reverb on wire", 0, plugin_callback, this); - menu->add("Edit/Insert stereo delay on wire",0,plugin_callback,this); - menu->add("Edit/Insert stereo reverb on wire",0,plugin_callback,this); - menu->add("File/Add stereo delay",0,plugin_callback,this); - menu->add("File/Add stereo reverb",0,plugin_callback,this); - menu->add("File/Add internal delay", 0, plugin_callback, this); - menu->add("File/Add internal reverb", 0, plugin_callback, this); - menu->add("File/Load plugin", 0, plugin_callback, this); - menu->add("Edit/Replace selected plugin", 0, plugin_callback, this); - menu->add("Edit/Insert plugin on selected wire", 0, plugin_callback, this); - menu->add("Edit/Reset selected plugin", 0, plugin_callback, this); - menu->add("Edit/Selected plugin automation", 0, plugin_callback, this); - menu->add("Edit/Selected plugin control", 0, plugin_callback, this); - menu->add("File/Quit", UI_CTRL + 'q', close_callback, this); - - menu->add("Audio/Start engine", UI_F + 5, play_callback, this); - menu->add("Audio/Stop engine", UI_F + 8, stop_callback, this); - menu->add("Audio/Reconnect", UI_F + 6, reconnect_callback, this); - - menu->add("Audio/Pause", UI_F + 7, [](UiWidget *, void *c) { - (void)static_cast(c)->pause_playback(); - }, this); - menu->add("Audio/Return to start", UI_Home, [](UiWidget *, void *c) { - (void)static_cast(c)->return_to_start(); - }, this); - - menu->add("View/Detach transport",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_transport(!w->transport_detached_); - },this,UI_MENU_TOGGLE); - menu->add("View/Detach mixer",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_mixer(!w->mixer_detached_); - },this,UI_MENU_TOGGLE); - menu->add("View/Dock all panels",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_transport(false); w->detach_mixer(false);w->detach_editor(0,false);w->detach_editor(1,false);w->detach_piano(false); - },this); - menu->add("View/Detach sequencer",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_editor(0,!w->editors_[0].detached);},this,UI_MENU_TOGGLE); - menu->add("View/Detach modular graph",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_editor(1,!w->editors_[1].detached);},this,UI_MENU_TOGGLE); - menu->add("View/Float piano roll",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_piano(!w->piano_detached_);},this,UI_MENU_TOGGLE|UI_MENU_VALUE); - menu->add("View/Reset workspace layout",0,[](UiWidget *,void *c){static_cast(c)->reset_layout();},this); - transport_group_=new UiGroup(0,28,1280,90); - transport_group_->begin(); - play_ = new UiButton(12, 40, 80, 30, "Play"); - play_->callback(play_callback, this); - play_->tooltip("Start the audio engine (F5 or Space). Play audio clips and the modular graph."); - stop_ = new UiButton(100, 40, 80, 30, "Stop"); - stop_->callback(stop_callback, this); - stop_->tooltip("Stop playback at its current position; finalize recording (F8 or Space)."); - auto *reconnect = new UiButton(188, 40, 110, 30, "Reconnect"); - reconnect->callback(reconnect_callback, this); - reconnect->tooltip("Close and reopen JACK, leaving the engine stopped (F6)."); - record_ = new UiButton(306, 40, 90, 30, "Record"); - record_->callback(audio_file_callback, this); - record_->tooltip("Record JACK input to WAV; choose path, then Stop to finalize and add the take."); - auto *panic = new UiButton(404, 40, 70, 30, "Panic"); - panic->callback(panic_callback, this); - panic->tooltip("Silence held instrument notes (F9)."); - pause_ = new UiButton(482, 40, 70, 30, "Pause"); - pause_->callback([](UiWidget *, void *c) { - (void)static_cast(c)->pause_playback(); - }, this); - pause_->tooltip("Pause at the current sample position (F7). Play resumes; recording uses Stop."); - home_ = new UiButton(560, 40, 100, 30, "To start"); - home_->callback([](UiWidget *, void *c) { - (void)static_cast(c)->return_to_start(); - }, this); - home_->tooltip("Stop and return to the project beginning (Home). Unavailable while recording."); - indicator_ = new UiBox(1000, 40, 268, 30, "Audio: disconnected"); - indicator_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - indicator_->labelfont(UI_BOLD); - indicator_->labelsize(12); - transport_position_ = new UiBox(680,40,190,30,"001 : 1 : 000"); - transport_position_->tooltip("Musical position: bar : beat : tick (4/4, 960 ticks per beat)"); - tempo_display_ = new UiValue_Input(880, 40, 78, 30, "BPM"); - tempo_display_->align(UI_ALIGN_RIGHT); - tempo_display_->bounds(20, 300); - tempo_display_->step(.1); - tempo_display_->when(UI_WHEN_RELEASE | UI_WHEN_ENTER_KEY); - tempo_display_->tooltip("Project tempo in BPM. Enter or release to apply; undoable. Stops playback."); - tempo_display_->callback( - [](UiWidget *, void *c) { - auto *w = static_cast(c); - double bpm = w->tempo_display_->value(); - xtc_result r = (!std::isfinite(bpm) || bpm < 20 || bpm > 300) - ? XTC_ERR_INVALID_ARGUMENT - : w->set_tempo(static_cast(std::lround(bpm * 1000))); - if (r != XTC_OK) - w->report_error("Tempo unchanged", r); - w->tempo_display_->value(xtc_project_tempo(w->project_.get()) / 1000.0); - w->refresh_status(); - }, - this); - auto *hint = new UiBox(12, 82, 160, 28, "xtc / SESSION"); - hint->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - session_info_ = new UiBox(180, 82, 280, 28); - session_info_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - auto *commands = new UiButton(480, 82, 120, 28, "Commands"); - commands->tooltip("Search actions (Ctrl+P)"); - commands->callback([](UiWidget *, void *c) { static_cast(c)->show_commands(); }, this); - follow_ = new UiCheck_Button(620, 82, 154, 28, "Follow playhead"); - follow_->tooltip("Keep playback visible in the arrangement; pauses while dragging."); - follow_->callback( - [](UiWidget *widget, void *c) { - static_cast(c)->settings_.follow_playhead = static_cast(widget)->value(); - }, - this); - auto *fit = new UiButton(790, 82, 100, 28, "Fit song"); - fit->tooltip("Choose the largest supported zoom that fits the song (Ctrl+0)"); - fit->callback([](UiWidget *, void *c) { static_cast(c)->fit_song(); }, this); - auto *tap=new UiButton(905,82,80,28,"Tap"); - tap->tooltip("Tap tempo: averages up to four intervals, 20–300 BPM; pauses over three seconds restart."); - tap->callback([](UiWidget *,void *c){auto *w=static_cast(c);double now=std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count();auto r=w->tap_tempo(now);if(r!=XTC_OK && r!=XTC_ERR_NOT_FOUND && r!=XTC_ERR_INVALID_ARGUMENT)w->report_error("Tap tempo unchanged",r);},this); - loop_button_ = new UiCheck_Button(1000, 82, 100, 28, "Loop"); - loop_button_->callback([](UiWidget *widget,void *c){auto *w=static_cast(c); - xtc_result r=w->set_loop(static_cast(w->settings_.loop_start),static_cast(w->settings_.loop_end),static_cast(widget)->value()!=0); - if(r!=XTC_OK) w->report_error("Loop unchanged",r); - w->refresh_status();},this); - auto *range=new UiButton(1110,82,150,28,"Loop range..."); - range->tooltip("Shift-drag the sequencer ruler to select a loop, or enter start/end ticks here."); - range->callback([](UiWidget *,void *c){auto *w=static_cast(c);char initial[100]; - std::snprintf(initial,sizeof(initial),"%lld %lld",static_cast(w->settings_.loop_start),static_cast(w->settings_.loop_end)); - const char *text=ui_input("Loop start and end ticks (end excluded)",initial);if(!text)return; - unsigned long long start,end;char extra;xtc_result r=XTC_ERR_INVALID_ARGUMENT; - if(std::sscanf(text,"%llu %llu %c",&start,&end,&extra)==2)r=w->set_loop(start,end,w->settings_.loop_enabled!=0); - if(r!=XTC_OK)w->report_error("Loop unchanged",r);},this); - - transport_group_->end(); - for(int i=0;ichildren();++i) { - auto *item=transport_group_->child(i); - transport_items_.push_back({item,item->x(),item->y()-28,item->w(),item->h()}); - } - - tabs_ = new UiTabs(216, 126, 1056, 578); - tabs_->begin(); - auto *sequencer = new UiGroup(216, 158, 1056, 546, "Sequencer"); - sequencer->begin(); - editors_[0].owner=this;editors_[0].index=0;editors_[0].page=sequencer; - editors_[0].content=new UiGroup(216,158,1056,546); - editors_[0].content->begin(); - auto *add_track = new UiMenu_Button(228, 168, 100, 26, "Create"); - auto *add_region = new UiButton(336, 168, 110, 26, "Add region"); - auto *delete_region = new UiButton(454, 168, 130, 26, "Delete region/track"); - auto *zoom_in = new UiButton(592, 168, 80, 26, "Zoom +"); - auto *zoom_out = new UiButton(680, 168, 80, 26, "Zoom -"); - auto *add_midi = new UiButton(768, 168, 110, 26, "Add MIDI clip"); - auto *piano = new UiButton(886, 168, 100, 26, "Piano roll"); - piano->callback(piano_callback, this); - auto *timeline_hint = - new UiBox(228, 666, 1032, 28, - "Drag clips to move, edges to trim. Click the ruler while stopped to seek; Home returns to start."); - timeline_hint->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - timeline_hint->labelsize(12); - auto *timeline_scroll = new UiScroll(228, 206, 1032, 452); - timeline_scroll_=timeline_scroll; - sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this); - timeline_scroll->end(); - sequencer_->before_edit = before_arrangement_edit; - sequencer_->loop_select=[](void *c,uint64_t start,uint64_t end){auto *w=static_cast(c);return w->set_loop(start,end,w->settings_.loop_enabled!=0);}; - sequencer_->open_clip = [](void *c) {auto *w=static_cast(c);xtc_clip_info clip{}; - if(xtc_project_find_clip(w->project_.get(),w->sequencer_->selected_clip(),&clip)==XTC_OK && !clip.midi){auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);}else piano_callback(nullptr,c);}; - sequencer_->instrument_select = [](void *c, uint64_t track, const xtc_plugin_spec *spec, bool browse) { - auto *w = static_cast(c); - w->sequencer_->select(track); - if (browse) { instrument_callback(nullptr,c); return; } - xtc_result r = w->assign_instrument(track,spec ? spec->path : nullptr,spec ? spec->index : 0); - if (r != XTC_OK) w->report_error("Track instrument unchanged",r); - }; - // Keep this in the editor group rather than the scrollable timeline. - auto *parent = UiGroup::current(); UiGroup::current(editors_[0].content); - snap_ = new UiChoice(1040,168,210,26,"Snap"); - for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = snap_->add("Snap"); snap_->replace(item,xtc_ui::snap_labels[i]); } - snap_->value(xtc_ui::snap_index(sequencer_->grid())); - snap_->callback([](UiWidget *widget, void *c) { - auto *choice = static_cast(widget); int value = choice->value(); - if (value >= 0 && static_cast(value) < xtc_ui::snap_count) - (void)static_cast(c)->sequencer_->grid(xtc_ui::snap_ticks[value]); - },this); - UiGroup::current(parent); - menu->add("Edit/Nudge selection left",0,[](UiWidget *,void *c){static_cast(c)->nudge_selection(-1);},sequencer_); - menu->add("Edit/Nudge selection right",0,[](UiWidget *,void *c){static_cast(c)->nudge_selection(1);},sequencer_); - menu->add("View/Sample editor",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);},this); - menu->add("Edit/Audio tools/Normalize source peak...",0,[](UiWidget *,void *c){static_cast(c)->normalize_dialog();},sequencer_); - menu->add("Edit/Audio tools/Set boundary fades...",0,[](UiWidget *,void *c){static_cast(c)->fades_dialog();},sequencer_); - menu->add("Edit/Audio tools/Remove boundary fades",0,[](UiWidget *,void *c){static_cast(c)->transform_selection(XTC_CLIPS_FADE,0);},sequencer_); - menu->add("Edit/MIDI tools/Generate arpeggio...",0,[](UiWidget *,void *c){static_cast(c)->arpeggio_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Build chords...",0,[](UiWidget *,void *c){static_cast(c)->chord_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Strum chords...",0,[](UiWidget *,void *c){static_cast(c)->strum_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Humanize...",0,[](UiWidget *,void *c){static_cast(c)->humanize_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Shape velocities...",0,[](UiWidget *,void *c){static_cast(c)->velocity_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Correct to scale...",0,[](UiWidget *,void *c){static_cast(c)->scale_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Note gate...",0,[](UiWidget *,void *c){static_cast(c)->gate_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Legato",0,[](UiWidget *,void *c){xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_LEGATO;static_cast(c)->transform_midi(edit);},sequencer_); - menu->add("Edit/MIDI tools/Swing quantize...",0,[](UiWidget *,void *c){static_cast(c)->swing_dialog();},sequencer_); - menu->add("Edit/Align clip starts to cursor",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_ALIGN,static_cast(s->position()));},sequencer_); - menu->add("Edit/Slip sources earlier",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,-static_cast(s->grid()));},sequencer_); - menu->add("Edit/Slip sources later",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,static_cast(s->grid()));},sequencer_); - menu->add("Edit/Repeat selected phrase...",0,[](UiWidget *,void *c){static_cast(c)->repeat_dialog();},sequencer_); - menu->add("Edit/Copy clips",0,[](UiWidget *,void *c){static_cast(c)->copy_selection();},sequencer_); - menu->add("Edit/Paste clips at cursor",0,[](UiWidget *,void *c){static_cast(c)->paste_selection();},sequencer_); - menu->add("Edit/Consolidate MIDI clips",0,[](UiWidget *,void *c){static_cast(c)->merge_midi();},sequencer_); - menu->add("Edit/Duplicate complete track...",0,[](UiWidget *,void *c){static_cast(c)->duplicate_track_dialog();},sequencer_); - menu->add("Edit/Time/Insert gap...",0,[](UiWidget *,void *c){static_cast(c)->ripple_dialog(false);},sequencer_); - menu->add("Edit/Time/Remove range...",0,[](UiWidget *,void *c){static_cast(c)->ripple_dialog(true);},sequencer_); - menu->add("Edit/Markers/Set at cursor...",0,[](UiWidget *,void *c){static_cast(c)->marker_dialog();},sequencer_); - menu->add("Edit/Markers/Remove at cursor",0,[](UiWidget *,void *c){static_cast(c)->marker(nullptr);},sequencer_); - menu->add("Edit/Select all clips",0,[](UiWidget *,void *c){static_cast(c)->select_all_clips();},sequencer_); - menu->add("Edit/Duplicate clip",UI_CTRL+'d',[](UiWidget *, void *c) { (void)static_cast(c)->duplicate_selection(); },sequencer_); - menu->add("Edit/Split clip at cursor", 0, [](UiWidget *, void *c) { (void)static_cast(c)->split_selection(); }, sequencer_); - menu->add("Edit/Audio tools/Crossfade two selected clips",0,[](UiWidget *,void *c){static_cast(c)->crossfade_selection();},sequencer_); - menu->add("Edit/Audio clip gain and fades", 0, [](UiWidget *, void *c) { static_cast(c)->audio_properties_dialog(); }, sequencer_); - menu->add("Edit/Rename clip or track",UI_F+2,[](UiWidget *, void *c) { - const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast(c)->rename_selection(name); - },sequencer_); - for (const char *label : {"View/Snap/1 tick","View/Snap/120 ticks","View/Snap/160 ticks","View/Snap/240 ticks","View/Snap/320 ticks","View/Snap/480 ticks","View/Snap/960 ticks","View/Snap/1920 ticks","View/Snap/3840 ticks"}) - menu->add(label,0,[](UiWidget *widget,void *c) { - auto *m = static_cast(widget); - (void)static_cast(c)->grid(std::strtoull(m->mvalue()->label(),nullptr,10)); - },sequencer_); - sequencer_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - add_midi->callback( - [](UiWidget *, void *c) { - static_cast(c)->add_midi_clip(); - }, - sequencer_); - add_region->callback( - [](UiWidget *, void *c) { - static_cast(c)->add_region(); - }, - sequencer_); - delete_region->callback( - [](UiWidget *, void *c) { - static_cast(c)->delete_selection(); - }, - sequencer_); - zoom_in->callback( - [](UiWidget *, void *c) { - static_cast(c)->zoom(1); - }, - sequencer_); - zoom_out->callback( - [](UiWidget *, void *c) { - static_cast(c)->zoom(-1); - }, - sequencer_); - editors_[0].content->resizable(timeline_scroll);editors_[0].content->end(); - sequencer->resizable(editors_[0].content); - sequencer->end(); - auto *mixer = new UiGroup(216, 158, 1056, 546, "Mixer"); - mixer_page_=mixer; - mixer->begin(); - mixer_ = new MixerWidget(228, 168, 1032, 522, project_.get(), before_arrangement_edit, this); - mixer_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - mixer_placeholder_=new UiGroup(228,210,1032,120); - auto *detached_hint=new UiBox(240,220,600,30,"The mixer is open in a separate window."); - detached_hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *show_mixer=new UiButton(240,260,170,30,"Show mixer window"); - show_mixer->callback([](UiWidget *,void *c){static_cast(c)->mixer_window_->show();},this); - auto *dock_mixer=new UiButton(424,260,140,30,"Dock mixer"); - dock_mixer->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - mixer_placeholder_->end(); mixer_placeholder_->hide(); - mixer->resizable(mixer_); - mixer->end(); - auto *graph = new UiGroup(216, 158, 1056, 546, "Modular Graph"); - graph->begin(); - editors_[1].owner=this;editors_[1].index=1;editors_[1].page=graph; - editors_[1].content=new UiGroup(216,158,1056,546);editors_[1].content->begin(); - graph_state_ = new UiBox(228, 200, 956, 24, "Graph: offline"); - graph_state_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - graph_state_->labelsize(12); - auto *feedback = - new UiBox(228, 666, 1032, 28, - "Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan."); - feedback->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - feedback->labelsize(12); - auto *scroll = new UiScroll(228, 230, 1032, 430); - graph_widget_ = new GraphWidget(228, 230, 2000, 4000, xtc_project_graph(project_.get()), edit_graph, this, feedback); - scroll->end(); - menu->add("View/Arrange graph by signal flow", 0, [](UiWidget *, void *c) { (void)static_cast(c)->auto_layout(); }, graph_widget_); - menu->add("Edit/Insert unity gain in selected wire", 0, [](UiWidget *, void *c) { (void)static_cast(c)->insert_gain(); }, graph_widget_); - auto *oscillator = new UiButton(228, 168, 130, 26, "Add oscillator"); - oscillator->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OSCILLATOR); - }, - graph_widget_); - auto *gain = new UiButton(366, 168, 110, 26, "Add gain"); - gain->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_GAIN); - }, - graph_widget_); - auto *meter = new UiButton(484, 168, 110, 26, "Add meter"); - meter->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_METER); - }, - graph_widget_); - auto *output = new UiButton(602, 168, 110, 26, "Add output"); - output->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OUTPUT); - }, - graph_widget_); - auto *audio = new UiButton(858, 168, 130, 26, "Add audio source"); - audio->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_AUDIO); - }, - graph_widget_); - auto *instrument = new UiButton(996, 168, 130, 26, "Add instrument"); - instrument->callback( - [](UiWidget *, void *c) { - (void)static_cast(c)->add_node(XTC_NODE_INSTRUMENT); - }, - graph_widget_); - auto *remove = new UiButton(720, 168, 130, 26, "Delete selection"); - remove->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->delete_selected(); - }, - graph_widget_); - editors_[1].content->resizable(scroll);editors_[1].content->end(); - graph->resizable(editors_[1].content);graph->end(); - piano_page_=new UiGroup(216,158,1056,546,"Piano roll"); - piano_placeholder_=new UiGroup(228,210,1000,120); - auto *piano_hint=new UiBox(240,220,900,30,"Select a MIDI clip, then open its piano roll. Float or dock it from View.");piano_hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *open_piano=new UiButton(240,260,190,30,"Open selected MIDI clip");open_piano->callback(piano_callback,this); - piano_placeholder_->end();piano_page_->end(); - for(auto &panel:editors_) { - panel.page->begin();panel.placeholder=new UiGroup(228,210,1000,120); - auto *hint=new UiBox(240,220,900,30,"This editor is open in a separate window.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *show=new UiButton(240,260,170,30,"Show editor window"); - show->callback([](UiWidget *,void *c){auto *p=static_cast(c);if(p->window)p->window->show();},&panel); - auto *dock=new UiButton(424,260,140,30,"Dock editor"); - dock->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - panel.placeholder->end();panel.placeholder->hide();panel.page->end(); - } - track_inserts_=new TrackInsertsPanel(216,158,1056,546,this); - tabs_->end(); - tabs_->value(sequencer); - menu->add("View/Track inserts",0,view_callback,track_inserts_); - menu->add("Track/Show selected track inserts",UI_CTRL+'5',[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->open_track_inserts();if(r!=XTC_OK)w->report_error("Select an audio or MIDI track to open inserts",r);},this); - menu->add("View/Sequencer", UI_CTRL + '1', view_callback, sequencer); - menu->add("View/Mixer", UI_CTRL + '2', view_callback, mixer); - menu->add("View/Modular Graph", UI_CTRL + '3', view_callback, graph); - menu->add("Help/About", 0, about_callback); - - auto *sidebar = new UiGroup(12,126,192,578); - sidebar_=sidebar; - sidebar->box(UI_BORDER_BOX); - auto midi_create = [](UiWidget *, void *c) { auto *w=static_cast(c); - xtc_result r=w->create_track(true); if(r!=XTC_OK)w->report_error("Create MIDI track",r); }; - auto audio_create = [](UiWidget *, void *c) { auto *w=static_cast(c); - xtc_result r=w->create_track(false); if(r!=XTC_OK)w->report_error("Create audio track",r); }; - add_track->add("Empty track",0,[](UiWidget *,void *c){static_cast(c)->add_track();},sequencer_); - add_track->add("New MIDI track",0,midi_create,this); - add_track->add("New audio track",0,audio_create,this); - add_track->add("Import MIDI",0,[](UiWidget *,void *c){static_cast(c)->import_midi_dialog();},this); - add_track->add("Import WAV",0,audio_file_callback,this); - add_track->add("Load instrument plugin",0,instrument_callback,this); - add_track->add("Edit MIDI notes",0,piano_callback,this); - menu->add("Track/New MIDI track",UI_CTRL+UI_SHIFT+'m',midi_create,this); - menu->add("Track/New audio track",UI_CTRL+UI_SHIFT+'a',audio_create,this); - menu->add("View/Track inspector",0,[](UiWidget *,void *c){ - auto *w=static_cast(c);w->show_track_inspector(!w->sidebar_->visible()); - },this,UI_MENU_TOGGLE); - auto *selection_title = new UiBox(24,140,168,24,"TRACK INSPECTOR"); - selection_title->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - selection_info_ = new UiBox(24,174,168,82,"Select a track to inspect it."); - selection_info_->align(UI_ALIGN_TOP_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - inspector_gain_ = new UiValue_Input(78, 264, 114, 26, "Gain"); - inspector_gain_->bounds(0, 4); - inspector_gain_->step(.01); - inspector_pan_ = new UiValue_Input(78, 298, 114, 26, "Pan"); - inspector_pan_->bounds(-1, 1); - inspector_pan_->step(.01); - inspector_mute_ = new UiCheck_Button(24, 332, 78, 24, "Mute"); - inspector_solo_ = new UiCheck_Button(108, 332, 84, 24, "Solo"); - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) { - control->when(UI_WHEN_RELEASE); - control->callback(inspector_callback, this); - } - inspector_gain_->tooltip( - "Selected track linear gain: 0 silence, 1 unity, 4 maximum. Automation overrides static gain."); - inspector_pan_->tooltip("Selected track pan: -1 left, 0 center, +1 right."); - auto *tip = new UiBox(24, 368, 168, 52, "Ctrl+P Commands\nCtrl+1/2/3 Views\nCtrl+0 Fit song"); - tip->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - sidebar->end(); - status_ = new UiBox(12,714,1256,26,"JACK: not connected"); - status_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - error_ = new UiBox(12,744,1256,44,""); - error_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - xtc_ui::style(this); - color(xtc_ui::background()); - selection_title->labelcolor(xtc_ui::muted()); - tip->labelcolor(xtc_ui::muted()); hint->labelcolor(xtc_ui::muted()); - transport_position_->box(xtc_ui::flat); transport_position_->color(xtc_ui::background()); - transport_position_->labelfont(UI_COURIER_BOLD); transport_position_->labelsize(18); - tempo_display_->labelcolor(xtc_ui::muted()); - play_->labelcolor(xtc_ui::accent()); record_->labelcolor(ui_rgb_color(245,132,142)); - error_->labelcolor(ui_rgb_color(255,166,139)); - end(); - resizable(tabs_); - size_range(1220, 720); - callback(close_callback, this); - apply_ui_preferences(); - show_track_inspector(settings_.inspector_visible!=0); - open_backend(); - if(config_result!=XTC_OK && config_result!=XTC_ERR_NOT_FOUND) report_error("Preferences file invalid; using defaults",config_result); - Ui::add_timeout(static_cast(settings_.refresh_ms)/1000,timer_callback,this); -} - -void MainWindow::detach_editor(unsigned int index,bool detached) -{ - if(index>=2)return; - auto &panel=editors_[index];if(!panel.content || panel.detached==detached)return; - auto *scroll=index==0 ? timeline_scroll_ : dynamic_cast(graph_widget_->parent()); - int scroll_x=scroll ? scroll->xposition() : 0,scroll_y=scroll ? scroll->yposition() : 0; - if(detached && !panel.window) { - auto *current=UiGroup::current();UiGroup::current(nullptr); - panel.window.reset(new PanelWindow(1100,650,index==0 ? "xtc — Sequencer (close to dock)" : "xtc — Modular Graph (close to dock)",this)); - auto *dock=new UiButton(12,6,150,28,"Dock editor");dock->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - panel.window->end();panel.window->size_range(index==0 ? 1100 : 1000,420);xtc_ui::style(panel.window.get()); - panel.window->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - restore_panel(panel.window.get(),static_cast(index==0 ? settings_.sequencer_x : settings_.graph_x),static_cast(index==0 ? settings_.sequencer_y : settings_.graph_y),static_cast(index==0 ? settings_.sequencer_width : settings_.graph_width),static_cast(index==0 ? settings_.sequencer_height : settings_.graph_height),index==0 ? 1100 : 1000,420); - UiGroup::current(current); - } - panel.content->parent()->remove(panel.content);panel.detached=detached; - if(detached) { - panel.page->resizable(nullptr);panel.window->add(panel.content); - panel.content->resize(12,44,panel.window->w()-24,panel.window->h()-56);panel.window->resizable(panel.content);panel.placeholder->show(); - } else { - panel.window->resizable(nullptr);panel.page->add(panel.content); - panel.content->resize(panel.page->x(),panel.page->y(),panel.page->w(),panel.page->h()); - panel.page->resizable(panel.content);panel.placeholder->hide();panel.window->hide(); - } - if(scroll)scroll->scroll_to(scroll_x,scroll_y); - panel.content->show();panel.page->init_sizes(); - if(index==0)settings_.sequencer_detached=detached;else settings_.graph_detached=detached; - auto *item=const_cast(menu_->find_item(index==0 ? "View/Detach sequencer" : "View/Detach modular graph"));if(detached)item->set();else item->clear(); - if(detached)panel.window->show(); - redraw(); -} -void MainWindow::detach_piano(bool detached) -{ - settings_.piano_detached=detached; - auto *item=const_cast(menu_->find_item("View/Float piano roll"));if(detached)item->set();else item->clear(); - if(!piano_content_) {piano_detached_=detached;return;} - if(piano_content_->window()==piano_window_.get() && detached) {piano_detached_=true;return;} - if(piano_content_->parent()==piano_page_ && !detached) {piano_detached_=false;return;} - auto *scroll=dynamic_cast(piano_->parent());int sx=scroll->xposition(),sy=scroll->yposition(); - piano_content_->parent()->remove(piano_content_);piano_detached_=detached; - if(detached) { - piano_page_->resizable(nullptr);piano_placeholder_->show();piano_window_->add(piano_content_); - piano_content_->resize(0,0,piano_window_->w(),piano_window_->h());piano_window_->resizable(piano_content_);piano_window_->show(); - } else { - piano_window_->resizable(nullptr);piano_placeholder_->hide();piano_page_->add(piano_content_); - piano_content_->resize(piano_page_->x(),piano_page_->y(),piano_page_->w(),piano_page_->h()); - piano_page_->resizable(piano_content_);piano_window_->hide();tabs_->value(piano_page_); - } - scroll->scroll_to(sx,sy);piano_content_->show();piano_page_->init_sizes(); - redraw(); -} -void MainWindow::reset_layout() -{ - detach_transport(false);detach_mixer(false);detach_editor(0,false);detach_editor(1,false);detach_piano(false); - settings_.transport_x=settings_.transport_y=settings_.sequencer_x=settings_.sequencer_y=settings_.graph_x=settings_.graph_y=80; - settings_.mixer_x=settings_.mixer_y=settings_.piano_x=settings_.piano_y=100; - settings_.transport_width=800;settings_.transport_height=300;settings_.mixer_width=1056;settings_.mixer_height=600; - settings_.sequencer_width=settings_.graph_width=1100;settings_.sequencer_height=settings_.graph_height=650; - settings_.piano_width=900;settings_.piano_height=620;settings_.track_header_width=180; - sequencer_->header_width(180);tabs_->value(editors_[0].page);apply_ui_preferences(); -} -void MainWindow::show_track_inspector(bool visible) -{ - const int left=visible ? 216 : 12; - if(visible)sidebar_->show();else sidebar_->hide(); - tabs_->resize(left,tabs_->y(),w()-left-8,tabs_->h()); - auto *menu=dynamic_cast(child(0)); - if(menu) { auto *item=const_cast(menu->find_item("View/Track inspector")); - if(item) { if(visible)item->set();else item->clear(); } } - init_sizes();redraw(); -} -void MainWindow::resize(int x,int y,int width,int height) -{ - UiDouble_Window::resize(x,y,width,height); - if(transport_group_)layout_transport(); -} -void MainWindow::layout_transport() -{ - if(layout_transport_busy_ || !transport_group_ || transport_items_.empty())return; - layout_transport_busy_=true; - int width=transport_detached_ && transport_window_ ? transport_window_->w() : w(); - int origin=transport_detached_ ? 32 : 28; - bool compact=transport_detached_ && width<1220; - int left=12,top=12; - transport_group_->resizable(nullptr); - for(const auto &item:transport_items_) { - int x,y; - if(compact) { - if(left+item.w+(item.widget==tempo_display_ ? 36 : 0)>width-12){left=12;top+=40;} - x=left;y=origin+top;left+=item.w+(item.widget==tempo_display_ ? 36 : 0)+8; - } else {x=item.x*width/1280;y=origin+item.y;} - item.widget->resize(x,y,item.w,item.h); - } - int height=compact ? top+48 : 90; - transport_group_->resize(0,origin,width,height); - // Group resize can transform children: restore exact layout after it. - left=12;top=12; - for(const auto &item:transport_items_) { - int x,y;if(compact){if(left+item.w+(item.widget==tempo_display_ ? 36 : 0)>width-12){left=12;top+=40;}x=left;y=origin+top;left+=item.w+(item.widget==tempo_display_ ? 36 : 0)+8;} - else{x=item.x*width/1280;y=origin+item.y;} - item.widget->resize(x,y,item.w,item.h); - } - transport_group_->init_sizes(); - if(transport_detached_ && transport_window_ && transport_window_->h()resize(transport_window_->x(),transport_window_->y(),width,origin+height+8); - layout_transport_busy_=false; -} -void MainWindow::detach_transport(bool detached) -{ - if (detached==transport_detached_ || !transport_group_) return; - if (detached && !transport_window_) { - auto *current=UiGroup::current(); UiGroup::current(nullptr); - transport_window_.reset(new PanelWindow(static_cast(settings_.transport_width),static_cast(settings_.transport_height),"xtc — Transport (close to dock)",this)); - auto *dock=new UiButton(12,4,140,24,"Dock transport"); - dock->callback([](UiWidget *,void *c){static_cast(c)->detach_transport(false);},this); - xtc_ui::style(transport_window_.get()); - transport_window_->end(); transport_window_->size_range(400,130); - transport_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_transport(false);},this); - restore_panel(transport_window_.get(),static_cast(settings_.transport_x),static_cast(settings_.transport_y),static_cast(settings_.transport_width),static_cast(settings_.transport_height),400,130); - UiGroup::current(current); - } - transport_detached_=detached;settings_.transport_detached=detached; - transport_group_->parent()->remove(transport_group_); - if (detached) { - transport_window_->add(transport_group_); - transport_group_->resize(0,32,transport_window_->w(),90); - transport_window_->resizable(transport_group_); - } else { - transport_window_->resizable(nullptr); - add(transport_group_); transport_group_->resize(0,28,w(),90); - transport_window_->hide(); - } - int delta=detached ? -90 : 90; - tabs_->resize(tabs_->x(),tabs_->y()+delta,tabs_->w(),tabs_->h()-delta); - sidebar_->resize(sidebar_->x(),sidebar_->y()+delta,sidebar_->w(),sidebar_->h()-delta); - init_sizes(); - auto *item=const_cast(menu_->find_item("View/Detach transport")); - if (detached) item->set(); else item->clear(); - layout_transport(); - if (detached) transport_window_->show(); - redraw(); -} - -void MainWindow::detach_mixer(bool detached) -{ - if (detached==mixer_detached_ || !mixer_) return; - if (detached && !mixer_window_) { - auto *current=UiGroup::current(); UiGroup::current(nullptr); - mixer_window_.reset(new PanelWindow(1056,600,"xtc — Mixer (close to dock)",this)); - auto *dock=new UiButton(12,6,140,28,"Dock mixer"); - dock->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - xtc_ui::style(mixer_window_.get()); - mixer_window_->end(); mixer_window_->size_range(600,420); - mixer_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - restore_panel(mixer_window_.get(),static_cast(settings_.mixer_x),static_cast(settings_.mixer_y),static_cast(settings_.mixer_width),static_cast(settings_.mixer_height),600,420); - UiGroup::current(current); - } - mixer_detached_=detached;settings_.mixer_detached=detached; - mixer_->parent()->remove(mixer_); - if (detached) { - mixer_page_->resizable(nullptr); - mixer_window_->add(mixer_); mixer_->resize(12,44,mixer_window_->w()-24,mixer_window_->h()-56); - mixer_window_->resizable(mixer_); mixer_placeholder_->show(); - } else { - mixer_window_->resizable(nullptr); - mixer_page_->add(mixer_); - mixer_->resize(mixer_page_->x()+12,mixer_page_->y()+10,mixer_page_->w()-24,mixer_page_->h()-24); - mixer_page_->resizable(mixer_); mixer_placeholder_->hide(); mixer_window_->hide(); - } - mixer_->show(); mixer_page_->init_sizes(); - auto *item=const_cast(menu_->find_item("View/Detach mixer")); - if (detached) item->set(); else item->clear(); - if (detached) mixer_window_->show(); - redraw(); -} - -void MainWindow::draw() -{ - // JUCE may reload the platform scheme when a window first becomes visible. - // Keep built-in arrows, popups and choice fields flat as well as our boxes. - xtc_ui::configure_boxes(); - UiDouble_Window::draw(); -} - -/** - * @brief Removes the status timer and releases backend and recording state. - * - * A backend that fails to close is intentionally retained with its recorder - * until process exit so active callbacks never observe freed state. - */ -MainWindow::~MainWindow() -{ - Ui::remove_timeout(timer_callback, this); - // Return ownership before member windows and the main widget tree destruct. - detach_transport(false); - detach_mixer(false);detach_editor(0,false);detach_editor(1,false);detach_piano(false); - // Normal window/menu close already synchronized callbacks. This fallback - // covers exceptional event-loop exit. A failed close must retain its C - // callback context until process exit, rather than free active audio state. - if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK) - std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n", - stderr); - else if (recorder_) { - xtc_result result = xtc_recorder_finish(recorder_); - std::fprintf(stderr, "Recording finalized during shutdown (%s): %s\n", xtc_result_string(result), - xtc_recorder_recovery_path(recorder_)); - xtc_recorder_destroy(recorder_); - } -} - -/** - * @brief Shows an operation failure and a context-sensitive hint. - * - * @param operation Human-readable operation name; names containing - * "Project" select a file and permission hint. - * @param result Failing result code whose name is included in the message. - */ -void MainWindow::report_error(const char *operation, xtc_result result) -{ - char text[384]; - const char *hint = std::strstr(operation, "Project") != nullptr - ? "Check the project path, file format and write permissions." - : result == XTC_ERR_OUT_OF_MEMORY ? "Insufficient memory; the command was not accepted." - : result == XTC_ERR_CAPACITY ? "A project capacity limit was reached." - : result == XTC_ERR_NOT_FOUND || result == XTC_ERR_INVALID_ARGUMENT ? "Check the selected target and entered values." - : result == XTC_ERR_UNSUPPORTED ? "Check optional build support and the requested format or plugin layout." - : "Check the operation details; for audio failures use Reconnect to retry."; - std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint); - set_label(error_, text); -} - -/** - * @brief Edit handler passed to the modular graph widget. - * - * Applies the edit to a candidate graph, clones the project around it, and - * submits the new graph to the engine when connected. The live project is - * replaced only after every step succeeds. - * - * @param context MainWindow * passed as the graph widget owner context. - * @param edit Graph edit to apply. - * @param out_added_id Optional output receiving the new node ID when the edit - * adds a node; untouched otherwise or when nullptr. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from the graph, project, or backend operation. - */ -xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id) -{ - auto *window = static_cast(context); - if (window->recorder_) - return XTC_ERR_INVALID_STATE; - xtc_graph *candidate = nullptr; - size_t added_id = SIZE_MAX; - xtc_result result = xtc_graph_apply_edit(xtc_project_graph(window->project_.get()), &edit, &candidate, &added_id); - if (result != XTC_OK) - return result; - xtc_project *replacement = nullptr; - result = xtc_project_clone(window->project_.get(), &replacement); - if (result == XTC_OK) - result = xtc_project_set_graph(replacement, candidate); - xtc_graph_destroy(candidate); - if (result == XTC_OK) result = xtc_history_reserve(window->history_.get()); - if (result == XTC_OK && window->ready_) - result = xtc_backend_submit(window->backend_, xtc_project_graph(replacement)); - if (result != XTC_OK) { - xtc_project_destroy(replacement); - return result; - } - window->project_.reset(replacement); - window->graph_widget_->set_graph(xtc_project_graph(replacement)); - window->sequencer_->set_project(replacement, false); - window->mark_dirty(); - if (out_added_id != nullptr && (edit.kind == XTC_EDIT_ADD_NODE || edit.kind == XTC_EDIT_INSERT_GAIN)) - *out_added_id = added_id; - window->refresh_status(); - return XTC_OK; -} - -/** - * @brief Opens the JACK backend and submits the current graph. - * - * Uses the default backend configuration and leaves the engine stopped. - * Failure is reported and leaves the backend disconnected; a handle that - * opened but failed to accept the graph is retained for reconnect. - */ -void MainWindow::open_backend() -{ - xtc_backend_config config = {XTC_BACKEND_JACK, settings_.client_name, static_cast(settings_.max_frames), static_cast(settings_.channels)}; - xtc_result result = xtc_backend_open(&config, &backend_); - ready_ = result == XTC_OK; - running_ = false; - if (result != XTC_OK) - report_error("Open JACK failed", result); - else { - xtc_backend_status format{}; - result = xtc_backend_get_status(backend_, &format); - if (result == XTC_OK) - result = xtc_backend_set_transition_frames(backend_, static_cast(format.sample_rate)*static_cast(settings_.transition_ms)/1000U); - if (result == XTC_OK) - result = xtc_backend_submit(backend_, xtc_project_graph(project_.get())); - if (result != XTC_OK) { - ready_ = false; // Handle is retained for close/reconnect; graph was not installed. - report_error("Submit graph failed", result); - } else - set_label(error_, ""); - } - refresh_status(); -} - -/** - * @brief Starts the audio engine with the current project. - * - * Does nothing when already running. Reports an error when disconnected. - * Otherwise prepares the project and starts the engine. Missing or - * unreadable audio is reported as a warning after a successful start. - */ -void MainWindow::start_engine() -{ - if (!ready_) { - report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE); - return; - } - if (running_) - return; - xtc_result result = configure_loop(project_.get(),settings_); - if(result==XTC_OK) result = xtc_backend_prepare_project(backend_, project_.get()); - if (result == XTC_OK) - result = start_configured_backend(); - if (result != XTC_OK) - report_error("Start failed; check audio and saved MIDI devices in Preferences", result); - else { - set_label(error_, midi_route_warning_.c_str()); - const char *issue = xtc_backend_asset_issue(backend_); - if (issue[0]) { - char message[640]; - std::snprintf(message, sizeof(message), - "Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", issue); - std::string warning=midi_route_warning_; - if(!warning.empty())warning+=" "; - warning+=message; - set_label(error_, warning.c_str()); - } - } - refresh_status(); -} - -/** - * @brief Requests a graceful playback stop, or immediately finalizes recording. - * - * Does nothing when disconnected. Failures are reported in the window. - */ -void MainWindow::stop_engine() -{ - if (!ready_) - return; - xtc_result result = recorder_ ? xtc_backend_stop(backend_) : xtc_backend_request_stop(backend_); - if (result != XTC_OK) - report_error("Stop engine failed", result); - else { - set_label(error_, ""); - if (recorder_) - finish_recording(); - } - refresh_status(); -} - -xtc_result MainWindow::pause_playback() -{ - xtc_result result = recorder_ || !ready_ ? XTC_ERR_INVALID_STATE : xtc_backend_request_stop(backend_); - if (result != XTC_OK) - report_error("Pause failed; use Stop to finish recording or Reconnect audio", result); - else - set_label(error_, ""); - refresh_status(); - return result; -} - -xtc_result MainWindow::return_to_start() -{ - if (recorder_) { - report_error("Stop recording before returning to start", XTC_ERR_INVALID_STATE); - return XTC_ERR_INVALID_STATE; - } - xtc_result result = XTC_OK; - if (ready_) { - result = xtc_backend_stop(backend_); - if (result == XTC_OK) - result = xtc_backend_seek(backend_, 0); - } - if (result != XTC_OK) - report_error("Return to start failed", result); - else { - sequencer_->position(0); - set_label(error_, ""); - } - refresh_status(); - return result; -} - -/** - * @brief Closes and reopens the JACK backend, leaving the engine stopped. - * - * Refuses to run while recording. If closing fails the existing handle is - * retained for a later retry and the failure is reported. - */ -void MainWindow::reconnect_engine() -{ - if (recorder_) { - report_error("Stop recording before reconnecting", XTC_ERR_INVALID_STATE); - return; - } - xtc_result result = xtc_backend_close(backend_); - if (result != XTC_OK) { - report_error("Reconnect could not close JACK", result); - return; // Preserve the existing handle for a later retry. - } - backend_ = nullptr; - ready_ = false; - running_ = false; - open_backend(); -} - -/** - * @brief Polls the backend and updates labels, meters, and button states. - * - * When disconnected, shows offline text and disables transport controls. - * Otherwise updates the sequencer position, mixer peaks, status line, and - * error indicators from the latest engine status, then collects backend - * work. - */ -void MainWindow::update_history_controls() -{ - size_t undo_count = 0, redo_count = 0; - if (!menu_ || xtc_history_counts(history_.get(),&undo_count,&redo_count) != XTC_OK) return; - for (const char *path : {"Edit/Undo","Edit/Redo","Edit/Redo alternate"}) { - int index = menu_->find_index(path); - if (index >= 0) menu_->mode(index,(!recorder_ && (std::strcmp(path,"Edit/Undo") == 0 ? undo_count : redo_count)) ? 0 : UI_MENU_INACTIVE); - } -} -void MainWindow::refresh_status() -{ - refresh_workspace(); - update_history_controls(); - if (mixer_ && !ready_) mixer_->meters(xtc_meter_snapshot{}); - if(track_inserts_ && !ready_)track_inserts_->meters(xtc_meter_snapshot{},false); - bool can_play = false, can_record = false; - if (!ready_) { - set_label(indicator_, "Audio: disconnected"); - set_label(status_, "JACK: not connected — use Reconnect after starting the server"); - indicator_->labelcolor(ui_rgb_color(255, 155, 120)); - set_label(graph_state_, "Graph: offline — edits stored locally; Reconnect to play"); - } else { - xtc_backend_collect(backend_); // Completes an acknowledged graceful stop before reading status. - xtc_backend_status status = {}; - xtc_result result = xtc_backend_get_status(backend_, &status); - if (result != XTC_OK) { - report_error("Read engine status failed", result); - if(track_inserts_)track_inserts_->meters(xtc_meter_snapshot{},false); - set_label(indicator_, "Audio: status unavailable"); - set_label(graph_state_, "Graph: engine status unavailable"); - set_label(status_, "Unable to read JACK status — use Reconnect"); - indicator_->labelcolor(ui_rgb_color(255, 155, 120)); - } else { - uint64_t tick = 0; - if (xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &tick) == XTC_OK) - sequencer_->position(tick); - running_ = status.running != 0; - const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK; - can_play = !running_ && !fault; - can_record = !fault && recorder_ == nullptr; - const char *state = status.server_lost ? "Audio: server lost — Reconnect required" - : status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" - : status.process_result != XTC_OK ? "Audio: processing error" - : status.stopping ? "Audio: stopping — fading to silence" - : recorder_ ? "Audio: recording JACK input — Stop to finish" - : running_ ? "Audio: running — modular graph" - : "Audio: ready / stopped"; - set_label(indicator_, state); - indicator_->labelcolor(fault ? ui_rgb_color(255, 155, 120) - : running_ ? ui_rgb_color(125, 225, 160) - : ui_rgb_color(215, 215, 215)); - xtc_meter_snapshot meters{}; - if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) mixer_->meters(meters); - else { meters.master_left=status.peak_left;meters.master_right=status.peak_right;mixer_->meters(meters); } - if(track_inserts_)track_inserts_->meters(meters,running_ && !fault,static_cast(settings_.refresh_ms)/1000); - char text[256]; - std::snprintf( - text, sizeof(text), - "JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops", - status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns, - xtc_result_string(status.process_result), status.asset_issues, status.midi_dropped); - set_label(status_, text); - if (fault) - set_label(graph_state_, "Graph: engine unavailable — Reconnect required"); - else if (status.submitted_revision != status.active_revision) - set_label(graph_state_, running_ ? "Graph: queued — engine still processing previous graph" - : "Graph: prepared — starts on Play"); - else - set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped"); - } - } - if (can_play) - play_->activate(); - else - play_->deactivate(); - if (ready_ && running_ && !recorder_) - pause_->activate(); - else - pause_->deactivate(); - if (!recorder_) - home_->activate(); - else - home_->deactivate(); - if (can_record) - record_->activate(); - else - record_->deactivate(); - if (ready_ && (running_ || recorder_)) - stop_->activate(); - else - stop_->deactivate(); -} - -/** - * @brief Attempts to close the window after discarding unsaved changes. - * - * Refuses while recording. On success closes the backend, removes the status - * timer, hides the piano-roll window, and hides this window. - * - * @return True when the window closed; false when recording is active, the - * user cancels discarding, or the backend cannot close. - */ -bool MainWindow::close_window() -{ - if (recorder_) { - report_error("Stop recording before closing", XTC_ERR_INVALID_STATE); - return false; - } - if (!discard_changes()) - return false; - xtc_result result = xtc_backend_close(backend_); - if (result != XTC_OK) { - report_error("Close JACK failed; window kept open for retry", result); - return false; - } - backend_ = nullptr; - ready_ = false; - running_ = false; - if(!configuration_.empty() && configuration_valid_) { - xtc_result saved=save_preferences(); - if(saved!=XTC_OK) {report_error("Could not save application settings; window kept open for retry",saved);return false;} - } - closing_ = true; - Ui::remove_timeout(timer_callback, this); - for(auto &panel:editors_)if(panel.window)panel.window->hide(); - if (piano_window_) - piano_window_->hide(); - if (transport_window_) transport_window_->hide(); - if (mixer_window_) mixer_window_->hide(); - hide(); - return true; -} - -/** - * @brief Handles JUCE events for the window. - * - * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. - * - * @param event JUCE event code, such as UI_SHORTCUT. - * @return 1 when Space was consumed; otherwise the UiDouble_Window result. - */ -int MainWindow::panel_shortcut(int event) -{ - if (event == UI_SHORTCUT && dynamic_cast(Ui::focus()) && - (Ui::event_key() == ' ' || (Ui::event_state(UI_CTRL) && - (Ui::event_key() == 'z' || Ui::event_key() == 'y' || Ui::event_key() == 'd')))) return 0; - if (event == UI_SHORTCUT && Ui::event_key() == ' ' && (Ui::event_state() & (UI_CTRL | UI_ALT | UI_META)) == 0) { - if (running_) - stop_engine(); - else - start_engine(); - return 1; - } - if (event == UI_SHORTCUT) return menu_->test_shortcut() != nullptr; - return 0; -} - -int MainWindow::handle(int event) -{ - if (panel_shortcut(event)) return 1; - return UiDouble_Window::handle(event); -} - -/** - * @brief JUCE callback for the Play menu item and button. - * - * @param context MainWindow * whose engine is started. - */ -void MainWindow::play_callback(UiWidget *, void *context) -{ - static_cast(context)->start_engine(); -} - -/** - * @brief JUCE callback for the Stop menu item and button. - * - * @param context MainWindow * whose engine is stopped. - */ -void MainWindow::stop_callback(UiWidget *, void *context) -{ - static_cast(context)->stop_engine(); -} - -/** - * @brief JUCE callback for the Reconnect menu item and button. - * - * @param context MainWindow * whose backend is reopened. - */ -void MainWindow::reconnect_callback(UiWidget *, void *context) -{ - static_cast(context)->reconnect_engine(); -} - -/** - * @brief JUCE callback for the window close box, File/Quit, and close events. - * - * @param context MainWindow * whose close is requested; a refused close - * simply leaves the window open. - */ -void MainWindow::close_callback(UiWidget *, void *context) -{ - (void)static_cast(context)->close_window(); -} - -/** - * @brief JUCE menu callback that selects a tab page in the main window. - * - * @param widget Triggering menu item; window() must return the MainWindow. - * @param view UiWidget * tab page to make current, not the MainWindow. - */ -void MainWindow::view_callback(UiWidget *widget, void *view) -{ - auto *window = static_cast(widget->window()); - window->tabs_->value(static_cast(view)); - window->tabs_->redraw(); - for(auto &panel:window->editors_)if(view==panel.page && panel.detached)panel.window->show(); - if(view==window->piano_page_) {if(window->open_piano_roll()!=XTC_OK)window->tabs_->value(window->editors_[0].page);} - if (view==window->mixer_page_ && window->mixer_detached_) window->mixer_window_->show(); -} - -/** - * @brief JUCE help callback that shows the application name and version. - * - */ -void MainWindow::about_callback(UiWidget *, void *) -{ - ui_message("%s %s\nModular audio workstation\nArrangement markers, MIDI composition, audio editing and modular mixing.", - XTC_PROJECT_NAME, XTC_VERSION_STRING); -} - -/** - * @brief JUCE timeout callback that refreshes status and supervises recording. - * - * While recording, drains the recorder and stops the engine if draining - * fails or the backend reports a lost server, sample-rate change, or - * processing error. Reschedules itself every 0.1 seconds until closing. - * - * @param context MainWindow * registered with Ui::add_timeout(). - */ -void MainWindow::timer_callback(void *context) -{ - xtc_plugin_idle(); - auto *window = static_cast(context); - if (window->recorder_) { - xtc_result r = xtc_recorder_drain(window->recorder_); - xtc_backend_status status{}; - xtc_backend_get_status(window->backend_, &status); - if (r != XTC_OK || status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK) - window->stop_engine(); - } - if(window->settings_.stop_after_tail && !window->settings_.loop_enabled && !window->recorder_) { - xtc_backend_status status{};uint64_t end=0; - if(xtc_backend_get_status(window->backend_,&status)==XTC_OK && status.running && - xtc_project_render_length(window->project_.get(),status.sample_rate,static_cast(window->settings_.tail_ms),&end)==XTC_OK && status.sample_position>=end) - window->stop_engine(); - } - window->refresh_status(); - window->follow_playhead(); - if (!window->closing_) - Ui::repeat_timeout(static_cast(window->settings_.refresh_ms)/1000,timer_callback,context); -} - -/** - * @brief Records the current project in history and marks the window dirty. - * - * Dependent widgets are refreshed with the current project and graph, and - * the title gains the unsaved marker. A history failure is reported. - */ -void MainWindow::mark_dirty() -{ - xtc_result history_result = xtc_history_record(history_.get(), project_.get()); - if (history_result != XTC_OK) - report_error("Undo history could not retain edit", history_result); - mixer_->set_project(project_.get()); - graph_widget_->set_graph(xtc_project_graph(project_.get())); - if (piano_) - piano_->set_project(project_.get(), piano_->clip()); - dirty_ = true; - update_history_controls(); - copy_label("xtc * — unsaved changes"); -} - -/** - * @brief Asks whether unsaved changes may be discarded. - * - * @return True when the project is clean, prompting is disabled, or the user - * chose Discard; false when the user cancels. - */ -bool MainWindow::discard_changes() -{ - return !dirty_ || !prompt_unsaved_ || - ui_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; -} - -/** - * @brief Hook used by the sequencer to seek the audio engine. - * - * Does nothing when the backend is disconnected. Otherwise reads the engine - * sample rate, converts the tick position to samples, and seeks the backend. - * - * @param context MainWindow * passed as the sequencer owner context. - * @param tick Target timeline position in project ticks. - * @return XTC_OK when disconnected or the seek succeeded, or the status, - * conversion, or backend seek error. - */ -xtc_result MainWindow::seek(void *context, uint64_t tick) -{ - auto *window = static_cast(context); - if (!window->ready_) - return XTC_OK; - xtc_backend_status status{}; - xtc_result r = xtc_backend_get_status(window->backend_, &status); - uint64_t sample = 0; - if (r == XTC_OK) - r = xtc_project_tick_to_sample(window->project_.get(), tick, status.sample_rate, &sample); - return r == XTC_OK ? xtc_backend_seek(window->backend_, sample) : r; -} - -/** - * @brief Saves the current project to a path and updates the window title. - * - * On success the path is remembered and the unsaved marker is cleared. - * - * @param path Destination project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, - * XTC_ERR_INVALID_STATE while recording, or the save error. - */ -xtc_result MainWindow::save_project(const char *path) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - if (path == nullptr) - return XTC_ERR_INVALID_ARGUMENT; - std::string saved_path(path); - xtc_result r = xtc_project_save(project_.get(), path); - if (r == XTC_OK) { - path_ = saved_path; - dirty_ = false; - copy_label(path_.c_str()); - } - return r; -} - -/** - * @brief Installs a candidate project as the active project. - * - * Rejected while recording. Optionally builds fresh undo history, stops and - * prepares the connected backend, or validates offline audio and MIDI plans - * when disconnected. On success the window takes ownership of candidate, - * rebinds widgets, resets the graph layout, clears the dirty flag, and shows - * any missing-asset warning. On failure ownership stays with the caller. - * - * @param candidate Project to install; must be non-null. - * @param reset_history True to discard existing undo/redo history and start - * from candidate. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from history creation, the backend, or offline planning. - */ -xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_history) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - std::unique_ptr new_history(nullptr, xtc_history_destroy); - if (reset_history) { - xtc_history *h = nullptr; - xtc_result result = xtc_history_create(candidate, &h); - if (result != XTC_OK) - return result; - new_history.reset(h); - } - xtc_result r = XTC_OK; - if (ready_) { - r = xtc_backend_stop(backend_); - if (r == XTC_OK) - r = xtc_backend_seek(backend_, 0); - if(r==XTC_OK) r=configure_loop(candidate,settings_); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, candidate); - } - xtc_audio_plan *offline = nullptr; - if (r == XTC_OK && !ready_) - r = xtc_audio_plan_create(candidate, static_cast(settings_.offline_rate), &offline); - xtc_midi_plan *offline_midi = nullptr; - if (r == XTC_OK && !ready_) - r = xtc_midi_plan_create(candidate, static_cast(settings_.offline_rate), &offline_midi); - size_t sample_issues=xtc_midi_plan_asset_issues(offline_midi); - xtc_midi_plan_destroy(offline_midi); - if (r == XTC_OK && !ready_) { - xtc_plan *graph = nullptr; - r = xtc_graph_compile(xtc_project_graph(candidate),static_cast(settings_.offline_rate),256,&graph); - xtc_plan_destroy(graph); - } - if (r != XTC_OK) { - xtc_audio_plan_destroy(offline); - refresh_status(); // A failed install may have stopped playback. - return r; - } - std::string issue = ready_ ? xtc_backend_asset_issue(backend_) : ""; - if (offline) { - const char *path = ""; - xtc_audio_plan_issues(offline, &path); - issue = path; - xtc_audio_plan_destroy(offline); - } - if(sample_issues)issue="Sampler WAV missing/invalid; File/Load sampler WAV relinks the selected track"; - if (piano_window_) - piano_window_->hide(); - if (piano_) - piano_->set_project(candidate, UINT64_MAX); - project_.reset(candidate); - mixer_->set_project(candidate); - if (reset_history) { - history_.swap(new_history); - for(auto &snapshot:mix_snapshots_)snapshot.reset(); - if(track_inserts_)track_inserts_->clear_session(); - xtc_tap_tempo_reset(&tap_state_); - } - graph_widget_->reset_layout(); - graph_widget_->set_graph(xtc_project_graph(candidate)); - sequencer_->set_project(candidate,true,!reset_history); - if (reset_history) - tabs_->value(tabs_->child(0)); - dirty_ = false; - if (!issue.empty()) { - char text[640]; - std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", - issue.c_str()); - set_label(error_, text); - } else - set_label(error_, ""); - refresh_status(); - return XTC_OK; -} - -/** - * @brief Loads a project from a path and makes it the current project. - * - * The file is loaded into a candidate project and installed only on success. - * - * @param path Source project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an - * error from loading or installing the project. - */ -xtc_result MainWindow::open_project(const char *path) -{ - BusyOperation busy(*this,status_,"Loading and preparing project…"); - if (path == nullptr) - return XTC_ERR_INVALID_ARGUMENT; - project_issue_.clear(); - std::string loaded_path(path); - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_create(&candidate); - xtc_project_diagnostic diagnostic{}; - if (r == XTC_OK) { - r = xtc_project_load_diagnostic(candidate,path,&diagnostic); - if (r != XTC_OK) { - project_issue_ = "Project load line " + std::to_string(diagnostic.line) + " (" + diagnostic.record + "): " + diagnostic.message; - report_error(project_issue_.c_str(),r); - } - } - if (r == XTC_OK) - r = install_project(candidate); - if (r != XTC_OK) { - if (candidate) { - const xtc_graph *graph = xtc_project_graph(candidate); - size_t nodes = 0, edges = 0; - xtc_graph_get_counts(graph,&nodes,&edges); - for (size_t i = 0; i < nodes; ++i) { - xtc_node_info node{}; xtc_plugin_spec spec{}; - if (xtc_graph_get_node(graph,i,&node) != XTC_OK || node.type != XTC_NODE_PLUGIN || - xtc_graph_get_plugin(graph,node.id,&spec) != XTC_OK) continue; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin *check = nullptr; - xtc_result plugin_result = xtc_plugin_create(&spec,rate,256,&check); - xtc_plugin_destroy(check); - if (plugin_result != XTC_OK) { - project_issue_ = std::string("Missing/incompatible plugin ") + spec.label + " at " + spec.path + - "; restore a compatible module and retry Open. Saved document retained"; - report_error(project_issue_.c_str(),plugin_result); - break; - } - } - } - xtc_project_destroy(candidate); - } else { - path_ = loaded_path; - copy_label(path_.c_str()); - } - return r; -} - -xtc_result MainWindow::new_project() -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result result = xtc_project_create(&candidate); - if(result==XTC_OK) project_defaults(candidate); - if (result == XTC_OK) - result = install_project(candidate); - if (result != XTC_OK) - xtc_project_destroy(candidate); - else { - path_.clear(); - copy_label("xtc — Untitled"); - } - return result; -} - -/** - * @brief JUCE menu callback for the File/New, Open, Save, and Save As items. - * - * Reads the action from the selected menu label, confirms discarding unsaved - * changes for New and Open, and uses a native file chooser unless Save can - * reuse the remembered path. Refuses file operations while recording and - * reports failures in the window. - * - * @param widget Triggering UiMenu_Bar whose selected label names the action. - * @param context MainWindow * owning the project and file path. - */ -void MainWindow::file_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) { - window->report_error("Stop recording before file operations", XTC_ERR_INVALID_STATE); - return; - } - auto *menu = static_cast(widget); - const char *action = menu->mvalue()->label(); - xtc_result r = XTC_OK; - window->project_issue_.clear(); - if (std::strcmp(action, "New") == 0) { - if (!window->discard_changes()) - return; - r = window->new_project(); - } else { - bool open = std::strcmp(action, "Open") == 0; - if (open && !window->discard_changes()) - return; - if (!open && std::strcmp(action, "Save") == 0 && !window->path_.empty()) - r = window->save_project(window->path_.c_str()); - else { - UiNative_File_Chooser chooser; - chooser.title(open ? "Open project" : "Save project"); - chooser.type(open ? UiNative_File_Chooser::BROWSE_FILE : UiNative_File_Chooser::BROWSE_SAVE_FILE); - chooser.filter("xtc projects *.xtc"); - if (!open) - chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM); - int result = chooser.show(); - if (result == 1) - return; - if (result < 0) - r = XTC_ERR_IO; - else - r = open ? window->open_project(chooser.filename()) : window->save_project(chooser.filename()); - } - } - if (r != XTC_OK) - window->report_error(window->project_issue_.empty() ? "Project operation failed" : window->project_issue_.c_str(), r); -} - -/** - * @brief Hook called by arrangement widgets before mutating the project. - * - * Refuses edits while recording. When connected, stops the engine so the - * mutation cannot race the audio thread, then refreshes the status display. - * - * @param context MainWindow * passed as the widget owner context. - * @return XTC_OK when disconnected or the stop succeeded, - * XTC_ERR_INVALID_STATE while recording, or the backend stop error. - */ -xtc_result MainWindow::before_arrangement_edit(void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) return XTC_ERR_INVALID_STATE; - xtc_result r = xtc_history_reserve(window->history_.get()); - if (r == XTC_OK && window->ready_) r = xtc_backend_stop(window->backend_); - window->refresh_status(); - return r; -} - -/** - * @brief Imports a WAV file as an audio clip in a cloned project. - * - * Stops the engine first, creates an "Audio" track when none is selected, - * and places the clip at the sequencer cursor. The clone replaces the live - * project only after every step succeeds. - * - * @param path Path to the WAV file to import. - * @return XTC_OK on success, or an error from the engine stop, clone, import, - * or backend preparation. - */ -xtc_result MainWindow::import_audio(const char *path) -{ - BusyOperation busy(*this,status_,"Importing audio and preparing playback…"); - xtc_result r = before_arrangement_edit(this); - if (r != XTC_OK) - return r; - xtc_project *candidate = nullptr; - r = xtc_project_clone(project_.get(), &candidate); - uint64_t track = sequencer_->selected_track(), clip = 0; - if (r == XTC_OK && track == UINT64_MAX) - r = xtc_project_add_track(candidate, "Audio", &track); - if (r == XTC_OK) - r = xtc_project_import_audio(candidate, track, path, sequencer_->position(), &clip); - if (r == XTC_OK && ready_) - r = xtc_backend_prepare_project(backend_, candidate); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - return r; - } - project_.reset(candidate); - sequencer_->set_project(candidate, false); - sequencer_->select(track,clip); - graph_widget_->set_graph(xtc_project_graph(candidate)); - mark_dirty(); - refresh_status(); - return XTC_OK; -} - -/** - * @brief Starts recording JACK input to a WAV file. - * - * Requires a connected backend and no active recording. The engine is - * stopped, the current position and selected track are remembered, and a - * stereo recorder at the engine sample rate is attached before starting. - * Failure leaves the window without a recorder. - * - * @param path Destination WAV path chosen by the user. - * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when - * disconnected or already recording, or another backend or recorder - * error. - */ -xtc_result MainWindow::record_audio(const char *path) -{ - if (!ready_ || recorder_ || settings_.loop_enabled) - return XTC_ERR_INVALID_STATE; - xtc_result r = xtc_backend_stop(backend_); - xtc_backend_status status{}; - if (r == XTC_OK) - r = xtc_backend_get_status(backend_, &status); - if (r == XTC_OK) - r = xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &recording_tick_); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, project_.get()); - if (r == XTC_OK) - r = xtc_recorder_create(path, status.sample_rate, static_cast(status.sample_rate) * static_cast(settings_.recording_seconds), &recorder_); - if (r != XTC_OK) - return r; - recording_path_ = path; - recording_track_ = sequencer_->selected_track(); - r = xtc_backend_set_recorder(backend_, recorder_); - if (r == XTC_OK) - r = start_configured_backend(); - if (r != XTC_OK) { - (void)xtc_backend_set_recorder(backend_, nullptr); - xtc_recorder_destroy(recorder_); - recorder_ = nullptr; - } - refresh_status(); - return r; -} - -/** - * @brief Finalizes the active recording and adds the take to the project. - * - * Must run only after the engine has stopped, so no audio callback can - * reference the recorder ring. The recorder is detached, finished, and - * destroyed. A failed finalize or empty take is reported and not added. If - * the subsequent import fails, the saved WAV path is reported for recovery. - */ -void MainWindow::finish_recording() -{ - // Called only after successful backend_stop: no producer can reference ring. - (void)xtc_backend_set_recorder(backend_, nullptr); - uint64_t frames = 0; - (void)xtc_recorder_status(recorder_, &frames); - xtc_result r = xtc_recorder_finish(recorder_); - std::string recovery_path = xtc_recorder_recovery_path(recorder_); - xtc_recorder_destroy(recorder_); - recorder_ = nullptr; - if (r != XTC_OK) { - char text[768]; - std::snprintf(text, sizeof(text), - "Recording failed (%s). Retained take/temporary: %s. Recover with xtc-check --recover-recording. Not added.", - xtc_result_string(r), recovery_path.c_str()); - set_label(error_, text); - return; - } - if (!frames) { - set_label(error_, "Empty recording saved; no clip added."); - return; - } - xtc_project *candidate = nullptr; - r = xtc_project_clone(project_.get(), &candidate); - uint64_t clip = 0, track = recording_track_; - if (r == XTC_OK && track == UINT64_MAX) - r = xtc_project_add_track(candidate, "Recording", &track); - if (r == XTC_OK) - r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, candidate); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - char text[768]; - std::snprintf(text, sizeof(text), "Take saved to %s; adding clip failed: %s. Import the WAV to recover.", - recording_path_.c_str(), xtc_result_string(r)); - set_label(error_, text); - return; - } - project_.reset(candidate); - sequencer_->set_project(candidate, false); - graph_widget_->set_graph(xtc_project_graph(candidate)); - mark_dirty(); -} - -/** - * @brief JUCE callback for the audio import, record, and relink commands. - * - * The triggering widget supplies the menu label; the Record button uses the - * "Record WAV" default. A native file chooser selects the file, after which - * the selected clip is relinked, recording starts, or a WAV is imported. - * Failures are reported in the window. - * - * @param widget Triggering UiMenu_Bar or UiButton; menu labels select the - * action. - * @param context MainWindow * owning the project and sequencer. - */ -void MainWindow::audio_file_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - auto *menu = dynamic_cast(widget); - const char *action = menu ? menu->mvalue()->label() : - (widget && widget->label() && !std::strcmp(widget->label(),"Import WAV") ? "Import WAV" : "Record WAV"); - bool recording = std::strcmp(action, "Record WAV") == 0; - bool relink = std::strcmp(action, "Relink selected audio") == 0; - if (window->recorder_) { - window->report_error("Stop recording first", XTC_ERR_INVALID_STATE); - return; - } - UiNative_File_Chooser chooser; - chooser.title(recording ? "Record JACK input to WAV" : relink ? "Relink selected clip" : "Import WAV"); - chooser.type(recording ? UiNative_File_Chooser::BROWSE_SAVE_FILE : UiNative_File_Chooser::BROWSE_FILE); - chooser.filter("WAV audio\t*.wav"); - // Recordings require a new destination; existing files are never replaced. - int chosen = chooser.show(); - if (chosen == 1) - return; - xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK; - if (r == XTC_OK && relink) { - r = before_arrangement_edit(window); - if (r == XTC_OK) - r = xtc_project_set_clip_asset(window->project_.get(), window->sequencer_->selected_clip(), - chooser.filename()); - if (r == XTC_OK) { - window->mark_dirty(); - window->sequencer_->redraw(); - } - } else if (r == XTC_OK) - r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); - if (r != XTC_OK) - window->report_error(recording ? "Project recording failed; choose a new WAV path in a writable directory" - : "Project audio operation failed", r); -} - -/** - * @brief Shows the piano roll for the selected MIDI clip. - * - * The piano-roll window is created on first use and reused afterwards. - * - * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip - * does not exist or is not MIDI. - */ -xtc_result MainWindow::open_piano_roll() -{ - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_.get(), sequencer_->selected_clip(), &clip) != XTC_OK || !clip.midi) - return XTC_ERR_INVALID_STATE; - if (!piano_window_) { - piano_window_ = std::make_unique(900, 620, "xtc — Piano roll (close to dock)",this); - piano_window_->begin(); - piano_content_=new UiGroup(0,0,900,620);piano_content_->begin(); - auto *menu = new UiMenu_Bar(12, 8, 876, 30); - const char *commands[] = {"Notes/Grid (G)", "Notes/Filter (F)", "Notes/Velocity and channel (V)", - "Notes/Quantize (Q)", "Notes/Transpose (T)", "Notes/Repeat (Ctrl+D)", - "Notes/Select filtered (Ctrl+A)", "Notes/Delete"}; - for (int i = 0; i < 8; ++i) - menu->add(commands[i], 0, [](UiWidget *widget, void *data) { - auto *owner = static_cast(data); - auto *bar = static_cast(widget); - owner->piano_->command(static_cast(bar->mvalue() - bar->menu())); - }, this); - menu->add("Edit/Undo",UI_CTRL+'z',history_callback,this); - menu->add("Edit/Redo",UI_CTRL+UI_SHIFT+'z',history_callback,this); - menu->add("Edit/Redo alternate",UI_CTRL+'y',history_callback,this); - piano_snap_ = new UiChoice(60,46,150,26,"Snap"); - for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = piano_snap_->add("Snap"); piano_snap_->replace(item,xtc_ui::snap_labels[i]); } - piano_snap_->callback([](UiWidget *widget, void *c) { - auto *w = static_cast(c); int value = static_cast(widget)->value(); - if (value >= 0 && static_cast(value) < xtc_ui::snap_count) (void)w->piano_->set_grid(xtc_ui::snap_ticks[value]); - },this); - note_velocity_ = new UiValue_Slider(340,46,300,26,"Note velocity"); - note_velocity_->align(UI_ALIGN_LEFT); note_velocity_->value(100); - note_velocity_->tooltip("Selected note MIDI velocity, 1..127. Instrument patches determine the response."); - note_velocity_->type(UI_HOR_FILL_SLIDER); note_velocity_->bounds(1,127); note_velocity_->step(1); - note_velocity_->when(UI_WHEN_RELEASE); - note_velocity_->callback([](UiWidget *widget, void *c) { - auto *w = static_cast(c); - (void)w->piano_->set_velocity(w->piano_->selected_note(),static_cast(static_cast(widget)->value())); - },this); - auto *scroll = new UiScroll(12,84,876,404); - piano_ = new PianoRollWidget(12,84, project_.get(), clip.id, before_arrangement_edit, this); - piano_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - scroll->end(); - velocity_lane_ = new VelocityLane(12,500,876,108,*piano_); - piano_content_->resizable(scroll);piano_content_->end(); - piano_window_->end(); - piano_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_piano(false);},this); - piano_window_->size_range(800,520); - xtc_ui::style(piano_window_.get()); - piano_window_->resizable(piano_content_); - scroll->scroll_to(0, 60 * 16); - auto px=settings_.piano_x,py=settings_.piano_y,pw=settings_.piano_width,ph=settings_.piano_height; - collect_preferences();settings_.piano_x=px;settings_.piano_y=py;settings_.piano_width=pw;settings_.piano_height=ph; - apply_ui_preferences(); - } else - piano_->set_project(project_.get(), clip.id); - refresh_workspace(); - detach_piano(settings_.piano_detached!=0); - if(piano_detached_)piano_window_->show();else {tabs_->value(piano_page_);tabs_->redraw();} - return XTC_OK; -} - -/** - * @brief JUCE callback for the Piano roll menu item and button. - * - * Refuses to open the editor while recording and asks the user to select a - * MIDI clip when open_piano_roll() fails. - * - * @param context MainWindow * owning the piano roll. - */ -void MainWindow::piano_callback(UiWidget *, void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) { - window->report_error("Stop recording before editing MIDI", XTC_ERR_INVALID_STATE); - return; - } - xtc_result r = window->open_piano_roll(); - if (r != XTC_OK) - set_label(window->error_, "Select a MIDI clip, then open Piano roll."); -} - -/** - * @brief JUCE callback for the Panic menu item and button. - * - * When the backend is connected, sends a panic request that silences held - * instrument notes and reports failures. - * - * @param context MainWindow * owning the backend. - */ -void MainWindow::panic_callback(UiWidget *, void *context) -{ - auto *window = static_cast(context); - if (!window->ready_) - return; - xtc_result r = xtc_backend_panic(window->backend_); - if (r != XTC_OK) - window->report_error("Panic failed", r); -} - -/** - * @brief Applies one undo or redo step from the project history. - * - * The selected snapshot is installed without resetting history and then - * accepted, leaving the window marked dirty. - * - * @param redo True to redo, false to undo. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, - * XTC_ERR_NOT_FOUND when no step is available, or another error from - * checkout, install, or accept. - */ -xtc_result MainWindow::undo(bool redo) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - const uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip(); - const uint64_t mixer_track = mixer_->selected_track(); - const size_t node = graph_widget_->selected_node(); - const uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX; - const uint64_t note = piano_ ? piano_->selected_note() : UINT64_MAX; - const bool piano_visible = piano_window_ && piano_window_->shown(); - xtc_project *candidate = nullptr; - xtc_result r = xtc_history_checkout(history_.get(), redo ? 1 : 0, &candidate); - if (r == XTC_OK) - r = install_project(candidate, false); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - return r; - } - xtc_clip_info selected_clip{}; - if (xtc_project_find_clip(project_.get(),clip,&selected_clip) == XTC_OK) - sequencer_->select(selected_clip.track_id,clip); - else sequencer_->select(track); - mixer_->select_track(mixer_track); - graph_widget_->select_node(node); - xtc_clip_info info{}; - if (piano_ && xtc_project_find_clip(project_.get(),piano_clip,&info) == XTC_OK && info.midi) { - piano_->set_project(project_.get(),piano_clip); piano_->select_note(note); - if (piano_visible) piano_window_->show(); - } - r = xtc_history_accept(history_.get(), redo ? 1 : 0); - dirty_ = true; - update_history_controls(); - copy_label("xtc * — unsaved changes"); - return r; -} - -/** - * @brief JUCE menu callback for the Undo and Redo menu items. - * - * The selected menu label starting with "Redo" chooses redo; all other - * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. - * - * @param widget UiMenu_Bar whose selected item triggered the callback. - * @param context MainWindow * owning the menu and history. - */ -void MainWindow::history_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - auto *menu = static_cast(widget); - bool redo = std::strncmp(menu->mvalue()->label(), "Redo", 4) == 0; - xtc_result r = window->undo(redo); - if (r != XTC_OK && r != XTC_ERR_NOT_FOUND) - window->report_error("Undo/redo failed", r); -} - -xtc_result MainWindow::create_track(bool midi) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - const uint64_t position = sequencer_->position(); - xtc_project *candidate = nullptr; - uint64_t track = UINT64_MAX, clip = UINT64_MAX; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_add_track(candidate,midi ? "MIDI track" : "Audio track",&track); - if (r == XTC_OK && midi) r = xtc_project_add_midi_clip(candidate,track,position,static_cast(settings_.clip_ticks),&clip); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); - else { - sequencer_->select(track,clip); - mixer_->select_track(track); tabs_->value(tabs_->child(0)); mark_dirty(); - } - return r; -} -void MainWindow::refresh_workspace() -{ - if(track_inserts_)track_inserts_->refresh(); - if (loop_button_) { - loop_button_->value(settings_.loop_enabled != 0); - if (recorder_) - loop_button_->deactivate(); - else - loop_button_->activate(); - } - if (snap_) - snap_->value(xtc_ui::snap_index(sequencer_->grid())); - if (piano_) { - piano_snap_->value(xtc_ui::snap_index(piano_->grid())); - size_t count = 0; - bool selected = false; - xtc_project_note_count(project_.get(), &count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; - xtc_project_get_note(project_.get(), i, ¬e); - if (note.id == piano_->selected_note() && piano_->note_visible(note)) { - selected = true; - if (Ui::pushed() != note_velocity_) - note_velocity_->value(note.velocity); - break; - } - } - if (selected) - note_velocity_->activate(); - else - note_velocity_->deactivate(); - velocity_lane_->redraw(); - } - char position[64], info[512]; - uint64_t tick = sequencer_->position(); - std::snprintf(position, sizeof(position), "%03llu : %u : %03u", static_cast(tick / 3840 + 1), - static_cast((tick / 960) % 4 + 1), static_cast(tick % 960)); - set_label(transport_position_, position); - if (!editing_number(tempo_display_)) - tempo_display_->value(xtc_project_tempo(project_.get()) / 1000.0); - if (recorder_) - tempo_display_->deactivate(); - else - tempo_display_->activate(); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_.get(), &tracks, &clips); - uint64_t end = 0; - for (size_t i = 0; i < clips; ++i) { - xtc_clip_info clip{}; - xtc_project_get_clip(project_.get(), i, &clip); - end = std::max(end, clip.start_tick + clip.length_tick); - } - std::snprintf(info, sizeof(info), "%zu tracks / %zu clips / %llu bars", tracks, clips, - static_cast((end + 3839) / 3840)); - set_label(session_info_, info); - bool mixer_context = tabs_->value() == mixer_page_ || - (mixer_detached_ && Ui::focus() && Ui::focus()->window() == mixer_window_.get()); - uint64_t target = mixer_context ? mixer_->selected_track() : sequencer_->selected_track(); - bool selection_changed = target != inspected_track_; - inspected_track_ = UINT64_MAX; - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info track{}; - xtc_project_get_track(project_.get(), i, &track); - if (track.id != target) - continue; - inspected_track_ = track.id; - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) { - if (recorder_) - control->deactivate(); - else - control->activate(); - } - if (selection_changed || !editing_number(inspector_gain_)) - inspector_gain_->value(track.gain); - if (selection_changed || !editing_number(inspector_pan_)) - inspector_pan_->value(track.pan); - if (Ui::pushed() != inspector_mute_) - inspector_mute_->value(track.mute); - if (Ui::pushed() != inspector_solo_) - inspector_solo_->value(track.solo); - xtc_plugin_spec spec{}; - bool plugin = xtc_project_get_instrument(project_.get(), track.id, &spec) == XTC_OK; - std::snprintf(info, sizeof(info), "%s\n%s %s%s\n%s", track.name, track.is_bus ? "BUS" : "TRACK", - track.mute ? "Muted " : "", track.solo ? "Solo" : "", - track.is_bus ? "Bus output" - : plugin ? spec.label - : "Built-in MIDI sine"); - set_label(selection_info_, info); - return; - } - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) - control->deactivate(); - std::snprintf(info, sizeof(info), "No track selected\nCreate a MIDI track or import a WAV to begin."); - set_label(selection_info_, info); -} - -xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index) -{ - uint64_t clip = sequencer_->selected_track() == track ? sequencer_->selected_clip() : UINT64_MAX; - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_plugin_spec spec{}; - xtc_result r = path ? xtc_plugin_probe(path,index,sample_rate(),&spec) : XTC_OK; - if (r == XTC_OK && path && !spec.instrument) return XTC_ERR_UNSUPPORTED; - if (r == XTC_OK) r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_instrument(candidate,track,path ? &spec : nullptr); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); } - return r; -} -xtc_result MainWindow::import_midi_file(const char *path) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - uint64_t track=sequencer_->selected_track(),start=sequencer_->position(),clip=0;xtc_project *candidate=nullptr; - xtc_result r=before_arrangement_edit(this); - if(r==XTC_OK)r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK && track==UINT64_MAX)r=xtc_project_add_track(candidate,"Imported MIDI",&track); - if(r==XTC_OK)r=xtc_project_import_midi(candidate,track,start,path,&clip); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK)xtc_project_destroy(candidate); - else{sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();refresh_status();} - return r; -} -void MainWindow::import_midi_dialog() -{ - if(recorder_){report_error("Stop recording before importing MIDI",XTC_ERR_INVALID_STATE);return;} - UiNative_File_Chooser chooser;chooser.title("Import MIDI notes (keeps project tempo)");chooser.type(UiNative_File_Chooser::BROWSE_FILE);chooser.filter("Standard MIDI File\t*.{mid,midi}"); - if(chooser.show()!=0)return; - auto r=import_midi_file(chooser.filename());if(r!=XTC_OK)report_error("MIDI import failed (type 0/1 PPQN notes required)",r); - else set_label(error_,"Imported MIDI notes at cursor; file tracks merged and project tempo retained. Undo reverses import."); -} -xtc_result MainWindow::export_midi_clip(const char *path) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - auto ids=sequencer_->selected_clips();if(ids.size()!=1)return XTC_ERR_INVALID_ARGUMENT; - return xtc_project_export_midi_clip(project_.get(),ids[0],path); -} -void MainWindow::export_midi_dialog() -{ - auto ids=sequencer_->selected_clips();xtc_clip_info clip{}; - if(ids.size()!=1 || xtc_project_find_clip(project_.get(),ids[0],&clip)!=XTC_OK || !clip.midi){report_error("Select one MIDI clip to export",XTC_ERR_INVALID_ARGUMENT);return;} - UiNative_File_Chooser chooser;chooser.title("Export selected MIDI clip");chooser.type(UiNative_File_Chooser::BROWSE_SAVE_FILE); - chooser.filter("Standard MIDI File\t*.mid");chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM);chooser.preset_file("clip.mid"); - if(chooser.show()!=0)return; - auto r=export_midi_clip(chooser.filename());if(r!=XTC_OK)report_error("MIDI export failed",r);else set_label(error_,"MIDI clip exported (notes, channels, velocities and tempo)."); -} -void MainWindow::export_audio() -{ - if(recorder_){report_error("Stop recording before export",XTC_ERR_INVALID_STATE);return;} - const char *answer=ui_input("Export stereo WAV to a new absolute path (tail configured in Preferences)",""); - if(!answer)return; - std::string path=answer;xtc_export *job=nullptr; - xtc_result r=xtc_export_create(project_.get(),path.c_str(),static_cast(settings_.offline_rate),static_cast(settings_.tail_ms),&job); - if(r==XTC_OK) { - UiDouble_Window dialog(460,130,"Export WAV");dialog.set_modal();dialog.begin(); - UiBox progress(12,12,436,45,"Preparing export");UiButton cancel(330,80,110,30,"Cancel export"); - cancel.callback([](UiWidget *,void *c){static_cast(c)->hide();},&dialog); - dialog.callback([](UiWidget *,void *c){static_cast(c)->hide();},&dialog);dialog.end();xtc_ui::style(&dialog);dialog.show(); - int done=0; - while(dialog.shown() && !done && r==XTC_OK) { - r=xtc_export_step(job,&done);uint64_t position=0,total=0;xtc_export_progress(job,&position,&total); - std::string text=std::to_string(position)+" / "+std::to_string(total)+" frames";progress.copy_label(text.c_str());Ui::check(); - } - if(r==XTC_OK && !done)set_label(status_,"Export cancelled; destination retained"); - else if(r==XTC_OK)set_label(status_,"Export complete"); - dialog.hide(); - } - xtc_export_destroy(job); - if(r!=XTC_OK)report_error("Export failed; destination must be new and media available",r); -} -xtc_result MainWindow::load_sampler(uint64_t track,const char *path) -{ - if(!path || path[0]!='/' || std::strlen(path)>=sizeof(xtc_plugin_spec{}.sample_path))return XTC_ERR_INVALID_ARGUMENT; - xtc_plugin_spec spec{};xtc_result r=xtc_project_get_instrument(project_.get(),track,&spec); - if(r!=XTC_OK || spec.format!=XTC_PLUGIN_BUILTIN || spec.index!=4)r=xtc_plugin_probe(XTC_BUILTIN_PATH,4,sample_rate(),&spec); - if(r!=XTC_OK)return r; - std::strcpy(spec.sample_path,path);return set_instrument_spec(track,spec); -} -xtc_result MainWindow::set_instrument_spec(uint64_t track,const xtc_plugin_spec &spec) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - uint64_t clip=sequencer_->selected_clip();xtc_project *candidate=nullptr; - xtc_result r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_instrument(candidate,track,&spec); - if(r==XTC_OK)r=xtc_history_reserve(history_.get()); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK)xtc_project_destroy(candidate); - else {sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();} - return r; -} -xtc_result MainWindow::select_live_instrument(uint64_t track) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_live_instrument(candidate,track); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty(); - return r; -} -void MainWindow::instrument_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before instrument changes",XTC_ERR_INVALID_STATE); return; } - auto *menu = dynamic_cast(widget); - const char *label = menu ? menu->mvalue()->label() : "Load instrument plugin"; - uint64_t track = w->sequencer_->selected_track(); - xtc_result r; - if(std::strcmp(label,"Load sampler WAV")==0) { - const char *path=ui_input("Sampler WAV path (absolute; replaces/relinks selected track sample)",""); - if(path) {r=w->load_sampler(track,path);if(r!=XTC_OK)w->report_error("Sampler preparation failed",r);} - return; - } - if(std::strcmp(label,"Selected instrument controls")==0 || std::strcmp(label,"Selected instrument automation")==0 || - std::strcmp(label,"Save instrument preset")==0 || std::strcmp(label,"Load instrument preset")==0) { - xtc_plugin_spec spec{}; - if(std::strcmp(label,"Load instrument preset")==0) { - const char *path=ui_input("Instrument preset path (absolute)","");if(!path)return; - r=xtc_plugin_preset_load(path,&spec); - if(r==XTC_OK)r=w->set_instrument_spec(track,spec); - } else { - r=xtc_project_get_instrument(w->project_.get(),track,&spec); - if(r==XTC_OK && spec.format==XTC_PLUGIN_BUILTIN && !spec.control_count) - r=xtc_plugin_probe(XTC_BUILTIN_PATH,spec.index,w->sample_rate(),&spec); - if(r==XTC_OK && std::strcmp(label,"Save instrument preset")==0) { - const char *path=ui_input("Save instrument preset path (absolute)","");if(!path)return; - r=xtc_plugin_preset_save(&spec,path); - } else if(r==XTC_OK && std::strcmp(label,"Selected instrument controls")==0) { - xtc_plugin_details details{};xtc_plugin_spec edited{}; - r=xtc_plugin_describe(&spec,w->sample_rate(),&details); - if(r==XTC_OK && xtc_edit_plugin(spec,details,edited))r=w->set_instrument_spec(track,edited); - } else if(r==XTC_OK) { - xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};r=xtc_plugin_parameters(&spec,w->sample_rate(),info); - std::string prompt="Instrument automation: port tick mode value (mode -1 removes)\n"; - for(unsigned int i=0;i1)r=XTC_ERR_INVALID_ARGUMENT; - else { - unsigned int i=0;for(;i(tick),port,static_cast(mode),value};} - if(r==XTC_OK)r=w->set_instrument_spec(track,spec); - } - } - } - } - if(r!=XTC_OK)w->report_error("Instrument edit failed; check track, preset and control ranges",r); - return; - } - if (std::strcmp(label,"Use built-in instrument") == 0) r = w->assign_instrument(track,nullptr,0); - else if (std::strcmp(label,"Live MIDI to selected instrument") == 0) r = w->select_live_instrument(track); - else if (std::strcmp(label,"Live MIDI to built-in sine") == 0) r = w->select_live_instrument(UINT64_MAX); - else { - std::string module; if(!xtc_choose_plugin_file(w->settings_,module)) return; - unsigned int index=0; - if(!xtc_choose_plugin(module.c_str(),w->sample_rate(),index,true))return; - r=w->assign_instrument(track,module.c_str(),index); - } - if (r != XTC_OK) w->report_error("Instrument assignment failed; select a non-bus track and compatible mono instrument",r); -} - -xtc_result MainWindow::set_control_map(unsigned int channel, const unsigned int controllers[3]) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_control_map(candidate,channel,controllers); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty(); - return r; -} -void MainWindow::control_map_callback(UiWidget *, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before controller mapping",XTC_ERR_INVALID_STATE); return; } - unsigned int controllers[3]{}; - std::string prompt = "Live sine MIDI bindings (channels 0..15; 128 disables). CC120/123 reserved.\n"; - for (unsigned int channel = 0; channel < 16; ++channel) { - xtc_project_get_control_map(w->project_.get(),channel,controllers); - prompt += std::to_string(channel) + ": volume " + std::to_string(controllers[0]) + - ", pan " + std::to_string(controllers[1]) + ", sustain " + std::to_string(controllers[2]) + "\n"; - } - prompt += "Enter channel volume_CC pan_CC sustain_CC (distinct enabled CCs):"; - const char *answer = ui_input("%s","0 7 10 64",prompt.c_str()); - if (!answer) return; - unsigned int channel; char extra; - xtc_result r = std::sscanf(answer,"%u %u %u %u %c",&channel,controllers,controllers+1,controllers+2,&extra) == 4 ? - w->set_control_map(channel,controllers) : XTC_ERR_INVALID_ARGUMENT; - if (r != XTC_OK) w->report_error("Controller mapping rejected: check channel, unique CCs and reserved panic controllers",r); -} - -xtc_result MainWindow::install_plugin_graph(xtc_graph *graph) -{ - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_graph(candidate,graph); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); - else mark_dirty(); - return r; -} -xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *id) -{ - BusyOperation busy(*this,status_,"Loading plugin and preparing playback…"); - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); - xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - xtc_graph *graph = nullptr; size_t added = SIZE_MAX; - if (r == XTC_OK) r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph); - if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added); - xtc_plan *check = nullptr; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); - if (r == XTC_OK) graph_widget_->select_node(added); - if (r == XTC_OK && id) *id = added; - return r; -} -xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int index) -{ - BusyOperation busy(*this,status_,"Replacing plugin and preparing playback…"); - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - if (r == XTC_OK) r = apply_plugin_spec(id,spec,true); - if (r == XTC_OK) graph_widget_->select_node(id); - return r; -} -xtc_result MainWindow::reset_plugin(size_t id, unsigned int port) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec); - if (r == XTC_OK) r = xtc_plugin_reset(&spec,rate,port); - return r == XTC_OK ? apply_plugin_spec(id,spec) : r; -} -xtc_result MainWindow::insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - if (r == XTC_OK && !spec.inputs) r = XTC_ERR_UNSUPPORTED; - xtc_graph *graph = nullptr; size_t added = SIZE_MAX; - xtc_graph_edit edit{}; edit.kind = XTC_EDIT_DISCONNECT; edit.data.connection = wire; - if (r == XTC_OK) r = xtc_graph_apply_edit(xtc_project_graph(project_.get()),&edit,&graph,nullptr); - if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added); - if (r == XTC_OK) r = xtc_graph_connect_ports(graph,wire.source_node,wire.source_port,added,0); - if (r == XTC_OK) r = xtc_graph_connect_ports(graph,added,1,wire.destination_node,wire.destination_port); - xtc_plan *check = nullptr; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); - if (r == XTC_OK) { graph_widget_->select_node(added); if (id) *id = added; } - return r; -} -xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_plugin_spec spec{}; - xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec); - if (r != XTC_OK) return r; - size_t i; - for (i = 0; i < spec.control_count && spec.ports[i] != port; ++i) {} - if (i == spec.control_count) return XTC_ERR_NOT_FOUND; - spec.values[i] = value; - return apply_plugin_spec(id,spec); -} -xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - xtc_parameter_key key{XTC_PARAMETER_PLUGIN,static_cast(id),point.port}; - if (r == XTC_OK) r = xtc_project_automate(candidate,key,rate,point.tick,point.step,point.value,remove ? 1 : 0); - xtc_plugin_spec spec{}; - if (r == XTC_OK) r = xtc_graph_get_plugin(xtc_project_graph(candidate),id,&spec); - xtc_project_destroy(candidate); - return r == XTC_OK ? apply_plugin_spec(id,spec) : r; -} -xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace) -{ - xtc_graph *graph = nullptr; - xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph); - if (r == XTC_OK) r = replace ? xtc_graph_replace_plugin(graph,id,&spec) : xtc_graph_set_plugin(graph,id,&spec); - /* Compile before adoption even with a mocked/disconnected backend. */ - xtc_plan *check = nullptr; - xtc_backend_status status{}; double rate = sample_rate(); - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); return r; -} -void MainWindow::plugin_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before plugin changes",XTC_ERR_INVALID_STATE); return; } - auto *menu = static_cast(widget); - const char *label = menu->mvalue()->label(); - xtc_result r; - if(std::strcmp(label,"Insert stereo delay on wire")==0 || std::strcmp(label,"Insert stereo reverb on wire")==0) { - xtc_connection_info wire{}; - r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert stereo delay on wire")==0 ? 5U : 6U) : XTC_ERR_INVALID_STATE; - } else if(std::strcmp(label,"Add stereo delay")==0 || std::strcmp(label,"Add stereo reverb")==0) { - r=w->add_plugin(XTC_BUILTIN_PATH,std::strcmp(label,"Add stereo delay")==0 ? 5U : 6U); - } else if (std::strcmp(label,"Insert internal delay on wire")==0 || std::strcmp(label,"Insert internal reverb on wire")==0) { - xtc_connection_info wire{}; - r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert internal delay on wire")==0 ? 2U : 3U) : XTC_ERR_INVALID_STATE; - } else if (std::strcmp(label,"Add internal delay") == 0 || std::strcmp(label,"Add internal reverb") == 0) { - size_t id=0; r=w->add_plugin(XTC_BUILTIN_PATH, std::strcmp(label,"Add internal delay")==0 ? 2U : 3U, &id); - } else if (std::strcmp(label,"Reset selected plugin") == 0) { - r = w->reset_plugin(w->graph_widget_->selected_node()); - } else if (std::strcmp(label,"Load plugin") == 0 || - std::strcmp(label,"Replace selected plugin") == 0 || - std::strcmp(label,"Insert plugin on selected wire") == 0) { - const size_t selected = w->graph_widget_->selected_node(); - xtc_connection_info wire{}; - const bool insert = std::strcmp(label,"Insert plugin on selected wire") == 0; - const bool replace = std::strcmp(label,"Replace selected plugin") == 0; - if (insert && !w->graph_widget_->selected_connection(wire)) { - w->report_error("Select an existing wire before inserting an effect",XTC_ERR_INVALID_STATE); return; - } - if (replace) { - xtc_plugin_spec old{}; - r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),selected,&old); - if (r != XTC_OK) { w->report_error("Select a plugin node to replace",r); return; } - } - std::string module; if(!xtc_choose_plugin_file(w->settings_,module))return; - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - unsigned int index = 0; - xtc_plugin_spec probe{}; - r = xtc_plugin_probe(module.c_str(),0,rate,&probe); - if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) { - w->report_error("Cannot discover plugin; check binary path, format, mono layout and dependencies",r); return; - } - if (!xtc_choose_plugin(module.c_str(),rate,index)) return; - if (replace) r = w->replace_plugin(selected,module.c_str(),index); - else if (insert) r = w->insert_plugin(wire,module.c_str(),index); - else r = w->add_plugin(module.c_str(),index); - } else { - size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{}; - r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec); - if (r == XTC_OK) { - std::string prompt = std::string(spec.label) + " control ports and values:\n"; - if (std::strcmp(label,"Selected plugin control") == 0) { - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_details details{}; xtc_plugin_spec edited{}; - r = xtc_plugin_describe(&spec,rate,&details); - if (r == XTC_OK && xtc_edit_plugin(spec,details,edited)) r = w->apply_plugin_spec(id,edited); - if (r != XTC_OK) w->report_error("Plugin editor failed; configuration retained. Check module identity and control ranges",r); - return; - } - xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{}; - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - r = xtc_plugin_parameters(&spec,rate,info); - if (r != XTC_OK) { w->report_error("Read plugin controls failed",r); return; } - for (unsigned i = 0; i < spec.control_count; ++i) - prompt += std::to_string(spec.ports[i]) + ": " + info[i].name + " = " + std::to_string(spec.values[i]) + - " [" + std::to_string(info[i].minimum) + ", " + std::to_string(info[i].maximum) + "]\n"; - if (std::strcmp(label,"Selected plugin automation") == 0) { - prompt += "Enter port tick mode value; mode 0 linear, 1 step, -1 remove.\nCurrent tick: " + - std::to_string(w->sequencer_->position()); - const char *answer = ui_input("%s","",prompt.c_str()); - if (!answer) return; - unsigned port; unsigned long long tick; int mode; float value; char extra; - if (std::sscanf(answer,"%u %llu %d %f %c",&port,&tick,&mode,&value,&extra) != 4 || mode < -1 || mode > 1) - r = XTC_ERR_INVALID_ARGUMENT; - else { - xtc_plugin_point point{static_cast(tick),port,mode == 1 ? 1U : 0U,value}; - r = w->set_plugin_automation(id,point,mode == -1); - } - if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r); - return; - } - } - } - if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r); -} - -void MainWindow::project_defaults(xtc_project *project) -{ - (void)xtc_project_set_tempo(project,static_cast(settings_.tempo)); - unsigned int cc[3]={static_cast(settings_.volume_cc),static_cast(settings_.pan_cc),static_cast(settings_.sustain_cc)}; - for(unsigned int channel=0;channel<16;++channel)(void)xtc_project_set_control_map(project,channel,cc); -} -double MainWindow::sample_rate() const -{ - xtc_backend_status status{}; - return ready_ && xtc_backend_get_status(backend_,&status)==XTC_OK ? status.sample_rate : static_cast(settings_.offline_rate); -} -xtc_result MainWindow::configure_loop(const xtc_project *project,const xtc_settings &settings) -{ - if(!ready_)return XTC_OK; - uint64_t start,end;unsigned int rate=static_cast(sample_rate()); - xtc_result r=xtc_project_tick_to_sample(project,static_cast(settings.loop_start),rate,&start); - if(r==XTC_OK)r=xtc_project_tick_to_sample(project,static_cast(settings.loop_end),rate,&end); - if(r==XTC_OK)r=xtc_backend_set_loop(backend_,start,end,settings.loop_enabled!=0); - return r; -} -xtc_result MainWindow::set_loop(uint64_t start,uint64_t end,bool enabled) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - if(start>=end || end>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT; - xtc_settings candidate=settings_;candidate.loop_start=static_cast(start);candidate.loop_end=static_cast(end);candidate.loop_enabled=enabled; - bool resume=running_;xtc_result r=ready_ ? xtc_backend_stop(backend_) : XTC_OK; - if(r==XTC_OK)r=configure_loop(project_.get(),candidate); - if(r==XTC_OK) {settings_=candidate;sequencer_->loop_region(start,end,enabled);} - if(ready_ && resume && r==XTC_OK) {xtc_result restarted=start_configured_backend();if(r==XTC_OK)r=restarted;} - refresh_status();return r; -} -void MainWindow::collect_preferences() -{ - settings_.sequencer_detached=editors_[0].detached;settings_.graph_detached=editors_[1].detached;settings_.piano_detached=piano_detached_; - settings_.track_header_width=sequencer_->header_width(); - for(unsigned int i=0;i<2;++i)if(editors_[i].window) { - auto *p=editors_[i].window.get(); - if(i==0){settings_.sequencer_x=p->x();settings_.sequencer_y=p->y();settings_.sequencer_width=p->w();settings_.sequencer_height=p->h();} - else{settings_.graph_x=p->x();settings_.graph_y=p->y();settings_.graph_width=p->w();settings_.graph_height=p->h();} - } - if(piano_window_){settings_.piano_x=piano_window_->x();settings_.piano_y=piano_window_->y();} - settings_.transport_detached=transport_detached_;settings_.mixer_detached=mixer_detached_; - if(transport_window_) {settings_.transport_x=transport_window_->x();settings_.transport_y=transport_window_->y();settings_.transport_width=transport_window_->w();settings_.transport_height=transport_window_->h();} - if(mixer_window_) {settings_.mixer_x=mixer_window_->x();settings_.mixer_y=mixer_window_->y();settings_.mixer_width=mixer_window_->w();settings_.mixer_height=mixer_window_->h();} - settings_.main_x=x();settings_.main_y=y();settings_.inspector_visible=sidebar_->visible(); - settings_.width=w();settings_.height=h();settings_.sequencer_snap=static_cast(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level(); - if(piano_) { - settings_.piano_width=piano_window_->w();settings_.piano_height=piano_window_->h();settings_.piano_snap=static_cast(piano_->grid()); - settings_.filter_channel=piano_->filter_channel();settings_.filter_low=piano_->filter_low();settings_.filter_high=piano_->filter_high(); - settings_.note_ticks=static_cast(piano_->new_note_ticks);settings_.note_velocity=piano_->new_note_velocity;settings_.note_channel=piano_->new_note_channel; - } -} -void MainWindow::apply_ui_preferences() -{ - bool seq=settings_.sequencer_detached!=0,graph=settings_.graph_detached!=0,piano=settings_.piano_detached!=0; - detach_editor(0,seq);detach_editor(1,graph);detach_piano(piano); - sequencer_->header_width(static_cast(settings_.track_header_width)); - for(unsigned int i=0;i<2;++i)if(editors_[i].window)restore_panel(editors_[i].window.get(),static_cast(i==0 ? settings_.sequencer_x : settings_.graph_x),static_cast(i==0 ? settings_.sequencer_y : settings_.graph_y),static_cast(i==0 ? settings_.sequencer_width : settings_.graph_width),static_cast(i==0 ? settings_.sequencer_height : settings_.graph_height),i==0 ? 1100 : 1000,420); - bool floating_transport=settings_.transport_detached!=0,floating_mixer=settings_.mixer_detached!=0; - detach_transport(floating_transport);detach_mixer(floating_mixer); - if(transport_window_)restore_panel(transport_window_.get(),static_cast(settings_.transport_x),static_cast(settings_.transport_y),static_cast(settings_.transport_width),static_cast(settings_.transport_height),400,130); - if(mixer_window_)restore_panel(mixer_window_.get(),static_cast(settings_.mixer_x),static_cast(settings_.mixer_y),static_cast(settings_.mixer_width),static_cast(settings_.mixer_height),600,420); - follow_->value(static_cast(settings_.follow_playhead)); - restore_panel(this,static_cast(settings_.main_x),static_cast(settings_.main_y),static_cast(settings_.width),static_cast(settings_.height),1220,720); - show_track_inspector(settings_.inspector_visible!=0); - sequencer_->grid(static_cast(settings_.sequencer_snap));sequencer_->set_zoom(static_cast(settings_.sequencer_zoom)); - sequencer_->new_clip_ticks=static_cast(settings_.clip_ticks); - sequencer_->loop_region(static_cast(settings_.loop_start),static_cast(settings_.loop_end),settings_.loop_enabled!=0); - if(piano_) { - restore_panel(piano_window_.get(),static_cast(settings_.piano_x),static_cast(settings_.piano_y),static_cast(settings_.piano_width),static_cast(settings_.piano_height),800,520); - piano_->set_grid(static_cast(settings_.piano_snap));piano_->set_filter(static_cast(settings_.filter_channel),static_cast(settings_.filter_low),static_cast(settings_.filter_high)); - piano_->new_note_ticks=static_cast(settings_.note_ticks);piano_->new_note_velocity=static_cast(settings_.note_velocity);piano_->new_note_channel=static_cast(settings_.note_channel); - } -} -xtc_result MainWindow::apply_preferences(const xtc_settings &candidate) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_result r=xtc_settings_validate(&candidate);if(r!=XTC_OK)return r; - if(configuration_.empty())return XTC_ERR_INVALID_STATE; - if(ready_)r=xtc_backend_stop(backend_); - if(r==XTC_OK)r=configure_loop(project_.get(),candidate); - if(r==XTC_OK)r=xtc_settings_save(&candidate,configuration_.c_str()); - if(r!=XTC_OK) {if(ready_)(void)configure_loop(project_.get(),settings_);refresh_status();return r;} - settings_=candidate;configuration_valid_=true;apply_ui_preferences();refresh_status();return XTC_OK; -} -xtc_result MainWindow::save_preferences() -{ - if(configuration_.empty() || !configuration_valid_)return XTC_ERR_INVALID_STATE; - collect_preferences();return xtc_settings_save(&settings_,configuration_.c_str()); -} -std::vector MainWindow::midi_devices(bool input) -{ - std::vector ports; - if(ready_)(void)xtc_backend_visit_midi_ports(backend_,input ? XTC_PORT_OUTPUT : XTC_PORT_INPUT, - [](const char *name,void *c){static_cast *>(c)->emplace_back(name);return 0;},&ports); - return ports; -} -xtc_result MainWindow::start_configured_backend() -{ - midi_route_warning_.clear(); - const char *input=settings_.midi_input_port, *output=settings_.midi_output_port; - // Saved production devices are optional. Keep their names so a later start - // reconnects them, but do not ask the strict backend API to route absent ports. - auto available=[this](const char *name, xtc_port_direction direction, bool &found) { - struct Search { const char *name; bool found; } search{name,false}; - xtc_result result=xtc_backend_visit_midi_ports(backend_,direction, - [](const char *port,void *context) { - auto &search=*static_cast(context); - search.found=std::strcmp(port,search.name)==0; - return search.found ? 1 : 0; - },&search); - found=search.found; - return result; - }; - auto optional_route=[&](const char *&name,xtc_port_direction direction) { - if(!*name)return XTC_OK; - bool found=false; - xtc_result result=available(name,direction,found); - if(result==XTC_OK && !found) { - if(midi_route_warning_.empty())midi_route_warning_="MIDI device unavailable: "; - else midi_route_warning_+=", "; - midi_route_warning_+=name; - name=""; - } - return result; - }; - xtc_result r=xtc_backend_set_midi_options(backend_,static_cast(settings_.midi_input_channel),static_cast(settings_.midi_output_channel),settings_.midi_thru!=0); - if(r==XTC_OK)r=optional_route(input,XTC_PORT_OUTPUT); - if(r==XTC_OK)r=optional_route(output,XTC_PORT_INPUT); - if(r==XTC_OK)r=xtc_backend_start_routed(backend_,input,output); - if(r==XTC_OK) { - if(!midi_route_warning_.empty())midi_route_warning_+=". Playback continues; saved device choices are kept."; - set_label(error_,midi_route_warning_.c_str()); - } - return r; -} -void MainWindow::show_preferences() -{ - if(recorder_) {report_error("Stop recording before preferences",XTC_ERR_INVALID_STATE);return;} - collect_preferences();PreferencesDialog dialog(settings_,configuration_,[this](const xtc_settings &s){return apply_preferences(s);},[this](bool input){return midi_devices(input);});dialog.run(); -} - -xtc_result MainWindow::set_tempo(unsigned int milli_bpm) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - if (milli_bpm < 20000 || milli_bpm > 300000) - return XTC_ERR_INVALID_ARGUMENT; - if (milli_bpm == xtc_project_tempo(project_.get())) - return XTC_OK; - uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip(), mix = mixer_->selected_track(); - uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX, note = piano_ ? piano_->selected_note() : UINT64_MAX; - bool piano_visible = piano_window_ && piano_window_->shown(); - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(), &candidate); - if (r == XTC_OK) - r = xtc_project_set_tempo(candidate, milli_bpm); - if (r == XTC_OK) - r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) - r = install_project(candidate, false); - if (r != XTC_OK) - xtc_project_destroy(candidate); - else { - sequencer_->select(track, clip); - mixer_->select_track(mix); - xtc_clip_info info{}; - if (piano_ && xtc_project_find_clip(project_.get(), piano_clip, &info) == XTC_OK) { - piano_->set_project(project_.get(), piano_clip); - piano_->select_note(note); - if (piano_visible) - piano_window_->show(); - } - mark_dirty(); - } - return r; -} -void MainWindow::inspector_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - const uint64_t id = w->inspected_track_; - float gain = static_cast(w->inspector_gain_->value()), pan = static_cast(w->inspector_pan_->value()); - int mute = w->inspector_mute_->value(), solo = w->inspector_solo_->value(); - // Editing a single field must not commit another field's unfinished input. - size_t count = 0, clips = 0; - xtc_project_get_counts(w->project_.get(), &count, &clips); - for (size_t i = 0; i < count; ++i) { - xtc_track_info track{}; - xtc_project_get_track(w->project_.get(), i, &track); - if (track.id != id) - continue; - if (widget != w->inspector_gain_) - gain = track.gain; - if (widget != w->inspector_pan_) - pan = track.pan; - if (widget != w->inspector_mute_) - mute = track.mute; - if (widget != w->inspector_solo_) - solo = track.solo; - if (gain != track.gain || pan != track.pan || mute != track.mute || solo != track.solo) - if (w->mixer_->select_track(id)) - w->mixer_->mix(gain, pan, mute, solo); - break; - } - w->inspected_track_ = UINT64_MAX; - w->refresh_status(); -} -xtc_result MainWindow::mix_snapshot(unsigned int slot,bool capture) -{ - if(slot>=2)return XTC_ERR_INVALID_ARGUMENT; - if(capture) { - xtc_mix_snapshot *snapshot=nullptr;xtc_result r=xtc_mix_snapshot_create(project_.get(),&snapshot); - if(r==XTC_OK)mix_snapshots_[slot].reset(snapshot); - return r; - } - if(!mix_snapshots_[slot])return XTC_ERR_NOT_FOUND; - xtc_result r=before_arrangement_edit(this); - if(r==XTC_OK)r=xtc_mix_snapshot_recall(mix_snapshots_[slot].get(),project_.get()); - if(r==XTC_OK)mark_dirty(); - return r; -} -xtc_result MainWindow::navigate_marker(int direction) -{ - xtc_marker marker{};if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_result r=xtc_project_adjacent_marker(project_.get(),sequencer_->position(),direction,&marker); - if(r==XTC_OK && ready_)r=xtc_backend_stop(backend_); - if(r==XTC_OK)r=seek(this,marker.tick); - if(r==XTC_OK)sequencer_->position(marker.tick); - return r; -} -xtc_result MainWindow::loop_section() -{ - uint64_t begin=0,end=0;xtc_result r=xtc_project_section(project_.get(),sequencer_->position(),&begin,&end); - return r==XTC_OK ? set_loop(begin,end,true) : r; -} -xtc_result MainWindow::collect_project(const char *directory) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - BusyOperation busy(*this,status_,"Collecting project media…"); - return xtc_project_collect(project_.get(),directory); -} -xtc_result MainWindow::adopt_sample_copy(uint64_t id,const xtc_sample *sample,const char *path) -{ - if(!sample||!path)return XTC_ERR_INVALID_ARGUMENT; - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_clip_info clip{};r=xtc_project_find_clip(project_.get(),id,&clip);if(r!=XTC_OK)return r; - if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE; - xtc_sample *original=nullptr;r=xtc_sample_load(clip.asset_path,&original);if(r!=XTC_OK)return r; - unsigned int old_rate=0,new_rate=0;size_t old_frames=0,new_frames=0; - xtc_sample_info(original,&old_rate,&old_frames);xtc_sample_info(sample,&new_rate,&new_frames);xtc_sample_destroy(original); - if(old_rate!=new_rate||old_frames!=new_frames)return XTC_ERR_INVALID_ARGUMENT; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_clip_asset(candidate,id,path); - bool written=false; - if(r==XTC_OK){r=xtc_sample_write(sample,0,new_frames,path);written=r==XTC_OK;} - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){if(written)std::remove(path);xtc_project_destroy(candidate);return r;} - sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::slice_sample_clip(uint64_t id,unsigned int rate,const std::vector &positions) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;size_t count=0;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_slice_source_frames(candidate,id,rate,positions.data(),positions.size(),&count); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::trim_sample_clip(uint64_t id,unsigned int rate,size_t begin,size_t end) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_trim_source_frames(candidate,id,rate,begin,end); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::open_sample_editor() -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_clip_info clip{};auto r=xtc_project_find_clip(project_.get(),sequencer_->selected_clip(),&clip); - if(r!=XTC_OK)return r; - if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE; - xtc_sample *sample=nullptr;r=xtc_sample_load(clip.asset_path,&sample);if(r!=XTC_OK)return r; - try { - SampleEditor editor(SampleEditor::Data(sample,xtc_sample_destroy)); - uint64_t first=0,last=0;unsigned int rate;size_t frames;xtc_sample_info(sample,&rate,&frames); - if(xtc_project_tick_to_sample(project_.get(),clip.offset_tick,rate,&first)==XTC_OK && xtc_project_tick_to_sample(project_.get(),clip.offset_tick+clip.length_tick,rate,&last)==XTC_OK) - editor.select(static_cast(std::min(first,static_cast(frames))),static_cast(std::min(last,static_cast(frames)))); - editor.slice=[this,id=clip.id](unsigned int sr,const std::vector &positions){return slice_sample_clip(id,sr,positions);}; - editor.adopt=[this,id=clip.id](const xtc_sample *data,const char *path){return adopt_sample_copy(id,data,path);}; - editor.trim=[this,id=clip.id](unsigned int sr,size_t b,size_t e){return trim_sample_clip(id,sr,b,e);}; - editor.run();return XTC_OK; - } catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;} -} -void MainWindow::show_commands() -{ - std::vector commands; - for (int i = 0; i < menu_->size(); ++i) { - const auto &item = menu_->menu()[i]; - if (!item.label() || item.submenu() || !item.active()) - continue; - char path[512]; - if (menu_->item_pathname(path, sizeof(path), &item) == 0) - commands.emplace_back(path); - } - std::string selected; - { - CommandPalette palette(std::move(commands)); - selected = palette.run(); - } - if (!selected.empty()) { - auto *item = menu_->find_item(selected.c_str()); - if (item && item->active()) - menu_->picked(item); - } -} -void MainWindow::fit_song() -{ - size_t tracks = 0, clips = 0; - uint64_t end = 3840; - xtc_project_get_counts(project_.get(), &tracks, &clips); - for (size_t i = 0; i < clips; ++i) { - xtc_clip_info clip{}; - xtc_project_get_clip(project_.get(), i, &clip); - end = std::max(end, clip.start_tick + clip.length_tick + 960); - } - int scale = 24; - for (int candidate : {24, 48, 96, 192, 384}) - if (end * static_cast(candidate) / 960 <= - static_cast(std::max(1, timeline_scroll_->w() - 204))) - scale = candidate; - sequencer_->set_zoom(scale); - timeline_scroll_->scroll_to(0, 0); - timeline_scroll_->redraw(); -} -void MainWindow::follow_playhead() -{ - if (!running_ || !follow_->value() || tabs_->value() != tabs_->child(0) || Ui::pushed()) - return; - int pixel = static_cast(sequencer_->position() * static_cast(sequencer_->zoom_level()) / 960); - int screen = sequencer_->x() + sequencer_->header_width() + pixel; - if (screen > timeline_scroll_->x() + timeline_scroll_->w() - 48 || screen < timeline_scroll_->x() + sequencer_->header_width()) - timeline_scroll_->scroll_to(std::max(0, pixel - 160), timeline_scroll_->yposition()); -} - -xtc_result MainWindow::set_track_chain(uint64_t track,const xtc_track_chain &chain) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_chain(candidate,track,&chain); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - mark_dirty();track_inserts_->refresh(true);return XTC_OK; -} -xtc_result MainWindow::add_track_effect(uint64_t track,const char *file,unsigned int index) -{ - xtc_track_chain chain{};auto r=xtc_project_get_chain(project_.get(),track,&chain); - if(r!=XTC_OK)return r; - if(chain.count==XTC_TRACK_INSERTS)return XTC_ERR_CAPACITY; - xtc_track_insert slot{};r=xtc_plugin_probe(file,index,sample_rate(),&slot.plugin); - if(r!=XTC_OK)return r; - slot.enabled=1;slot.wet=1;chain.slots[chain.count++]=slot; - return set_track_chain(track,chain); -} - -xtc_result MainWindow::open_track_inserts(uint64_t track) -{ - if(track==UINT64_MAX) { - bool mixer_context=tabs_->value()==mixer_page_ || - (mixer_detached_ && Ui::focus() && Ui::focus()->window()==mixer_window_.get()); - track=mixer_context ? mixer_->selected_track() : sequencer_->selected_track(); - } - size_t count=0,clips=0;xtc_project_get_counts(project_.get(),&count,&clips); - for(size_t i=0;iselect_track(track);tabs_->value(track_inserts_);tabs_->redraw();return XTC_OK; - } - return XTC_ERR_NOT_FOUND; -} - -xtc_result MainWindow::tap_tempo(double seconds) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - auto state=tap_state_;unsigned int tempo=0;auto r=xtc_tap_tempo_push(&state,seconds,&tempo); - if(r==XTC_ERR_NOT_FOUND){tap_state_=state;return r;} - if(r!=XTC_OK)return r; - r=set_tempo(tempo);if(r==XTC_OK)tap_state_=state;return r; -} diff --git a/src/gui/JUCE/main_window.hpp b/src/gui/JUCE/main_window.hpp deleted file mode 100644 index a700c28..0000000 --- a/src/gui/JUCE/main_window.hpp +++ /dev/null @@ -1,557 +0,0 @@ -#ifndef XTC_MAIN_WINDOW_HPP -#define XTC_MAIN_WINDOW_HPP - -#include "ui.hpp" -#include -#include -#include -#include "xtc/xtc_project.h" -#include "xtc/xtc_sample.h" -#include "xtc/xtc_editing.h" -#include "xtc/xtc_backend.h" -#include "xtc/xtc_mixer.h" -#include "xtc/xtc_plugin.h" -#include "xtc/xtc_tempo.h" -#include "xtc/xtc_control.h" -#include "xtc/xtc_settings.h" - -class GraphWidget; -class MixerWidget; -class PianoRollWidget; -class SequencerWidget; -class UiBox; -class UiGroup; -class UiButton; -class UiTabs; -class UiMenu_Bar; -class UiChoice; -class UiValue_Slider; -class UiValue_Input; -class UiScroll; -class UiCheck_Button; - -class TrackInsertsPanel; -class MainWindow : public UiDouble_Window { - public: - xtc_result set_track_chain(uint64_t track,const xtc_track_chain &chain); - xtc_result add_track_effect(uint64_t track,const char *file,unsigned int index); - TrackInsertsPanel *track_inserts() const {return track_inserts_;} - void show_track_inspector(bool visible); - xtc_result open_track_inserts(uint64_t track=UINT64_MAX); - /* One undoable candidate edit. MIDI also adds a one-bar clip at the cursor. - * Preparation stops/resets transport; recording and failed candidates retain - * the current document. No engine/model changes happen in audio callbacks. */ - xtc_result create_track(bool midi); - xtc_result set_tempo(unsigned int milli_bpm); - xtc_result tap_tempo(double seconds); - xtc_result collect_project(const char *directory); - xtc_result navigate_marker(int direction); - xtc_result loop_section(); - void show_commands(); - xtc_result mix_snapshot(unsigned int slot,bool capture); - /* Reparent the existing controls on the UI thread; closing a panel docks it. */ - void detach_editor(unsigned int index,bool detached); /* 0 sequencer, 1 graph */ - void detach_piano(bool detached); - void reset_layout(); - UiDouble_Window *editor_window(unsigned int index) const {return index<2 ? editors_[index].window.get() : nullptr;} - void layout_transport(); - void resize(int x,int y,int width,int height) override; - void detach_transport(bool detached); - void detach_mixer(bool detached); - UiDouble_Window *transport_window() const { return transport_window_.get(); } - UiDouble_Window *mixer_window() const { return mixer_window_.get(); } - int panel_shortcut(int event); - - void fit_song(); - const xtc_settings &settings() const { return settings_; } - xtc_result apply_preferences(const xtc_settings &settings); - void show_preferences(); - std::vector midi_devices(bool input); - xtc_result start_configured_backend(); - xtc_result save_preferences(); - xtc_result set_loop(uint64_t start,uint64_t end,bool enabled); - UiWidget *velocity_lane() const { return velocity_lane_; } - /* Control-thread plugin commands, transactional model adoption. */ - xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index); - xtc_result adopt_sample_copy(uint64_t clip,const xtc_sample *sample,const char *path); - xtc_result slice_sample_clip(uint64_t clip,unsigned int rate,const std::vector &positions); - xtc_result trim_sample_clip(uint64_t clip,unsigned int rate,size_t begin,size_t end); - xtc_result open_sample_editor(); - void export_audio(); - xtc_result export_midi_clip(const char *path); - void export_midi_dialog(); - xtc_result import_midi_file(const char *path); - void import_midi_dialog(); - xtc_result load_sampler(uint64_t track, const char *path); - xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec); - xtc_result select_live_instrument(uint64_t track); - static void instrument_callback(UiWidget *, void *); - xtc_result set_control_map(unsigned int channel, const unsigned int controllers[3]); - xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr); - xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false); - xtc_result replace_plugin(size_t id, const char *path, unsigned int index); - xtc_result reset_plugin(size_t id, unsigned int port = UINT32_MAX); - xtc_result insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id = nullptr); - xtc_result set_plugin_control(size_t id, unsigned int port, float value); - /** - * @brief Constructs the window, its widgets, and the initial project. - * - * Creates an empty project and undo history, opens the audio backend, and - * schedules the periodic status timer. Throws std::runtime_error when the - * initial project or history cannot be created. - * - * @param prompt_unsaved Whether open, new, and close actions ask before - * discarding unsaved changes. - */ - explicit MainWindow(bool prompt_unsaved = true, const char *configuration = nullptr); - /** - * @brief Removes the status timer and releases backend and recording state. - * - * A backend that fails to close is intentionally retained with its recorder - * until process exit so active callbacks never observe freed state. - */ - ~MainWindow() override; - /** - * @brief Deleted copy constructor; a window uniquely owns its project and backend. - */ - MainWindow(const MainWindow &) = delete; - /** - * @brief Deleted copy assignment; a window uniquely owns its project and backend. - */ - MainWindow &operator=(const MainWindow &) = delete; - using UiDouble_Window::show; - /** - * @brief Handles JUCE events for the window. - * - * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. - * - * @param event JUCE event code, such as UI_SHORTCUT. - * @return 1 when Space was consumed; otherwise the UiDouble_Window result. - */ - int handle(int event) override; - - /** - * @brief Applies one undo or redo step from the project history. - * - * The selected snapshot is installed without resetting history and then - * accepted, leaving the window marked dirty. - * - * @param redo True to redo, false to undo. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, - * XTC_ERR_NOT_FOUND when no step is available, or another error from - * checkout, install, or accept. - */ - xtc_result undo(bool redo = false); - - /** - * @brief Returns the mixer tab widget owned by the window. - * - * @return Borrowed mixer widget, or nullptr before construction completes. - */ - MixerWidget *mixer() const - { - return mixer_; - } - - /** - * @brief Shows the piano roll for the selected MIDI clip. - * - * The piano-roll window is created on first use and reused afterwards. - * - * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip - * does not exist or is not MIDI. - */ - xtc_result open_piano_roll(); - - /** - * @brief Returns the piano-roll editor widget. - * - * @return Borrowed widget, or nullptr until open_piano_roll() first succeeds. - */ - PianoRollWidget *piano_roll() const - { - return piano_; - } - - /** - * @brief Imports a WAV file as an audio clip in a cloned project. - * - * Stops the engine first, creates an "Audio" track when none is selected, - * and places the clip at the sequencer cursor. The clone replaces the live - * project only after every step succeeds. - * - * @param path Path to the WAV file to import. - * @return XTC_OK on success, or an error from the engine stop, clone, import, - * or backend preparation. - */ - xtc_result import_audio(const char *path); - /** - * @brief Starts recording JACK input to a WAV file. - * - * Requires a connected backend and no active recording. The engine is - * stopped, the current position and selected track are remembered, and a - * stereo recorder at the engine sample rate is attached before starting. - * Failure leaves the window without a recorder. - * - * @param path Destination WAV path chosen by the user. - * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when - * disconnected or already recording, or another backend or recorder - * error. - */ - xtc_result record_audio(const char *path); - /** - * @brief Saves the current project to a path and updates the window title. - * - * On success the path is remembered and the unsaved marker is cleared. - * - * @param path Destination project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, - * XTC_ERR_INVALID_STATE while recording, or the save error. - */ - xtc_result save_project(const char *path); - /** - * @brief Loads a project from a path and makes it the current project. - * - * The file is loaded into a candidate project and installed only on success. - * - * @param path Source project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an - * error from loading or installing the project. - */ - xtc_result open_project(const char *path); - - /** Creates a fresh untitled project; the File menu checks unsaved changes first. - * On failure the current document remains owned by the window. */ - xtc_result new_project(); - /** Requests a playback fade/stop; the timer completes deactivation after - * callback acknowledgement. Recording must use immediate Stop instead. - * Returns request acceptance or the backend failure. */ - xtc_result pause_playback(); - /** Stops playback and seeks to zero. Refuses during recording. A failed - * stop/seek is reported without moving the displayed cursor to zero. */ - xtc_result return_to_start(); - - private: - friend class TrackInsertsPanel; - xtc_tap_tempo tap_state_{}; - TrackInsertsPanel *track_inserts_=nullptr; - using MixSnapshot=std::unique_ptr; - MixSnapshot mix_snapshots_[2]{{nullptr,xtc_mix_snapshot_destroy},{nullptr,xtc_mix_snapshot_destroy}}; - struct EditorPanel { - MainWindow *owner=nullptr;unsigned int index=0; - UiGroup *page=nullptr,*content=nullptr,*placeholder=nullptr; - std::unique_ptr window;bool detached=false; - }; - EditorPanel editors_[2]; - UiGroup *piano_page_=nullptr,*piano_content_=nullptr,*piano_placeholder_=nullptr; - bool piano_detached_=true; - std::unique_ptr transport_window_, mixer_window_; - UiGroup *transport_group_=nullptr, *mixer_page_=nullptr, *sidebar_=nullptr; - UiGroup *mixer_placeholder_=nullptr; - struct TransportItem {UiWidget *widget;int x,y,w,h;}; - std::vector transport_items_; - bool layout_transport_busy_=false; - bool transport_detached_=false, mixer_detached_=false; - xtc_settings settings_{}; - std::string configuration_; - std::string midi_route_warning_; - bool configuration_valid_=true; - UiCheck_Button *loop_button_=nullptr; - xtc_result configure_loop(const xtc_project *project,const xtc_settings &settings); - void apply_ui_preferences(); - void collect_preferences(); - void project_defaults(xtc_project *project); - double sample_rate() const; - void draw() override; - void refresh_workspace(); - UiChoice *snap_ = nullptr, *piano_snap_ = nullptr; - UiValue_Slider *note_velocity_ = nullptr; - UiWidget *velocity_lane_ = nullptr; - UiBox *transport_position_ = nullptr; - UiValue_Input *tempo_display_ = nullptr; - UiValue_Input *inspector_gain_=nullptr,*inspector_pan_=nullptr; - UiCheck_Button *inspector_mute_=nullptr,*inspector_solo_=nullptr,*follow_=nullptr; - UiScroll *timeline_scroll_=nullptr; - UiBox *session_info_=nullptr; - uint64_t inspected_track_=UINT64_MAX; - static void inspector_callback(UiWidget *,void *); - void follow_playhead(); - UiBox *selection_info_ = nullptr; - MixerWidget *mixer_ = nullptr; - UiMenu_Bar *menu_ = nullptr; - void update_history_controls(); - std::unique_ptr history_{nullptr, xtc_history_destroy}; - /** - * @brief JUCE menu callback for the Undo and Redo menu items. - * - * The selected menu label starting with "Redo" chooses redo; all other - * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. - * - * @param widget UiMenu_Bar whose selected item triggered the callback. - * @param context MainWindow * owning the menu and history. - */ - static void history_callback(UiWidget * widget, void * context); - std::unique_ptr project_{nullptr, xtc_project_destroy}; - std::unique_ptr piano_window_; - PianoRollWidget *piano_ = nullptr; - /** - * @brief JUCE callback for the Piano roll menu item and button. - * - * Refuses to open the editor while recording and asks the user to select a - * MIDI clip when open_piano_roll() fails. - * - * @param widget Unused triggering widget. - * @param context MainWindow * owning the piano roll. - */ - static void piano_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Panic menu item and button. - * - * When the backend is connected, sends a panic request that silences held - * instrument notes and reports failures. - * - * @param widget Unused triggering widget. - * @param context MainWindow * owning the backend. - */ - static void panic_callback(UiWidget * widget, void *context); - SequencerWidget *sequencer_ = nullptr; - xtc_recorder *recorder_ = nullptr; - std::string recording_path_; - uint64_t recording_tick_ = 0, recording_track_ = UINT64_MAX; - /** - * @brief Finalizes the active recording and adds the take to the project. - * - * Must run only after the engine has stopped, so no audio callback can - * reference the recorder ring. The recorder is detached, finished, and - * destroyed. A failed finalize or empty take is reported and not added. If - * the subsequent import fails, the saved WAV path is reported for recovery. - */ - void finish_recording(); - /** - * @brief Hook called by arrangement widgets before mutating the project. - * - * Refuses edits while recording. When connected, stops the engine so the - * mutation cannot race the audio thread, then refreshes the status display. - * - * @param context MainWindow * passed as the widget owner context. - * @return XTC_OK when disconnected or the stop succeeded, - * XTC_ERR_INVALID_STATE while recording, or the backend stop error. - */ - static xtc_result before_arrangement_edit(void *context); - /** - * @brief JUCE callback for the audio import, record, and relink commands. - * - * The triggering widget supplies the menu label; the Record button uses the - * "Record WAV" default. A native file chooser selects the file, after which - * the selected clip is relinked, recording starts, or a WAV is imported. - * Failures are reported in the window. - * - * @param widget Triggering UiMenu_Bar or UiButton; menu labels select the - * action. - * @param context MainWindow * owning the project and sequencer. - */ - static void audio_file_callback(UiWidget * widget, void *context); - bool dirty_ = false, prompt_unsaved_ = true; - std::string path_; - /** - * @brief Asks whether unsaved changes may be discarded. - * - * @return True when the project is clean, prompting is disabled, or the user - * chose Discard; false when the user cancels. - */ - bool discard_changes(); - /** - * @brief Records the current project in history and marks the window dirty. - * - * Dependent widgets are refreshed with the current project and graph, and - * the title gains the unsaved marker. A history failure is reported. - */ - void mark_dirty(); - /** - * @brief Installs a candidate project as the active project. - * - * Rejected while recording. Optionally builds fresh undo history, stops and - * prepares the connected backend, or validates offline audio and MIDI plans - * when disconnected. On success the window takes ownership of candidate, - * rebinds widgets, resets the graph layout, clears the dirty flag, and shows - * any missing-asset warning. On failure ownership stays with the caller. - * - * @param candidate Project to install; must be non-null. - * @param reset_history True to discard existing undo/redo history and start - * from candidate. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from history creation, the backend, or offline planning. - */ - xtc_result install_project(xtc_project *candidate, bool reset_history = true); - /** - * @brief Hook used by the sequencer to seek the audio engine. - * - * Does nothing when the backend is disconnected. Otherwise reads the engine - * sample rate, converts the tick position to samples, and seeks the backend. - * - * @param context MainWindow * passed as the sequencer owner context. - * @param tick Target timeline position in project ticks. - * @return XTC_OK when disconnected or the seek succeeded, or the status, - * conversion, or backend seek error. - */ - static xtc_result seek(void *context, uint64_t tick); - /** - * @brief JUCE menu callback for the File/New, Open, Save, and Save As items. - * - * Reads the action from the selected menu label, confirms discarding unsaved - * changes for New and Open, and uses a native file chooser unless Save can - * reuse the remembered path. Refuses file operations while recording and - * reports failures in the window. - * - * @param widget Triggering UiMenu_Bar whose selected label names the action. - * @param context MainWindow * owning the project and file path. - */ - static void control_map_callback(UiWidget *widget, void *context); - static void plugin_callback(UiWidget *widget, void *context); - xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false); - xtc_result install_plugin_graph(xtc_graph *graph); - static void file_callback(UiWidget * widget, void *context); - GraphWidget *graph_widget_ = nullptr; - UiBox *graph_state_ = nullptr; - xtc_backend *backend_ = nullptr; - std::string project_issue_; - bool ready_ = false; - bool running_ = false; - bool closing_ = false; - UiButton *record_ = nullptr; - UiButton *play_ = nullptr; - UiButton *stop_ = nullptr; - UiButton *pause_ = nullptr; - UiButton *home_ = nullptr; - UiBox *indicator_ = nullptr; - UiBox *status_ = nullptr; - UiBox *error_ = nullptr; - UiTabs *tabs_ = nullptr; - - /** - * @brief Edit handler passed to the modular graph widget. - * - * Applies the edit to a candidate graph, clones the project around it, and - * submits the new graph to the engine when connected. The live project is - * replaced only after every step succeeds. - * - * @param context MainWindow * passed as the graph widget owner context. - * @param edit Graph edit to apply. - * @param out_added_id Optional output receiving the new node ID when the edit - * adds a node; untouched otherwise or when nullptr. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from the graph, project, or backend operation. - */ - static xtc_result edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id); - /** - * @brief Opens the JACK backend and submits the current graph. - * - * Uses the default backend configuration and leaves the engine stopped. - * Failure is reported and leaves the backend disconnected; a handle that - * opened but failed to accept the graph is retained for reconnect. - */ - void open_backend(); - /** - * @brief Starts the audio engine with the current project. - * - * Does nothing when already running. Reports an error when disconnected. - * Otherwise prepares the project and starts the engine. Missing or - * unreadable audio is reported as a warning after a successful start. - */ - void start_engine(); - /** - * @brief Requests a graceful playback stop, or immediately finalizes recording. - * - * Does nothing when disconnected. Failures are reported in the window. - */ - void stop_engine(); - /** - * @brief Closes and reopens the JACK backend, leaving the engine stopped. - * - * Refuses to run while recording. If closing fails the existing handle is - * retained for a later retry and the failure is reported. - */ - void reconnect_engine(); - /** - * @brief Polls the backend and updates labels, meters, and button states. - * - * When disconnected, shows offline text and disables transport controls. - * Otherwise updates the sequencer position, mixer peaks, status line, and - * error indicators from the latest engine status, then collects backend - * work. - */ - void refresh_status(); - /** - * @brief Attempts to close the window after discarding unsaved changes. - * - * Refuses while recording. On success closes the backend, removes the status - * timer, hides the piano-roll window, and hides this window. - * - * @return True when the window closed; false when recording is active, the - * user cancels discarding, or the backend cannot close. - */ - bool close_window(); - /** - * @brief Shows an operation failure and a context-sensitive hint. - * - * @param operation Human-readable operation name; names containing - * "Project" select a file and permission hint. - * @param result Failing result code whose name is included in the message. - */ - void report_error(const char *operation, xtc_result result); - /** - * @brief JUCE callback for the Play menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose engine is started. - */ - static void play_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Stop menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose engine is stopped. - */ - static void stop_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Reconnect menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose backend is reopened. - */ - static void reconnect_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the window close box, File/Quit, and close events. - * - * @param widget Unused triggering widget or window. - * @param context MainWindow * whose close is requested; a refused close - * simply leaves the window open. - */ - static void close_callback(UiWidget * widget, void *context); - /** - * @brief JUCE menu callback that selects a tab page in the main window. - * - * @param widget Triggering menu item; window() must return the MainWindow. - * @param view UiWidget * tab page to make current, not the MainWindow. - */ - static void view_callback(UiWidget *widget, void *view); - /** - * @brief JUCE help callback that shows the application name and version. - * - * @param widget Unused triggering widget. - * @param context Unused callback context. - */ - static void about_callback(UiWidget * widget, void * context); - /** - * @brief JUCE timeout callback that refreshes status and supervises recording. - * - * While recording, drains the recorder and stops the engine if draining - * fails or the backend reports a lost server, sample-rate change, or - * processing error. Reschedules itself every 0.1 seconds until closing. - * - * @param context MainWindow * registered with Ui::add_timeout(). - */ - static void timer_callback(void *context); -}; - -#endif diff --git a/src/gui/JUCE/mixer_widget.cpp b/src/gui/JUCE/mixer_widget.cpp deleted file mode 100644 index 195d0bc..0000000 --- a/src/gui/JUCE/mixer_widget.cpp +++ /dev/null @@ -1,856 +0,0 @@ -#include "theme.hpp" -#include "mixer_widget.hpp" -#include "xtc/xtc_control.h" -#include "ui.hpp" -#include -#include -#include -#include -#include - -namespace { -constexpr uint64_t lane_ticks = 15360; -} - -class AutomationLane : public UiWidget { - MixerWidget &mixer_; - - public: - /** - * @brief Creates an automation lane bound to its owning mixer. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Lane width in pixels; tick math requires a value greater than 1. - * @param h Lane height in pixels. - * @param m Mixer that owns the lane; stored by reference and must outlive it. - */ - AutomationLane(int x, int y, int w, int h, MixerWidget &m) : UiWidget(x, y, w, h), mixer_(m) - { - } - - /** - * @brief Draws the lane grid, automation curve, points, and empty hint. - * - * Points for the selected track and parameter are sorted by tick, rendered - * as an interpolated curve, and marked within the visible tick range. - */ - void draw() override - { - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(32, 35, 40)); - ui_rectf(x(), y(), w(), h()); - ui_font(UI_HELVETICA, 11); - for (int i = 0; i <= 16; ++i) { - int px = x() + i * (w() - 1) / 16; - ui_color(ui_rgb_color(i % 4 ? 55 : 95, 60, 65)); - ui_line(px, y(), px, y() + h()); - if (i % 4 == 0) { - char label[32]; - std::snprintf(label, sizeof(label), "%llu", - static_cast(mixer_.lane_start() + static_cast(i) * 960)); - ui_color(UI_WHITE); - ui_draw(label, px + 3, y() + 14); - } - } - size_t count = 0; - xtc_project_automation_count(mixer_.project(), &count); - std::vector points; - for (size_t i = 0; i < count; ++i) { - xtc_automation_point a{}; - xtc_project_get_automation(mixer_.project(), i, &a); - if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter()) - points.push_back(a); - } - std::sort(points.begin(), points.end(), [](const auto &a, const auto &b) { - return a.tick < b.tick; - }); - float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; - float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; - auto py = [&](float v) { - return y() + h() - 6 - static_cast((v - min) / range * (h() - 28)); - }; - auto value = [&](uint64_t tick) { - if (points.empty()) - return min; - auto it = std::upper_bound(points.begin(), points.end(), tick, [](uint64_t t, const auto &a) { - return t < a.tick; - }); - if (it == points.begin()) - return it->value; - if (it == points.end()) - return points.back().value; - const auto &a = *(it - 1); - if (a.step) return a.value; - return a.value + - static_cast(static_cast(tick - a.tick) / static_cast(it->tick - a.tick)) * - (it->value - a.value); - }; - ui_color(xtc_ui::accent()); - if (!points.empty()) - for (int px = 1; px < w(); ++px) { - uint64_t a = - mixer_.lane_start() + static_cast(px - 1) * lane_ticks / static_cast(w() - 1); - uint64_t b = - mixer_.lane_start() + static_cast(px) * lane_ticks / static_cast(w() - 1); - ui_line(x() + px - 1, py(value(a)), x() + px, py(value(b))); - } - for (const auto &a : points) - if (a.tick >= mixer_.lane_start() && a.tick <= mixer_.lane_start() + lane_ticks) { - int px = x() + - static_cast((a.tick - mixer_.lane_start()) * static_cast(w() - 1) / lane_ticks); - ui_rectf(px - 4, py(a.value) - 4, 8, 8); - } - if (points.empty()) { - ui_color(UI_WHITE); - ui_draw("Click to add automation; right-click a point to remove. Values replace the " - "static control.", - x() + 12, y() + h() / 2); - } - ui_pop_clip(); - } - - /** - * @brief Handles JUCE input events and delegates unhandled events to the base widget. - * - * @param event JUCE event code. - * @return Nonzero if the event was handled, zero otherwise. - */ - int handle(int event) override - { - if (event != UI_PUSH) - return UiWidget::handle(event); - if (Ui::event_button() != UI_LEFT_MOUSE && Ui::event_button() != UI_RIGHT_MOUSE) - return 0; - uint64_t tick = mixer_.lane_start() + static_cast(std::clamp(Ui::event_x() - x(), 0, w() - 1)) * - lane_ticks / static_cast(w() - 1); - tick = (tick + 120) / 240 * 240; - tick = std::min(tick, XTC_PROJECT_MAX_TICK); - float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; - float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; - float value = min + static_cast(std::clamp(y() + h() - 6 - Ui::event_y(), 0, h() - 28)) / - static_cast(h() - 28) * range; - if (Ui::event_button() == UI_RIGHT_MOUSE) { - size_t count = 0; - xtc_project_automation_count(mixer_.project(), &count); - uint64_t distance = static_cast(8) * lane_ticks / static_cast(w() - 1) + 1, - closest = UINT64_MAX; - for (size_t i = 0; i < count; ++i) { - xtc_automation_point a{}; - xtc_project_get_automation(mixer_.project(), i, &a); - uint64_t d = a.tick > tick ? a.tick - tick : tick - a.tick; - if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() && d < distance) { - distance = d; - closest = a.tick; - } - } - if (closest != UINT64_MAX) - mixer_.point(closest, 0, true); - } else - mixer_.point(tick, value); - return 1; - } -}; - -/* UiMenu_::add merges duplicate labels and interprets menu paths. Append a - * unique temporary entry, then replace its text literally so IDs stay dense. */ -static int literal_item(UiChoice *choice, const char *label) -{ - std::string key="#"; - while (choice->find_item(key.c_str())) key+="#"; - int index=choice->add(key.c_str()); - choice->replace(index,label); - return index; -} - -class VerticalPeak : public UiProgress -{ - public: - VerticalPeak(int x, int y, int w, int h, const char *label) : UiProgress(x, y, w, h, label) - { - labeltype(UI_NO_LABEL); - } - void draw() override - { - ui_color(xtc_ui::background()); - ui_rectf(x(), y(), w(), h()); - int height = static_cast((std::clamp(value(), -90.0f, 6.0f) + 90) / 96 * (h() - 2)); - ui_color(selection_color()); - ui_rectf(x() + 1, y() + h() - 1 - height, w() - 2, height); - ui_color(xtc_ui::border()); - ui_rect(x(), y(), w(), h()); - } -}; - -/* A flat pan dial: no embossed face, bevels or simulated lighting. */ -class PanKnob : public UiDial -{ - bool centering_ = false; - - public: - PanKnob(int x, int y, int w, int h) : UiDial(x, y, w, h, "Pan") - { - bounds(-1, 1); - step(.01); - type(UI_LINE_DIAL); - when(UI_WHEN_RELEASE); - tooltip("Pan: -1 left, 0 center, +1 right. Double-click to center."); - } - -}; - -/* JUCE owns these children. Identity is a model ID, never a dense track index. */ -class MixerStrip : public UiGroup -{ - MixerWidget &owner_; - UiButton *name_; - UiCheck_Button *mute_, *solo_; - PanKnob *pan_; - UiValue_Slider *gain_; - UiChoice *route_; - UiBox *pan_text_, *readout_; - std::vector destinations_; - static void control(UiWidget *widget, void *context) - { - auto *s = static_cast(context); - // Copy values before the owner refreshes dependent widgets. - const auto id = s->id; - xtc_track_info track{}; - size_t count = 0, clips = 0; - xtc_project_get_counts(s->owner_.project(), &count, &clips); - bool found = false; - for (size_t i = 0; i < count; ++i) { - xtc_project_get_track(s->owner_.project(), i, &track); - if (track.id == id) { - found = true; - break; - } - } - if (!found) - return; - const float gain = - widget == s->gain_ - ? (s->gain_->value() <= -60 ? 0.0f - : std::min(4.0f, static_cast(std::pow(10, s->gain_->value() / 20)))) - : track.gain; - const float pan = widget == s->pan_ ? static_cast(s->pan_->value()) : track.pan; - const int mute = widget == s->mute_ ? s->mute_->value() : track.mute; - const int solo = widget == s->solo_ ? s->solo_->value() : track.solo; - auto &owner = s->owner_; - if (owner.select_track(id)) - owner.mix(gain, pan, mute, solo); - } - - public: - uint64_t id; - xtc_peak_meter meter[2]{}; - MixerStrip(MixerWidget &owner, uint64_t track) : UiGroup(0, 0, 148, 350), owner_(owner), id(track) - { - box(xtc_ui::flat); - color(xtc_ui::panel()); - begin(); - name_ = new UiButton(0, 0, 120, 26); - name_->callback( - [](UiWidget *, void *c) { - auto *s = static_cast(c); - s->owner_.select_track(s->id); - }, - this); - mute_ = new UiCheck_Button(0, 0, 64, 24, "Mute"); - solo_ = new UiCheck_Button(0, 0, 64, 24, "Solo"); - pan_ = new PanKnob(0, 0, 48, 48); - pan_text_ = new UiBox(0, 0, 124, 18); - route_ = new UiChoice(0, 0, 124, 24); - route_->tooltip("Output route: Master or project bus"); - route_->callback( - [](UiWidget *, void *c) { - auto *s = static_cast(c); - int i = s->route_->value(); - if (i < 0 || static_cast(i) >= s->destinations_.size()) - return; - auto bus = s->destinations_[static_cast(i)]; - auto &owner = s->owner_; - auto id = s->id; - if (owner.select_track(id)) - owner.route(bus); - }, - this); - gain_ = new UiValue_Slider(0, 0, 42, 150, "dB"); - gain_->align(UI_ALIGN_TOP); - gain_->labelsize(10); - gain_->type(UI_VERT_SLIDER); - gain_->slider(UI_FLAT_BOX); - gain_->slider_size(.10); - gain_->bounds(20 * std::log10(4.0), -60); - gain_->step(.1); - gain_->when(UI_WHEN_RELEASE); - gain_->tooltip( - "Level in dB: bottom is silence, 0 dB is unity, top is +12.04 dB. Automation overrides this static value."); - for (auto *w : {static_cast(gain_), static_cast(pan_), - static_cast(mute_), static_cast(solo_)}) - w->callback(control, this); - readout_ = new UiBox(0, 0, 132, 24); - readout_->labelsize(10); - end(); - xtc_ui::style(this); - pan_->label(""); - reset(); - resize(0, 0, 148, 350); - } - void reset() - { - xtc_peak_meter_reset(meter); - xtc_peak_meter_reset(meter + 1); - } - void sync(const xtc_track_info &track) - { - name_->copy_label((std::string(track.is_bus ? "Bus: " : "") + track.name).c_str()); - name_->tooltip(track.is_bus ? "Bus — select for automation" : "Track — select for automation"); - name_->color(owner_.selected_track() == id ? xtc_ui::track(id) : xtc_ui::panel()); - gain_->value(track.gain > 0 ? std::max(-60.0, 20 * std::log10(static_cast(track.gain))) : -60); - pan_->value(track.pan); - mute_->value(track.mute); - solo_->value(track.solo); - char text[64]; - std::snprintf(text, sizeof(text), "Pan %s %d%%", - track.pan < 0 ? "L" - : track.pan > 0 ? "R" - : "C", - static_cast(std::fabs(track.pan) * 100)); - pan_text_->copy_label(text); - route_->clear(); - destinations_.clear(); - destinations_.push_back(UINT64_MAX); - literal_item(route_,"Master"); - route_->value(0); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(owner_.project(), &tracks, &clips); - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info bus{}; - xtc_project_get_track(owner_.project(), i, &bus); - if (!bus.is_bus) - continue; - // Literal labels, including JUCE menu metacharacters. - int index = literal_item(route_,(std::string("Bus: ")+bus.name).c_str()); - destinations_.push_back(bus.id); - if (bus.id == track.bus_id) - route_->value(index); - } - if (track.is_bus) - route_->deactivate(); - else - route_->activate(); - redraw(); - } - void update(float left, float right, double elapsed) - { - xtc_peak_meter_update(meter, left, elapsed); - xtc_peak_meter_update(meter + 1, right, elapsed); - char text[96]; - std::snprintf(text, sizeof(text), "L %.1f R %.1f dBFS%s", static_cast(meter[0].db), - static_cast(meter[1].db), meter[0].clipped || meter[1].clipped ? " CLIP" : ""); - readout_->copy_label(text); - readout_->labelcolor(meter[0].clipped || meter[1].clipped ? UI_RED : xtc_ui::muted()); - redraw(); - } - void resize(int x, int y, int w, int h) override - { - UiWidget::resize(x, y, w, h); - name_->resize(x + 8, y + 8, w - 16, 26); - mute_->resize(x + 6, y + 42, 50, 24); - solo_->resize(x + 56, y + 42, 50, 24); - pan_->resize(x + 105, y + 38, 38, 38); - pan_text_->resize(x + 8, y + 78, w - 16, 18); - route_->resize(x + 8, y + 104, w - 16, 24); - gain_->resize(x + 10, y + 150, 44, h - 184); - readout_->resize(x + 4, y + h - 30, w - 8, 24); - redraw(); - } - void draw() override - { - UiGroup::draw(); - ui_color(xtc_ui::track(id)); - ui_rectf(x() + 1, y() + 1, w() - 2, 3); - ui_font(UI_HELVETICA, 10); - for (int db : {12, 0, -12, -24, -48, -60}) { - char text[12]; - std::snprintf(text, sizeof(text), "%d", db); - if (h() < 300 && db != 0 && db != -60) - continue; - int py = y() + 175 + static_cast((12.0412 - db) / 72.0412 * (h() - 212)); - ui_color(xtc_ui::muted()); - ui_draw(text, x() + 112, py); - } - int top = y() + 150, height = h() - 184; - for (int side = 0; side < 2; ++side) { - int px = x() + 70 + side * 15; - ui_color(xtc_ui::background()); - ui_rectf(px, top, 11, height); - const float db = std::clamp(meter[side].db, -60.0f, 6.0f); - int pixels = static_cast((db + 60) / 66 * height); - ui_color(meter[side].clipped ? UI_RED : xtc_ui::accent()); - ui_rectf(px, top + height - pixels, 11, pixels); - ui_color(xtc_ui::border()); - ui_rect(px, top, 11, height); - ui_color(meter[side].clipped ? UI_RED : xtc_ui::border()); - ui_rectf(px, top - 6, 11, 3); - } - } -}; - -/** - * @brief Creates the mixer panel and its child controls. - * - * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output - * meters, automation controls, and lane, then displays the given project. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Widget width in pixels. - * @param h Widget height in pixels. - * @param p Project to display; borrowed and must outlive its use. - * @param before Callback invoked before each edit; may be nullptr. - * @param context Opaque pointer forwarded to before. - */ -MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before, void *context) - : UiGroup(x, y, w, h), project_(p), before_(before), context_(context) -{ - begin(); - tracks_ = new UiChoice(x + 70, y + 8, 240, 26, "Track"); - tracks_->callback(select, this); - routes_ = new UiChoice(x + 600, y + 8, 180, 26, "Route"); - routes_->callback( - [](UiWidget *, void *c) { - auto *m = static_cast(c); - int index = m->routes_->value(); - if (index >= 0 && static_cast(index) < m->route_ids_.size()) - m->route(m->route_ids_[static_cast(index)]); - }, - this); - bus_ = new UiButton(x + 820, y + 8, 110, 26, "Add bus"); - bus_->callback([](UiWidget *, void *c) { static_cast(c)->add_bus(); }, this); - left_ = new VerticalPeak(x + 70, y + 90, 320, 20, "Output L"); - right_ = new VerticalPeak(x + 480, y + 90, 320, 20, "Output R"); - for (auto *meter : {left_, right_}) { - meter->minimum(-90); - meter->maximum(6); - } - reset_ = new UiButton(x, y, 120, 26, "Reset peaks"); - reset_->callback([](UiWidget *, void *c) { static_cast(c)->reset_meters(); }, this); - bank_ = new UiScroll(x, y + 40, w - 154, h - 172); - bank_->type(UiScroll::HORIZONTAL_ALWAYS); - bank_->end(); - master_panel_ = new UiGroup(x + w - 144, y + 40, 144, h - 172); - master_panel_->box(xtc_ui::flat); - auto *master_name = new UiBox(x + w - 140, y + 48, 136, 26, "MASTER"); - master_name->labelfont(UI_HELVETICA_BOLD); - auto *master_hint = new UiBox(x + w - 140, y + 80, 136, 24, "Output monitor"); - master_hint->labelsize(11); - master_readout_ = new UiBox(x + w - 140, y + 340, 136, 40); - master_readout_->labelsize(10); - master_panel_->add(left_); - master_panel_->add(right_); - master_panel_->end(); - reset_meters(); - parameter_ = new UiChoice(x + 110, y + 146, 150, 26, "Automation"); - parameter_->add("Gain|Pan"); - parameter_->value(0); - parameter_->callback(select, this); - start_ = new UiValue_Input(x + 420, y + 146, 140, 26, "First tick"); - start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks); - start_->step(3840); - start_->callback(select, this); - mode_ = new UiChoice(x + 705, y + 146, 150, 26, "Interpolation"); - mode_->add("Linear|Hold (step)"); - mode_->value(0); - hint_ = new UiBox(x, y, w, 18, - "Select a strip for automation. M/S toggles; arrows adjust gain, Shift+arrows pan. Faders show " - "static dB gain."); - hint_->labelsize(11); - lane_ = new AutomationLane(x + 10, y + h - 80, w - 20, 76, *this); - end(); - set_project(p); - layout(); -} - -/** - * @brief Returns the ID of the track selected in the track chooser. - * - * @return Selected track ID, or UINT64_MAX when the chooser selection does - * not resolve to a project track. - */ -uint64_t MixerWidget::selected_track() const -{ - return selected_id_; -} - -/** - * @brief Returns the automation parameter selected in the chooser. - * - * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise - * XTC_AUTOMATION_GAIN. - */ -xtc_automation_parameter MixerWidget::parameter() const -{ - return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN; -} - -/** - * @brief Returns the first tick shown by the automation lane. - * - * @return Clamped first tick in project ticks, or 0 when the input value is - * not finite. - */ -uint64_t MixerWidget::lane_start() const -{ - if (!std::isfinite(start_->value())) - return 0; - return static_cast( - std::clamp(start_->value(), 0.0, static_cast(XTC_PROJECT_MAX_TICK - lane_ticks))); -} - -/** - * @brief Displays a different project and refreshes the controls. - * - * The current track selection is preserved only when the same project - * pointer is passed again. The project is borrowed and must outlive use. - * - * @param p Project to display; must remain valid while shown. - */ -void MixerWidget::set_project(xtc_project *p) -{ - bool reset = project_ != p; - uint64_t selected = project_ == p ? selected_track() : UINT64_MAX; - project_ = p; - tracks_->clear(); - routes_->clear(); routes_->add("Master"); - route_ids_.clear(); route_ids_.push_back(UINT64_MAX); - size_t n = 0, c = 0; - xtc_project_get_counts(p, &n, &c); - int index = 0; - for (size_t i = 0; i < n; ++i) { - xtc_track_info t{}; - xtc_project_get_track(p, i, &t); - literal_item(tracks_,(std::string(t.is_bus?"Bus: ":"Track: ")+t.name).c_str()); - if(t.is_bus){literal_item(routes_,(std::string("Bus: ")+t.name).c_str());route_ids_.push_back(t.id);} - if (t.id == selected) - index = static_cast(i); - } - tracks_->value(n ? index : -1); - selected_id_=UINT64_MAX; - if(n){xtc_track_info track{};xtc_project_get_track(p,static_cast(index),&track);selected_id_=track.id;} - sync_strips(reset); - selection(); - redraw(); -} - -/** - * @brief Synchronizes the child controls with the selected track. - * - * Reconcile strip values and routing with model IDs, highlight the selected - * track, and keep its automation controls in sync. - */ -void MixerWidget::selection() -{ - xtc_track_info t{}; - bool valid = xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK; - if (valid) - lane_->activate(); - else - lane_->deactivate(); - for (auto *s : strips_) { - xtc_track_info track{}; - size_t count = 0, clips = 0; - xtc_project_get_counts(project_, &count, &clips); - for (size_t i = 0; i < count; ++i) { - xtc_project_get_track(project_, i, &track); - if (track.id == s->id) { - s->sync(track); - break; - } - } - } - if (valid && !t.is_bus) - routes_->activate(); - else - routes_->deactivate(); - routes_->value(0); - for (size_t i = 0; i < route_ids_.size(); ++i) - if (route_ids_[i] == t.bus_id) - routes_->value(static_cast(i)); - if (auto *selected = strip(selected_track())) { - int target = bank_->xposition(); - if (selected->x() < bank_->x()) - target -= bank_->x() - selected->x(); - else if (selected->x() + selected->w() > bank_->x() + bank_->w() - 12) - target += selected->x() + selected->w() - (bank_->x() + bank_->w() - 12); - bank_->scroll_to(std::max(0, target), 0); - } - lane_->redraw(); -} - -/** - * @brief Updates the left and right output meters. - * - * Meter values are clamped to 1.0, while the over-range color is chosen - * from the unclamped level. - * - * @param left Left-channel peak level. - * @param right Right-channel peak level. - */ -void MixerWidget::peaks(float left, float right) -{ - auto now = std::chrono::steady_clock::now(); - meter_elapsed_ = std::clamp(std::chrono::duration(now-peak_time_).count(), 0.0, 60.0); - peak_time_ = now; - (void)xtc_peak_meter_update(master_, left, meter_elapsed_); - (void)xtc_peak_meter_update(master_+1, right, meter_elapsed_); - display_peaks(left_, right_, master_, "Master"); - char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); - master_readout_->copy_label(text);master_readout_->align(UI_ALIGN_WRAP|UI_ALIGN_INSIDE); -} - -/** - * @brief Reports an edit result and refreshes the controls. - * - * A successful result invokes the widget callback so the owner can mark - * the project dirty; a failure shows a modal alert. The controls are - * resynchronized in both cases. - * - * @param r Result of the attempted edit. - * @return The same result passed in. - */ -xtc_result MixerWidget::changed(xtc_result r) -{ - if (r == XTC_OK) - do_callback(); - else - ui_alert("Mixer edit failed: %s", xtc_result_string(r)); - selection(); - return r; -} - -/** - * @brief Applies gain, pan, mute, and solo to the selected track. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param gain Linear gain in the range 0..4. - * @param pan Pan position in the range -1 (left)..1 (right). - * @param mute Mute state: 0 or 1. - * @param solo Solo state: 0 or 1. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ -xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) - r = xtc_project_set_mix(project_, selected_track(), gain, pan, mute, solo); - return changed(r); -} - -/** - * @brief Adds, replaces, or removes an automation point. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param tick Automation tick to modify; must be within the project range. - * @param value New value; ignored when remove is true. - * @param remove When true, removes the point instead of setting it. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ -xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - xtc_parameter_key key{XTC_PARAMETER_TRACK,selected_track(),static_cast(parameter())}; - if (r == XTC_OK) - r = xtc_project_automate(project_,key,48000,tick,mode_->value() == 1 ? 1U : 0U,value,remove ? 1 : 0); - return changed(r); -} - -/** - * @brief JUCE callback for the track, parameter, and first-tick controls. - * - * @param c Opaque MixerWidget pointer supplied as callback context. - */ -void MixerWidget::select(UiWidget *widget, void *c) -{ - auto *m=static_cast(c); - if(widget==m->tracks_){xtc_track_info track{}; - if(xtc_project_get_track(m->project_,static_cast(m->tracks_->value()),&track)==XTC_OK)m->selected_id_=track.id; - } - m->selection(); -} - -/** - * @brief Handles keyboard shortcuts and forwards other events. - * - * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), - * and Up/Down change the selected track. Modified key presses fall through. - * - * @param e JUCE event code. - * @return 1 when the event was consumed, otherwise the UiGroup result. - */ -int MixerWidget::handle(int e) -{ - if (e == UI_KEYDOWN && dynamic_cast(Ui::focus())) return UiGroup::handle(e); - if (e == UI_KEYDOWN && !(Ui::event_state() & (UI_CTRL | UI_ALT | UI_META))) { - int key = Ui::event_key(); - xtc_track_info t{}; - if (xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK) { - if (key == 'm' || key == 's') { - mix(t.gain, t.pan, key == 'm' ? !t.mute : t.mute, key == 's' ? !t.solo : t.solo); - return 1; - } - if (key == UI_Left || key == UI_Right) { - float d = key == UI_Left ? -0.05f : 0.05f; - bool pan = (Ui::event_state() & UI_SHIFT) != 0; - mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f), pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, - t.mute, t.solo); - return 1; - } - if (key == UI_Up || key == UI_Down) { - size_t n = 0, c = 0; - xtc_project_get_counts(project_, &n, &c); - tracks_->value(std::clamp(tracks_->value() + (key == UI_Up ? -1 : 1), 0, static_cast(n) - 1)); - select(tracks_,this); - return 1; - } - } - } - return UiGroup::handle(e); -} - -void MixerWidget::display_peaks(UiProgress *left, UiProgress *right, xtc_peak_meter *meters, const char *prefix) -{ - for (unsigned int side = 0; side < 2; ++side) { - auto *bar = side ? right : left; - char label[80]; - std::snprintf(label, sizeof(label), "%s %c %.1f dBFS%s", prefix, side ? 'R' : 'L', - static_cast(meters[side].db), meters[side].clipped ? " CLIP" : ""); - bar->copy_label(label); - bar->value(std::clamp(meters[side].db, -90.0f, 6.0f)); - bar->selection_color(meters[side].clipped ? UI_RED : xtc_ui::accent()); - } -} -void MixerWidget::track_peaks(float left, float right) -{ - for(auto *s:strips_)if(s->id==selected_track())s->update(left,right,meter_elapsed_); -} -void MixerWidget::reset_meters() -{ - for (unsigned int i = 0; i < 2; ++i) { xtc_peak_meter_reset(master_+i); } - display_peaks(left_, right_, master_, "Master"); - char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); - master_readout_->copy_label(text);master_readout_->align(UI_ALIGN_WRAP|UI_ALIGN_INSIDE); - for(auto *s:strips_){s->reset();s->update(0,0,0);} -} -xtc_result MixerWidget::add_bus() -{ - uint64_t id; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_add_bus(project_, "Bus", &id); - if (r == XTC_OK) { changed(r); select_track(id); return r; } - return changed(r); -} -xtc_result MixerWidget::route(uint64_t bus) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_route_track(project_, selected_track(), bus); - return changed(r); -} -bool MixerWidget::select_track(uint64_t id) -{ - size_t n = 0, clips = 0; xtc_project_get_counts(project_, &n, &clips); - for (size_t i = 0; i < n; ++i) { - xtc_track_info track{}; xtc_project_get_track(project_, i, &track); - if (track.id == id) { selected_id_=id;tracks_->value(static_cast(i)); selection(); return true; } - } - return false; -} - -UiGroup *MixerWidget::strip(uint64_t id) const -{ - for (auto *s : strips_) - if (s->id == id) - return s; - return nullptr; -} -void MixerWidget::meters(const xtc_meter_snapshot &snapshot) -{ - peaks(snapshot.master_left, snapshot.master_right); - for (auto *s : strips_) { - float left = 0, right = 0; - for (size_t i = 0; i < std::min(snapshot.count, static_cast(XTC_PROJECT_MAX_TRACKS)); ++i) - if (snapshot.tracks[i].id == s->id) { - left = snapshot.tracks[i].left; - right = snapshot.tracks[i].right; - break; - } - s->update(left, right, meter_elapsed_); - } -} -void MixerWidget::sync_strips(bool reset) -{ - size_t n = 0, clips = 0; - xtc_project_get_counts(project_, &n, &clips); - bool rebuild = n != strips_.size(); - for (size_t i = 0; i < n && !rebuild; ++i) { - xtc_track_info t{}; - xtc_project_get_track(project_, i, &t); - rebuild = t.id != strips_[i]->id; - } - if (rebuild) { - auto *previous = UiGroup::current(); - bank_->clear(); - strips_.clear(); - bank_->begin(); - for (size_t i = 0; i < n; ++i) { - xtc_track_info t{}; - xtc_project_get_track(project_, i, &t); - strips_.push_back(new MixerStrip(*this, t.id)); - } - bank_->end(); - UiGroup::current(previous); - layout(); - } - if (reset) - for (auto *s : strips_) - s->reset(); -} -void MixerWidget::layout() -{ - const int bank_height = std::max(250, h() - 172); - tracks_->resize(x() + 48, y() + 4, 210, 26); - routes_->resize(x() + 326, y() + 4, 160, 26); - bus_->resize(x() + 500, y() + 4, 100, 26); - reset_->resize(x() + 612, y() + 4, 112, 26); - bank_->resize(x(), y() + 40, w() - 154, bank_height); - int offset = bank_->xposition(); - for (size_t i = 0; i < strips_.size(); ++i) - strips_[i]->resize(bank_->x() + static_cast(i) * 156 - offset, bank_->y(), 148, bank_height - 16); - master_panel_->resize(x() + w() - 144, y() + 40, 144, bank_height); - // Master is pinned outside the horizontally scrolling bank. - left_->resize(x() + w() - 118, y() + 118, 32, bank_height - 140); - right_->resize(x() + w() - 66, y() + 118, 32, bank_height - 140); - master_readout_->resize(x() + w() - 140, y() + 40 + bank_height - 52, 136, 42); - left_->align(UI_ALIGN_TOP); - right_->align(UI_ALIGN_TOP); - left_->labelsize(10); - right_->labelsize(10); - parameter_->resize(x() + 84, y() + h() - 120, 100, 26); - start_->resize(x() + 264, y() + h() - 120, 116, 26); - mode_->resize(x() + 488, y() + h() - 120, 136, 26); - hint_->resize(x() + 8, y() + h() - 92, w() - 16, 18); - lane_->resize(x() + 8, y() + h() - 72, w() - 16, 68); - redraw(); -} -void MixerWidget::resize(int x, int y, int w, int h) -{ - UiWidget::resize(x, y, w, h); - layout(); -} diff --git a/src/gui/JUCE/mixer_widget.hpp b/src/gui/JUCE/mixer_widget.hpp deleted file mode 100644 index bc187bb..0000000 --- a/src/gui/JUCE/mixer_widget.hpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifndef XTC_MIXER_WIDGET_HPP -#define XTC_MIXER_WIDGET_HPP -#include "xtc/xtc_mixer.h" -#include "xtc/xtc_backend.h" -#include "ui.hpp" -#include -#include - -class UiChoice; -class UiButton; -class UiValue_Slider; -class UiCheck_Button; -class UiProgress; -class UiValue_Input; -class AutomationLane; -class MixerStrip; -class UiScroll; -class UiBox; - -class MixerWidget : public UiGroup { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using BeforeEdit = xtc_result (*)(void *); - - /** - * @brief Creates the mixer panel and its child controls. - * - * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output - * meters, automation controls, and lane, then displays the given project. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Widget width in pixels. - * @param h Widget height in pixels. - * @param p Project to display; borrowed and must outlive its use. - * @param before Callback invoked before each edit; may be nullptr. - * @param context Opaque pointer forwarded to before. - */ - MixerWidget(int x, int y, int w, int h, xtc_project * p, BeforeEdit before, void * context); - - /** - * @brief Displays a different project and refreshes the controls. - * - * The current track selection is preserved only when the same project - * pointer is passed again. The project is borrowed and must outlive use. - * - * @param p Project to display; must remain valid while shown. - */ - void set_project(xtc_project * p); - - /** - * @brief Updates the left and right output meters. - * - * Meter values are clamped to 1.0, while the over-range color is chosen - * from the unclamped level. - * - * @param left Left-channel peak level. - * @param right Right-channel peak level. - */ - void peaks(float left, float right); - - /** - * @brief Returns the ID of the track selected in the track chooser. - * - * @return Selected track ID, or UINT64_MAX when the chooser selection does - * not resolve to a project track. - */ - uint64_t selected_track() const; - - /** - * @brief Returns the project currently displayed by the widget. - * - * @return Borrowed project pointer, possibly nullptr; the widget does not - * own it. - */ - xtc_project *project() const - { - return project_; - } - - /** - * @brief Returns the automation parameter selected in the chooser. - * - * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise - * XTC_AUTOMATION_GAIN. - */ - xtc_automation_parameter parameter() const; - - /** - * @brief Returns the first tick shown by the automation lane. - * - * @return Clamped first tick in project ticks, or 0 when the input value is - * not finite. - */ - uint64_t lane_start() const; - - /** - * @brief Adds, replaces, or removes an automation point. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param tick Automation tick to modify; must be within the project range. - * @param value New value; ignored when remove is true. - * @param remove When true, removes the point instead of setting it. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ - xtc_result point(uint64_t tick, float value, bool remove = false); - - /** - * @brief Applies gain, pan, mute, and solo to the selected track. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param gain Linear gain in the range 0..4. - * @param pan Pan position in the range -1 (left)..1 (right). - * @param mute Mute state: 0 or 1. - * @param solo Solo state: 0 or 1. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ - xtc_result add_bus(); - xtc_result route(uint64_t bus); - bool select_track(uint64_t id); - void track_peaks(float left, float right); - /* One control-thread snapshot updates all strips; missing taps decay to silence. */ - void meters(const xtc_meter_snapshot &snapshot); - UiGroup *strip(uint64_t id) const; - size_t strip_count() const { return strips_.size(); } - void resize(int x, int y, int w, int h) override; - void reset_meters(); - bool master_clipped() const { return master_[0].clipped || master_[1].clipped; } - xtc_result mix(float gain, float pan, int mute, int solo); - - /** - * @brief Handles keyboard shortcuts and forwards other events. - * - * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), - * and Up/Down change the selected track. Modified key presses fall through. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, otherwise the UiGroup result. - */ - int handle(int event) override; - - private: - xtc_project *project_; - BeforeEdit before_; - void *context_; - UiChoice *tracks_, *parameter_, *routes_, *mode_; - std::vector route_ids_; - UiProgress *left_, *right_; - UiScroll *bank_; - UiGroup *master_panel_; - UiButton *bus_, *reset_; - UiBox *hint_, *master_readout_; - std::vector strips_; - void sync_strips(bool reset = false); - void layout(); - xtc_peak_meter master_[2]{}; - uint64_t selected_id_ = UINT64_MAX; - std::chrono::steady_clock::time_point peak_time_ = std::chrono::steady_clock::now(); - double meter_elapsed_ = 0.1; - void display_peaks(UiProgress *left, UiProgress *right, xtc_peak_meter *meters, const char *prefix); - - UiValue_Input *start_; - AutomationLane *lane_; - /** - * @brief Reports an edit result and refreshes the controls. - * - * A successful result invokes the widget callback so the owner can mark - * the project dirty; a failure shows a modal alert. The controls are - * resynchronized in both cases. - * - * @param r Result of the attempted edit. - * @return The same result passed in. - */ - xtc_result changed(xtc_result r); - - /** - * @brief Synchronizes the child controls with the selected track. - * - * Reconcile strip controls with stable model IDs and the selected - * automation target. - */ - void selection(); - - /** - * @brief JUCE callback for the track, parameter, and first-tick controls. - * - * @param widget Track chooser or automation control that changed. - * @param c Opaque MixerWidget pointer supplied as callback context. - */ - static void select(UiWidget * widget, void * c); -}; -#endif diff --git a/src/gui/JUCE/piano_roll.cpp b/src/gui/JUCE/piano_roll.cpp deleted file mode 100644 index c16f7ed..0000000 --- a/src/gui/JUCE/piano_roll.cpp +++ /dev/null @@ -1,455 +0,0 @@ -#include "theme.hpp" -#include "snap.hpp" -#include "piano_roll.hpp" -#include "ui.hpp" -#include -#include -#include - -namespace { -constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; -template bool parse(const char *text, Values &...values) -{ - std::istringstream input(text); - (input >> ... >> values); - return !input.fail() && (input >> std::ws).eof(); -} -} - -/** - * @brief Creates a piano-roll editor for a MIDI clip. - * - * The pre-edit guard, when non-NULL, is called before every project mutation - * and can veto the edit by returning an error. - * - * @param left Left edge of the widget in the parent's coordinate system. - * @param top Top edge of the widget in the parent's coordinate system. - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to edit. - * @param before Optional pre-edit guard; NULL means no guard is used. - * @param context Opaque pointer passed to @p before on every edit. - */ -PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64_t clip, BeforeEdit before, - void *context) - : UiWidget(left, top, 900, header + 128 * row_height), project_(project), clip_(clip), before_(before), - context_(context) -{ - set_project(project, clip); -} - -/** - * @brief Points the editor at a different project or clip. - * - * Clears any in-progress drag when the project or clip changes and resizes the - * widget to fit the clip's source length. Selection is cleared on a new model or clip. - * - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to display. - */ -bool PianoRollWidget::select_note(uint64_t id) -{ - size_t count = 0; - xtc_project_note_count(project_,&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; - if (xtc_project_get_note(project_,i,¬e) == XTC_OK && note.id == id && visible(note)) { - selected_ = id; all_ = false; mode_ = 0; redraw(); return true; - } - } - return false; -} - -void PianoRollWidget::set_project(xtc_project *project, uint64_t clip) -{ - if (project != project_ || clip != clip_) { - mode_ = 0; - selected_ = UINT64_MAX; - all_ = false; - } - project_ = project; - clip_ = clip; - xtc_clip_info c{}; - int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK - ? static_cast(c.source_length_tick * scale / 960) + key_width + 20 - : 900; - size(std::max(900, width), header + 128 * row_height); - redraw(); -} - -/** - * @brief Converts a horizontal pixel position to a snapped tick. - * - * The position is measured in absolute window coordinates and snapped down to - * the current configured grid. - * - * @param px Horizontal position in window coordinates. - * @return Tick offset within the clip source, or 0 for positions on or left of - * the keyboard. - */ -uint64_t PianoRollWidget::tick(int px) const -{ - return static_cast(std::max(0, px - x() - key_width)) * 960U / scale / grid_ * grid_; -} - -/** - * @brief Reports an edit result and notifies listeners on success. - * - * On success the widget callback runs and the editor is redrawn; on failure an - * alert shows the error and the project is left unchanged. - * - * @param result Result of the attempted project mutation. - * @return The same result code. - */ -xtc_result PianoRollWidget::change(xtc_result result) -{ - if (result == XTC_OK) { - do_callback(); - redraw(); - } else - ui_alert("Note unchanged: %s", xtc_result_string(result)); - return result; -} - -/** - * @brief Adds a note to the current clip and selects the new note. - * - * Invokes the pre-edit guard, if any, before mutating the project. - * - * @param start Note start in clip source ticks. - * @param length Note length in ticks. - * @param pitch MIDI pitch from 0 to 127. - * @param velocity MIDI velocity from 1 to 127. - * @param channel MIDI channel from 0 to 15. - * @return XTC_OK on success, the guard's error, or the project error. - */ -xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, - unsigned int channel) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - uint64_t id; - if (r == XTC_OK) - r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id); - if (r == XTC_OK) { - selected_ = id; - all_ = false; - } - return change(r); -} - -/** - * @brief Removes the selected note from the current clip. - * - * Invokes the pre-edit guard, if any, before mutating the project. The - * selection is cleared only when the removal succeeds. - * - * @return XTC_OK on success, the guard's error, or the project error. - */ -xtc_result PianoRollWidget::delete_selected() -{ - xtc_note_edit edit{}; - edit.action = XTC_NOTES_DELETE; - return apply(edit); -} - -/** - * @brief Draws the keyboard, beat grid, and notes of the current clip. - * - * Paints only the background when the clip is missing or is not a MIDI clip. - */ -void PianoRollWidget::draw() -{ - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(30, 33, 38)); - ui_rectf(x(), y(), w(), h()); - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) { - ui_pop_clip(); - return; - } - int cx, cy, cw, ch; - ui_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); - ui_font(UI_HELVETICA, 11); - for (int pitch = 0; pitch < 128; ++pitch) { - int py = y() + header + (127 - pitch) * row_height; - if (py + row_height < cy || py > cy + ch) - continue; - int pc = pitch % 12; - bool black = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10; - ui_color(black ? ui_rgb_color(29, 31, 35) : ui_rgb_color(43, 46, 52)); - ui_rectf(cx, py, cw, row_height - 1); - ui_color(black ? ui_rgb_color(60, 63, 70) : ui_rgb_color(130, 135, 140)); - ui_rectf(x(), py, key_width - 2, row_height - 1); - if (!pc) { - char text[20]; - std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1); - ui_color(UI_WHITE); - ui_draw(text, x() + 6, py + 12); - } - } - // At 1-tick snap, show at least a 120-tick visual grid to avoid pixel overdraw. - uint64_t visual = std::max(120, grid_); - int spacing = static_cast(visual * scale / 960); - int first = std::max(0, (cx - x() - key_width) / spacing); - int last = (cx + cw - x() - key_width) / spacing + 1; - for (int step = first; step <= last; ++step) { - int px = x() + key_width + static_cast(static_cast(step) * visual * scale / 960); - ui_color((static_cast(step) * visual) % 3840 == 0 ? ui_rgb_color(95, 98, 105) : ui_rgb_color(52, 55, 62)); - ui_line(px, cy, px, cy + ch); - } - size_t count = 0; - xtc_project_note_count(project_, &count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info n{}; - xtc_project_get_note(project_, i, &n); - if (!visible(n)) - continue; - int left = x() + key_width + static_cast(n.start_tick * scale / 960); - int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); - int py = y() + header + (127 - static_cast(n.pitch)) * row_height; - int a = std::max(cx, left), b = std::min(cx + cw, right); - if (a >= b || py + row_height < cy || py > cy + ch) - continue; - ui_color(all_ || n.id == selected_ ? xtc_ui::accent() - : ui_rgb_color(50, static_cast(70 + n.velocity), 110)); - ui_rectf(a, py + 1, b - a, row_height - 3); - } - ui_color(ui_rgb_color(30, 33, 38)); - ui_rectf(x(), y(), w(), header); - char status[160]; - std::snprintf(status, sizeof(status), "Snap %llu ticks | Channel %d (-1 all) | Pitches %u..%u | %s", - static_cast(grid_), channel_filter_, low_, high_, - all_ ? "All filtered notes selected" : "Click selects; Ctrl+A selects filtered notes"); - ui_color(UI_WHITE); - ui_draw(status, x() + 6, y() + 18); - ui_pop_clip(); -} - -/** - * @brief Handles mouse, keyboard, and focus events for the editor. - * - * Left-click on empty space adds a note, clicking a note selects it, and - * dragging moves or resizes it. Right-click and Delete remove the selected - * note; Escape cancels an in-progress drag. - * - * @param event JUCE event code. - * @return 1 when the event was handled, otherwise the result of the base - * class handler. - */ -int PianoRollWidget::handle(int event) -{ - if (event == UI_PUSH) { - take_focus(); - mode_ = 0; - size_t count = 0; - xtc_project_note_count(project_, &count); - selected_ = UINT64_MAX; - all_ = false; - for (size_t i = count; i > 0; --i) { - xtc_note_info n{}; - xtc_project_get_note(project_, i - 1, &n); - int left = x() + key_width + static_cast(n.start_tick * scale / 960); - int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); - int top = y() + header + (127 - static_cast(n.pitch)) * row_height; - if (visible(n) && Ui::event_x() >= left && Ui::event_x() < right && Ui::event_y() >= top && - Ui::event_y() < top + row_height) { - selected_ = n.id; - original_ = n; - drag_x_ = Ui::event_x(); - drag_y_ = Ui::event_y(); - mode_ = right - Ui::event_x() <= 6 ? 2 : 1; - break; - } - } - if (selected_ != UINT64_MAX && Ui::event_button() == UI_RIGHT_MOUSE) { - mode_ = 0; - UiMenu_Item items[5]{}; - items[0].text = "Velocity and channel (V)"; - items[1].text = "Quantize (Q)"; - items[2].text = "Repeat (Ctrl+D)"; - items[3].text = "Delete (undoable)"; - const auto *picked = items->popup(Ui::event_x(),Ui::event_y()); - if (picked == items) command(2); - else if (picked == items+1) command(3); - else if (picked == items+2) command(5); - else if (picked == items+3) delete_selected(); - } else if (Ui::event_button() == UI_LEFT_MOUSE && selected_ == UINT64_MAX && Ui::event_x() >= x() + key_width && - Ui::event_y() >= y() + header) { - int pitch = 127 - (Ui::event_y() - y() - header) / row_height; - xtc_clip_info c{}; - uint64_t start = tick(Ui::event_x()); - if (pitch >= static_cast(low_) && pitch <= static_cast(high_) && xtc_project_find_clip(project_, clip_, &c) == XTC_OK && - start < c.source_length_tick) - add_note(start, std::min(new_note_ticks, c.source_length_tick - start), - static_cast(pitch), new_note_velocity, channel_filter_ < 0 ? new_note_channel : static_cast(channel_filter_)); - } - redraw(); - return 1; - } - if (event == UI_DRAG) - return mode_ != 0; - if (event == UI_RELEASE && mode_) { - int mode = mode_; - mode_ = 0; - int64_t delta = static_cast(Ui::event_x() - drag_x_) * 960 / scale / static_cast(grid_) * static_cast(grid_); - xtc_clip_info c{}; - if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK) - return 1; - int64_t start = static_cast(original_.start_tick), - length = static_cast(original_.length_tick); - unsigned int pitch = original_.pitch; - if (mode == 1) { - start = std::clamp(start + delta, 0, static_cast(c.source_length_tick) - length); - pitch = static_cast( - std::clamp(static_cast(pitch) - (Ui::event_y() - drag_y_) / row_height, 0, 127)); - } else { - int64_t maximum = static_cast(c.source_length_tick) - start; - length = std::clamp(length + delta, std::min(static_cast(grid_), maximum), maximum); - } - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) - r = xtc_project_edit_note(project_, selected_, static_cast(start), static_cast(length), - pitch, original_.velocity, original_.channel); - change(r); - return 1; - } - if (event == UI_KEYDOWN) { - int key = Ui::event_key(); - if (key == 'a' && (Ui::event_state() & UI_CTRL)) { select_filtered(); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'q') { command(3); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'v') { command(2); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'g') { command(0); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'f') { command(1); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 't') { command(4); return 1; } - if (key == 'd' && (Ui::event_state() & UI_CTRL)) { command(5); return 1; } - if (key == UI_Up || key == UI_Down) { - xtc_note_edit edit{}; - edit.velocity = edit.channel = -1; - edit.transpose = key == UI_Up ? 1 : -1; - apply(edit); - return 1; - } - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Delete) { - delete_selected(); - return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { - mode_ = 0; - return 1; - } - if (event == UI_FOCUS || event == UI_UNFOCUS) - return 1; - return UiWidget::handle(event); -} - -bool PianoRollWidget::visible(const xtc_note_info &n) const -{ - return n.clip_id == clip_ && (channel_filter_ < 0 || n.channel == static_cast(channel_filter_)) && - n.pitch >= low_ && n.pitch <= high_; -} - -xtc_result PianoRollWidget::set_grid(uint64_t ticks) -{ - if (xtc_ui::snap_index(ticks) < 0) - return XTC_ERR_INVALID_ARGUMENT; - grid_ = ticks; - redraw(); - return XTC_OK; -} - -xtc_result PianoRollWidget::set_filter(int channel, unsigned int low, unsigned int high) -{ - if (channel < -1 || channel > 15 || low > high || high > 127) return XTC_ERR_INVALID_ARGUMENT; - channel_filter_ = channel; - low_ = low; - high_ = high; - selected_ = UINT64_MAX; - all_ = false; - mode_ = 0; - redraw(); - return XTC_OK; -} - -void PianoRollWidget::select_filtered() -{ - selected_ = UINT64_MAX; - all_ = true; - mode_ = 0; - redraw(); -} - -xtc_result PianoRollWidget::apply(const xtc_note_edit &edit) -{ - if (!all_ && selected_ == UINT64_MAX) return XTC_ERR_NOT_FOUND; - xtc_note_filter filter{clip_, all_ ? UINT64_MAX : selected_, channel_filter_, low_, high_}; - size_t count; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_edit_notes(project_, &filter, &edit, &count); - if (r == XTC_OK && edit.action == XTC_NOTES_DELETE) { - selected_ = UINT64_MAX; - all_ = false; - } - return change(r); -} - -void PianoRollWidget::command(int action) -{ - const char *text; - xtc_note_edit edit{}; - edit.velocity = edit.channel = -1; - if (action == 0) { - text = ui_input("Snap ticks: 1, 120, 160 (1/16 triplet), 240, 320 (1/8 triplet), 480, 960, 1920, 3840", "240"); - unsigned int value; - if (text && (!parse(text, value) || set_grid(value) != XTC_OK)) - ui_alert("Choose one of the listed grids."); - return; - } - if (action == 1) { - text = ui_input("Channel (-1 all, 0..15), lowest pitch, highest pitch", "-1 0 127"); - int channel; unsigned int low, high; - if (text && (!parse(text, channel, low, high) || - set_filter(channel, low, high) != XTC_OK)) ui_alert("Invalid note filter."); - return; - } - if (action == 6) { select_filtered(); return; } - if (action == 7) { delete_selected(); return; } - if (action == 2) { - text = ui_input("Velocity (1..127, -1 preserve), channel (0..15, -1 preserve)", "100 -1"); - if (!text) return; - if (!parse(text, edit.velocity, edit.channel)) { - ui_alert("Enter velocity and channel."); return; - } - } else if (action == 3) edit.grid_tick = grid_; - else if (action == 4) { - text = ui_input("Transpose semitones (-127..127)", "12"); - if (!text) return; - if (!parse(text, edit.transpose)) { - ui_alert("Enter a semitone count."); return; - } - } else if (action == 5) { - text = ui_input("Additional copies (1..4096), spacing in source ticks (clip must have room)", "1 960"); - unsigned long long interval; - if (!text) return; - if (!parse(text, edit.copies, interval)) { - ui_alert("Enter copy count and spacing."); return; - } - edit.action = XTC_NOTES_REPEAT; - edit.interval_tick = static_cast(interval); - } else return; - apply(edit); -} - -xtc_result PianoRollWidget::set_velocity(uint64_t id, unsigned int velocity) -{ - if (velocity < 1 || velocity > 127) return XTC_ERR_INVALID_ARGUMENT; - size_t count = 0; xtc_project_note_count(project_,&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(project_,i,¬e); - if (note.id != id || !visible(note)) continue; - if (note.velocity == velocity) return XTC_OK; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_edit_note(project_,id,note.start_tick,note.length_tick,note.pitch,velocity,note.channel); - return change(r); - } - return XTC_ERR_NOT_FOUND; -} diff --git a/src/gui/JUCE/piano_roll.hpp b/src/gui/JUCE/piano_roll.hpp deleted file mode 100644 index 944d186..0000000 --- a/src/gui/JUCE/piano_roll.hpp +++ /dev/null @@ -1,150 +0,0 @@ -#ifndef XTC_PIANO_ROLL_HPP -#define XTC_PIANO_ROLL_HPP -#include "ui.hpp" -#include "xtc/xtc_midi.h" - -class PianoRollWidget : public UiWidget { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using BeforeEdit = xtc_result (*)(void *); - /** - * @brief Creates a piano-roll editor for a MIDI clip. - * - * The pre-edit guard, when non-NULL, is called before every project - * mutation and can veto the edit by returning an error. - * - * @param x Left edge of the widget in the parent's coordinate system. - * @param y Top edge of the widget in the parent's coordinate system. - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to edit. - * @param before Optional pre-edit guard; NULL means no guard is used. - * @param context Opaque pointer passed to @p before on every edit. - */ - PianoRollWidget(int x, int y, xtc_project *project, uint64_t clip, BeforeEdit before, void *context); - /** - * @brief Points the editor at a different project or clip. - * - * Clears any in-progress drag when the project or clip changes and resizes - * the widget to fit the clip's source length. Selection is cleared on a new - * model or clip. - * - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to display. - */ - void set_project(xtc_project *project, uint64_t clip); - - /** - * @brief Returns the ID of the clip currently being edited. - * - * @return Clip identifier supplied to the constructor or set_project(). - */ - uint64_t clip() const - { - return clip_; - } - - /** - * @brief Adds a note to the current clip and selects the new note. - * - * Invokes the pre-edit guard, if any, before mutating the project. - * - * @param start Note start in clip source ticks. - * @param length Note length in ticks. - * @param pitch MIDI pitch from 0 to 127. - * @param velocity MIDI velocity from 1 to 127. - * @param channel MIDI channel from 0 to 15. - * @return XTC_OK on success, the guard's error, or the project error. - */ - xtc_result add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity = 100, - unsigned int channel = 0); - /** - * @brief Removes the selected note from the current clip. - * - * Invokes the pre-edit guard, if any, before mutating the project. The - * selection is cleared only when the removal succeeds. - * - * @return XTC_OK on success, the guard's error, or the project error. - */ - xtc_project *project() const { return project_; } - bool note_visible(const xtc_note_info ¬e) const { return visible(note); } - int note_x(uint64_t tick) const { return x()+64+static_cast(tick*96/960); } - uint64_t new_note_ticks=960; - unsigned int new_note_velocity=100,new_note_channel=0; - int filter_channel() const { return channel_filter_; } - unsigned int filter_low() const { return low_; } - unsigned int filter_high() const { return high_; } - uint64_t grid() const { return grid_; } - xtc_result set_velocity(uint64_t note, unsigned int velocity); - uint64_t selected_note() const { return selected_; } - bool select_note(uint64_t id); - xtc_result delete_selected(); - /* UI preferences; independent of persisted project state. */ - xtc_result set_grid(uint64_t ticks); - xtc_result set_filter(int channel, unsigned int low, unsigned int high); - void select_filtered(); - xtc_result apply(const xtc_note_edit &edit); - /* Menu/keyboard commands: grid, filter, properties, quantize, transpose, - repeat, select all, delete. */ - void command(int action); - - /** - * @brief Handles mouse, keyboard, and focus events for the editor. - * - * Left-click on empty space adds a note, clicking a note selects it, and - * dragging moves or resizes it. Right-click and Delete remove the selected - * note; Escape cancels an in-progress drag. - * - * @param event JUCE event code. - * @return 1 when the event was handled, otherwise the result of the base - * class handler. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the keyboard, beat grid, and notes of the current clip. - * - * Paints only the background when the clip is missing or is not a MIDI clip. - */ - void draw() override; - - private: - xtc_project *project_; - uint64_t clip_, selected_ = UINT64_MAX; - BeforeEdit before_; - void *context_; - xtc_note_info original_{}; - int mode_ = 0, drag_x_ = 0, drag_y_ = 0; - uint64_t grid_ = 240; - int channel_filter_ = -1; - unsigned int low_ = 0, high_ = 127; - bool all_ = false; - bool visible(const xtc_note_info &n) const; - - /** - * @brief Reports an edit result and notifies listeners on success. - * - * On success the widget callback runs and the editor is redrawn; on failure - * an alert shows the error and the project is left unchanged. - * - * @param result Result of the attempted project mutation. - * @return The same result code. - */ - xtc_result change(xtc_result result); - /** - * @brief Converts a horizontal pixel position to a snapped tick. - * - * The position is measured in absolute window coordinates and snapped down - * to the current configured grid. - * - * @param px Horizontal position in window coordinates. - * @return Tick offset within the clip source, or 0 for positions on or left - * of the keyboard. - */ - uint64_t tick(int px) const; -}; -#endif diff --git a/src/gui/JUCE/plugin_dialog.cpp b/src/gui/JUCE/plugin_dialog.cpp deleted file mode 100644 index 678396d..0000000 --- a/src/gui/JUCE/plugin_dialog.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "theme.hpp" -#include "plugin_dialog.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -namespace { -struct Dialog : UiDouble_Window { - bool accepted = false; - Dialog(int width, int height, const char *title) : UiDouble_Window(width,height,title) { set_modal(); } - static void accept(UiWidget *, void *data) { auto *d = static_cast(data); d->accepted = true; d->hide(); } - static void cancel(UiWidget *, void *data) { static_cast(data)->hide(); } - bool run() { callback(cancel,this); end(); xtc_ui::style(this); show(); while (shown()) Ui::wait(); return accepted; } -}; -} -bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument) -{ - Dialog dialog(760,450,"Plugin descriptors"); - auto *heading = new UiBox(15,10,730,40); - heading->copy_label(path); heading->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_CLIP); - auto *list = new UiHold_Browser(15,55,730,325); - list->format_char(0); // Plugin names are data, never browser formatting. - std::vector indices; - size_t rejected = 0; - for (unsigned int i = 0; i < 1024; ++i) { - xtc_plugin_spec spec{}; xtc_plugin_details details{}; - xtc_result r = xtc_plugin_probe(path,i,rate,&spec); - if (r == XTC_ERR_NOT_FOUND) break; - if (r != XTC_OK) { ++rejected; continue; } - if (!!spec.instrument != instrument) { ++rejected; continue; } - if (xtc_plugin_describe(&spec,rate,&details) != XTC_OK) { ++rejected; continue; } - std::string row = std::to_string(i) + ": " + details.name + " (" + spec.label + ") — " + details.maker + - (spec.inputs ? " [effect]" : " [source]"); - list->add(row.c_str()); indices.push_back(i); - } - auto *status = new UiBox(15,385,520,50); - std::string summary = std::to_string(indices.size()) + " compatible; " + std::to_string(rejected) + - " unsupported/unavailable. Select a descriptor."; - status->copy_label(summary.c_str()); status->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *cancel = new UiButton(540,400,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog); - auto *choose = new UiReturn_Button(645,400,100,30,"Choose"); choose->callback(Dialog::accept,&dialog); - if (indices.empty()) choose->deactivate(); else list->select(1); - if (!dialog.run() || list->value() < 1) return false; - index = indices[static_cast(list->value()-1)]; return true; -} -bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, xtc_plugin_spec &edited) -{ - Dialog dialog(800,600,"Plugin identity and controls"); - auto *identity = new UiBox(15,10,770,125); - std::string text = std::string(details.name) + " — " + details.maker + "\n" + spec.label + - " | " + std::string(xtc_plugin_format_name(spec.format)) + " ID " + std::to_string(spec.unique_id) + " | descriptor " + std::to_string(spec.index) + " | identity " + spec.identifier + - "\n" + spec.path + "\n" + details.copyright + - "\nMetadata verified; Apply validates initialization. Adapter may not expose units or editable controls."; - identity->copy_label(text.c_str()); identity->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *scroll = new UiScroll(10,140,780,370); - std::array inputs{}; - for (unsigned int i = 0; i < spec.control_count; ++i) { - int y = 150 + static_cast(i)*65; - const auto &p = details.parameters[i]; - std::string name = std::to_string(p.id) + ": " + p.name; - auto *label = new UiBox(25,y,425,25); label->copy_label(name.c_str()); label->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - inputs[i] = new UiValue_Input(460,y,150,25); inputs[i]->value(spec.values[i]); - inputs[i]->step((details.flags[i] & (XTC_PLUGIN_INTEGER|XTC_PLUGIN_TOGGLED)) ? 1 : 0.001); - std::string hint = "Range " + std::string(details.flags[i] & XTC_PLUGIN_BOUNDED_BELOW ? std::to_string(p.minimum) : "unbounded") + - " .. " + (details.flags[i] & XTC_PLUGIN_BOUNDED_ABOVE ? std::to_string(p.maximum) : "unbounded") + - "; default " + std::to_string(p.default_value) + - ((details.flags[i] & XTC_PLUGIN_TOGGLED) ? "; toggle 0/1" : (details.flags[i] & XTC_PLUGIN_INTEGER) ? "; integer" : "; continuous") + - ((details.flags[i] & XTC_PLUGIN_LOGARITHMIC) ? "; logarithmic hint" : ""); - auto *range = new UiBox(25,y+27,715,25); range->copy_label(hint.c_str()); range->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - } - if(!spec.control_count) { - auto *unavailable=new UiBox(25,160,730,75,"This adapter exposes no editable parameters. The plugin runs with its default configuration.\nNative plugin editor windows are not available in this host."); - unavailable->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - } - scroll->end(); - auto *note = new UiBox(15,515,770,30,"Automation overrides static controls. Reset defaults also clears all plugin automation."); - note->labelsize(12); - struct Reset { const xtc_plugin_details *details; std::array *inputs; unsigned int count; bool used; } reset{&details,&inputs,spec.control_count,false}; - auto *defaults = new UiButton(20,555,170,30,"Reset defaults"); - if(!spec.control_count)defaults->deactivate(); - defaults->callback([](UiWidget *, void *data) { - auto *r = static_cast(data); r->used = true; - for (unsigned int i = 0; i < r->count; ++i) (*r->inputs)[i]->value(r->details->parameters[i].default_value); - },&reset); - auto *cancel = new UiButton(570,555,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog); - auto *apply = new UiReturn_Button(675,555,105,30,"Apply"); apply->callback(Dialog::accept,&dialog); - if (!dialog.run()) return false; - xtc_plugin_spec candidate = spec; - for (unsigned int i = 0; i < spec.control_count; ++i) { - double value = inputs[i]->value(); const auto &p = details.parameters[i]; unsigned int flags = details.flags[i]; - if (!std::isfinite(value) || value < p.minimum || value > p.maximum || - ((flags & XTC_PLUGIN_INTEGER) && value != std::floor(value)) || - ((flags & XTC_PLUGIN_TOGGLED) && value != 0 && value != 1)) { - ui_alert("Invalid value for port %u (%s); no controls were changed.",p.id,p.name); return false; - } - candidate.values[i] = static_cast(value); - } - if (reset.used) candidate.point_count = 0; - edited = candidate; return true; -} diff --git a/src/gui/JUCE/plugin_dialog.hpp b/src/gui/JUCE/plugin_dialog.hpp deleted file mode 100644 index a1aad8f..0000000 --- a/src/gui/JUCE/plugin_dialog.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef XTC_PLUGIN_DIALOG_HPP -#define XTC_PLUGIN_DIALOG_HPP -#include "xtc/xtc_plugin.h" -/* Modal, control-thread-only views. They own copied metadata and widget state, - * never engine/module handles. Cancel/close leaves caller configuration intact. */ -bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument = false); -bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, - xtc_plugin_spec &edited); -#endif diff --git a/src/gui/JUCE/plugin_library.hpp b/src/gui/JUCE/plugin_library.hpp deleted file mode 100644 index c80dd44..0000000 --- a/src/gui/JUCE/plugin_library.hpp +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef XTC_PLUGIN_LIBRARY_HPP -#define XTC_PLUGIN_LIBRARY_HPP -#include "xtc/xtc_settings.h" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include -#include - -inline std::vector xtc_plugin_files(const xtc_settings &settings) -{ - namespace fs=std::filesystem; - std::set files; - size_t visited=0; - for(const char *list : {settings.clap_paths,settings.vst2_paths,settings.vst3_paths,settings.ladspa_paths}) { - std::string paths=list; size_t start=0; - while(start<=paths.size()) { - size_t end=paths.find(':',start); std::string root=paths.substr(start,end==std::string::npos ? end : end-start); - if(root.rfind("~/",0)==0) { char config[4096]; if(xtc_settings_path(config,sizeof(config))==XTC_OK) { - std::string home=config; home.resize(home.size()-10); root=home+root.substr(1); - } } - std::error_code error; - if(!root.empty() && fs::path(root).is_absolute()) { - fs::recursive_directory_iterator it(root,fs::directory_options::skip_permission_denied,error), finish; - while(!error && it!=finish && visited<4096 && files.size()<512) { - ++visited; - if(it.depth()>=6) it.disable_recursion_pending(); - if(it->is_regular_file(error)) { - auto extension=it->path().extension().string(); - if(extension==".so" || extension==".clap") files.insert(it->path().lexically_normal().string()); - } - error.clear(); it.increment(error); - } - } - if(end==std::string::npos) break; - start=end+1; - } - } - return {files.begin(),files.end()}; -} -/* Paths are a discovery hint, never a compatibility claim. Ambiguous .so files - * remain visible under both LADSPA and VST2; probing identifies the real ABI. */ -inline bool xtc_plugin_file_matches(const std::string &file,const std::string &query,int format) -{ - auto lower=[](std::string value){for(char &c:value)c=static_cast(std::tolower(static_cast(c)));return value;}; - const auto name=lower(file);bool vst3=name.find(".vst3/")!=std::string::npos; - bool clap=std::filesystem::path(name).extension()==".clap"; - if(format==3 && !vst3)return false; - if(format==4 && !clap)return false; - if((format==1 || format==2) && (vst3 || clap))return false; - return name.find(lower(query))!=std::string::npos; -} -inline bool xtc_choose_plugin_file(const xtc_settings &settings,std::string &path) -{ - struct Library : UiDouble_Window { - bool accepted=false,browse=false; - UiInput *search;UiChoice *format;UiHold_Browser *list; - UiButton *choose;std::vector files,visible; - explicit Library(const xtc_settings &settings):UiDouble_Window(900,560,"Plugin library — effects and instruments"),files(xtc_plugin_files(settings)) { - set_modal(); - auto *hint=new UiBox(16,8,868,42,"Search installed binaries; VST3 bundles are discovered recursively. Modules load only when inspected.\nLADSPA/VST2 share .so candidates; inspection verifies the format and compatible mono layout.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - search=new UiInput(78,60,510,28,"Search");search->when(UI_WHEN_CHANGED); - format=new UiChoice(666,60,218,28,"Format");format->add("All formats|LADSPA|VST2|VST3|CLAP");format->value(0); - for(auto *w:{static_cast(search),static_cast(format)})w->callback([](UiWidget *,void *c){static_cast(c)->filter();},this); - list=new UiHold_Browser(16,102,868,370);list->format_char(0); - auto *browse_button=new UiButton(16,506,130,32,"Browse files...");browse_button->callback([](UiWidget *,void *c){auto *d=static_cast(c);d->browse=true;d->hide();},this); - auto *cancel=new UiButton(650,506,100,32,"Cancel");cancel->callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - choose=new UiButton(764,506,120,32,"Inspect plugin");choose->callback([](UiWidget *,void *c){auto *d=static_cast(c);d->accepted=true;d->hide();},this); - callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - end();filter();xtc_ui::style(this); - } - void filter() { - list->clear();visible.clear(); - for(const auto &file:files)if(xtc_plugin_file_matches(file,search->value(),format->value())){visible.push_back(file);list->add(file.c_str());} - if(visible.empty())choose->deactivate();else{choose->activate();list->select(1);} - } - } dialog(settings); - dialog.show();while(dialog.shown())Ui::wait(); - if(dialog.browse) { - UiNative_File_Chooser chooser;chooser.title("Choose trusted plugin binary");chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*"); - if(chooser.show()!=0 || !chooser.filename())return false; - path=chooser.filename();return true; - } - if(!dialog.accepted || dialog.list->value()<1)return false; - path=dialog.visible[static_cast(dialog.list->value()-1)];return true; -} -#endif diff --git a/src/gui/JUCE/preferences_dialog.hpp b/src/gui/JUCE/preferences_dialog.hpp deleted file mode 100644 index 3de0f2e..0000000 --- a/src/gui/JUCE/preferences_dialog.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef XTC_PREFERENCES_DIALOG_HPP -#define XTC_PREFERENCES_DIALOG_HPP -#include "xtc/xtc_settings.h" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include - -class PreferencesDialog : public UiDouble_Window { - xtc_settings original_; - std::function apply_; - std::vector inputs_; - UiBox *message_; - std::function(bool)> devices_; - std::vector midi_names_[2]; - UiInput_Choice *midi_input_=nullptr,*midi_output_=nullptr; - void refresh_devices() { - for(auto pair:{std::pair{midi_input_,true},{midi_output_,false}}){ - if(!pair.first)continue; - std::string value=pair.first->value(); - // copy() owns the menu array but borrows its label strings. - // Release old items before replacing their backing storage. - pair.first->menubutton()->clear(); - auto &names=midi_names_[pair.second ? 0 : 1]; - names=devices_ ? devices_(pair.second) : std::vector{}; - names.insert(names.begin(),"");std::vector items(names.size()+1); - for(size_t i=0;imenubutton()->copy(items.data());pair.first->value(value.c_str()); - } - } - - static void apply_callback(UiWidget *,void *context) { - auto *d=static_cast(context); - xtc_settings candidate=d->original_; size_t count=0; - const xtc_setting_info *schema=xtc_settings_schema(&count); - for(size_t i=0;iinputs_[i]->value())!=XTC_OK) { - std::string text=std::string("Invalid value: ")+schema[i].label; - d->message_->copy_label(text.c_str()); return; - } - xtc_result r=xtc_settings_validate(&candidate); - if(r==XTC_OK) r=d->apply_(candidate); - if(r==XTC_OK) { - d->original_=candidate; - d->message_->copy_label("Saved and applied. Audio options take effect on Reconnect; project defaults on New."); - } else { - std::string text=std::string("Preferences unchanged: ")+xtc_result_string(r)+". Check values, loop range and configuration file permissions."; - d->message_->copy_label(text.c_str()); - } - d->redraw(); - } - public: - PreferencesDialog(const xtc_settings &settings,const std::string &path, - std::function apply, - std::function(bool)> devices={}) - : UiDouble_Window(1050,660,"xtc — Preferences"),original_(settings),apply_(std::move(apply)),devices_(std::move(devices)) { - set_modal(); begin(); - auto *location=new UiBox(16,12,1018,30); location->copy_label(path.c_str()); location->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *tabs=new UiTabs(12,50,1026,510); tabs->begin(); - size_t count=0; const auto *schema=xtc_settings_schema(&count); - inputs_.resize(count); std::string section; UiGroup *group=nullptr; UiScroll *scroll=nullptr; int row=0; - for(size_t i=0;iend(); group->end(); } - section=schema[i].section; tabs->begin(); - group=new UiGroup(12,82,1026,478); group->copy_label(section.c_str()); group->begin(); - scroll=new UiScroll(22,94,1000,450); scroll->begin(); row=0; - } - UiInput *input; - if(!strcmp(schema[i].key,"midi_input_port") || !strcmp(schema[i].key,"midi_output_port")){ - auto *choice=new UiInput_Choice(510,108+row*48,480,30,schema[i].label); - choice->callback([](UiWidget *w,void *){auto *c=static_cast(w);auto *item=c->menubutton()->mvalue();if(item && !strcmp(item->label(),"(None)"))c->value("");}); - input=choice->input();if(!strcmp(schema[i].key,"midi_input_port"))midi_input_=choice;else midi_output_=choice; - }else input=new UiInput(510,108+row*48,480,30,schema[i].label); - input->align(UI_ALIGN_LEFT); input->maximum_size(schema[i].capacity ? static_cast(schema[i].capacity-1) : 22); - char value[4096]; xtc_settings_get(&settings,i,value,sizeof(value)); input->value(value); - inputs_[i]=input; ++row; - } - if(scroll) { scroll->end(); group->end(); } tabs->end(); - tabs->begin();auto *midi_info=new UiGroup(12,82,1026,478,"MIDI help"); - auto *help=new UiBox(42,110,950,190,"Choose JACK/PipeWire MIDI ports on the midi tab. Refresh to see newly connected devices.\nYou can also type a remembered port name while it is offline; blank means no automatic route.\nRouting is restored on Play/Record; missing ports are skipped with a warning; playback continues and choices stay saved.\nInput/output channels use 0..15 for MIDI channels 1..16. MIDI thru defaults off.\nNote length/velocity/channel defaults are on editing; CC defaults are on project_defaults.\nHardware MIDI devices must be exposed by your host JACK/PipeWire MIDI bridge."); - help->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP);midi_info->end();tabs->end(); - refresh_devices(); - auto *refresh=new UiButton(16,614,200,30,"Refresh MIDI devices"); - refresh->callback([](UiWidget *,void *c){static_cast(c)->refresh_devices();},this); - message_=new UiBox(16,568,1018,36,"Apply saves every tab. Existing project contents are not overwritten by defaults."); - message_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *apply_button=new UiButton(812,614,100,30,"Apply"); apply_button->callback(apply_callback,this); - auto *close=new UiButton(924,614,110,30,"Close"); - close->callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - end(); xtc_ui::style(this); - } - ~PreferencesDialog() override { - // Destroy borrowed-label menus before the member strings are released. - if(midi_input_)midi_input_->menubutton()->clear(); - if(midi_output_)midi_output_->menubutton()->clear(); - } - void run() { show(); while(shown()) Ui::wait(); } -}; -#endif diff --git a/src/gui/JUCE/sample_editor.cpp b/src/gui/JUCE/sample_editor.cpp deleted file mode 100644 index dc2cbed..0000000 --- a/src/gui/JUCE/sample_editor.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "sample_editor.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include -class SampleCanvas : public UiWidget { - SampleEditor &editor_;size_t anchor_=0; - size_t frame(int px) const {size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom());return std::min(editor_.frames()-1,editor_.origin()+static_cast(std::max(0,std::min(w()-1,px-x())))*width/static_cast(w()));} -public: - SampleCanvas(int x,int y,int w,int h,SampleEditor &e):UiWidget(x,y,w,h),editor_(e){} - int handle(int event) override { - if(event==UI_PUSH && Ui::event_button()==UI_LEFT_MOUSE){anchor_=frame(Ui::event_x());editor_.select(anchor_,anchor_+1);return 1;} - if(event==UI_DRAG){size_t f=frame(Ui::event_x());editor_.select(std::min(f,anchor_),std::max(f,anchor_)+1);return 1;} - return UiWidget::handle(event); - } - void draw() override { - ui_push_clip(x(),y(),w(),h());ui_color(xtc_ui::background());ui_rectf(x(),y(),w(),h()); - size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom()); - for(int px=0;px(px)*width/static_cast(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast(px+1)*width/static_cast(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1; - if(beditor_.begin()){ui_color(ui_rgb_color(45,76,80));ui_rectf(x()+px,y(),1,h());} - xtc_sample_stats stats{};if(xtc_sample_analyze(editor_.sample(),b,e,&stats)==XTC_OK){ui_color(ui_rgb_color(95,204,182));int a=static_cast(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast(std::max(-1.f,std::min(1.f,stats.minimum))*(h()/2-8));ui_line(x()+px,y()+h()/2-a,x()+px,y()+h()/2-z);} - } - ui_color(ui_rgb_color(220,164,92));for(size_t frame:editor_.attacks()){if(frame=width)continue;int px=x()+static_cast((frame-editor_.origin())*static_cast(w())/width);ui_line(px,y(),px,y()+h());} - ui_color(xtc_ui::border());ui_line(x(),y()+h()/2,x()+w(),y()+h()/2);ui_pop_clip(); - } -}; -SampleEditor::SampleEditor(Data sample):UiDouble_Window(1100,650,"xtc — Sample editor"),sample_(std::move(sample)) -{ - if(!sample_)throw std::invalid_argument("Sample editor requires a loaded source"); - xtc_sample_info(sample_.get(),&rate_,&frames_);end_=frames_;set_modal();UiGroup::begin(); - auto *hint=new UiBox(16,12,1068,34,"Source frames / mono downmix — drag a range; numeric endpoints are exclusive. Original file unchanged.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - begin_input_=new UiValue_Input(90,58,140,28,"Start frame");end_input_=new UiValue_Input(330,58,140,28,"End frame");origin_input_=new UiValue_Input(580,58,140,28,"View start"); - for(auto *input:{begin_input_,end_input_,origin_input_}){input->step(1);input->bounds(0,static_cast(frames_));input->when(UI_WHEN_RELEASE);input->callback([](UiWidget *,void *c){auto *e=static_cast(c);double b=e->begin_input_->value(),z=e->end_input_->value(),o=e->origin_input_->value();if(std::isfinite(b)&&std::isfinite(z)&&std::isfinite(o)&&b>=0&&z>=0&&o>=0&&b<=e->frames_&&z<=e->frames_&&oframes_){e->select(static_cast(b),static_cast(z));e->view(static_cast(o),e->zoom_);}e->refresh();},this);} - auto *plus=new UiButton(750,58,100,28,"Zoom in");plus->callback([](UiWidget *,void *c){auto *e=static_cast(c);e->view(e->begin_,std::min(64U,e->zoom_*2));},this); - auto *minus=new UiButton(860,58,100,28,"Zoom out");minus->callback([](UiWidget *,void *c){auto *e=static_cast(c);e->view(e->origin_,std::max(1U,e->zoom_/2));},this); - canvas_=new SampleCanvas(16,104,1068,340,*this);stats_=new UiBox(16,452,1068,50);stats_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *trim_button=new UiButton(16,520,180,30,"Trim clip to selection");trim_button->callback([](UiWidget *,void *c){auto r=static_cast(c)->trim_clip();if(r!=XTC_OK)ui_alert("Clip trim unchanged: %s",xtc_result_string(r));},this); - auto *save=new UiButton(210,520,210,30,"Save full copy and relink...");save->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *path=ui_input("NEW absolute WAV path (full source; project undo keeps the new file)","");if(path){auto r=e->save_copy(path,true);if(r!=XTC_OK)ui_alert("Copy not adopted: %s",xtc_result_string(r));}},this); - auto *export_button=new UiButton(434,520,200,30,"Export selected frames...");export_button->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *path=ui_input("NEW absolute WAV path (selection only)","");if(path){auto r=e->export_selection(path);if(r!=XTC_OK)ui_alert("Export failed: %s",xtc_result_string(r));}},this); - auto *reverse=new UiButton(16,562,160,30,"Reverse selection");reverse->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REVERSE);if(r!=XTC_OK)ui_alert("Reversal failed: %s",xtc_result_string(r));},this); - auto *invert=new UiButton(188,562,160,30,"Invert polarity");invert->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_INVERT);if(r!=XTC_OK)ui_alert("Polarity edit failed: %s",xtc_result_string(r));},this); - auto *dc=new UiButton(360,562,160,30,"Remove DC offset");dc->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REMOVE_DC);if(r!=XTC_OK)ui_alert("DC removal failed: %s",xtc_result_string(r));},this); - auto *fade=new UiButton(532,562,160,30,"Sample fades...");fade->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Fade: 0 linear in, 1 linear out, 2 equal-power in, 3 equal-power out","0");if(!text)return;unsigned int mode;char extra;if(std::sscanf(text,"%u %c",&mode,&extra)!=1||mode>3){ui_alert("Enter a fade mode 0..3.");return;}auto r=e->process(static_cast(XTC_SAMPLE_FADE_IN+mode));if(r!=XTC_OK)ui_alert("Fade failed: %s",xtc_result_string(r));},this); - auto *gain=new UiButton(704,562,160,30,"Gain / normalize...");gain->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Mode (0 gain, 1 normalize), linear gain 0..16 or target peak >0..1","1 0.9");if(!text)return;unsigned int mode;double value;char extra;if(std::sscanf(text,"%u %lf %c",&mode,&value,&extra)!=2||mode>1){ui_alert("Enter mode and linear value.");return;}auto r=e->process(mode ? XTC_SAMPLE_NORMALIZE : XTC_SAMPLE_GAIN,value);if(r!=XTC_OK)ui_alert("Gain edit failed: %s",xtc_result_string(r));},this); - auto *silence=new UiButton(876,562,200,30,"Select audible bounds...");silence->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Silence threshold dBFS (-120..0), padding ms (0..10000)","-60 2");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<0||ms>10000){ui_alert("Enter a threshold and padding within the shown bounds.");return;}auto r=e->select_audible(std::pow(10.,db/20),static_cast(std::min(static_cast(e->frames_),ms*e->rate_/1000)));if(r!=XTC_OK)ui_alert("Audible bounds not found: %s",xtc_result_string(r));},this); - auto *zero=new UiButton(16,604,180,30,"Snap to zero crossings...");zero->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Search radius in source frames (0..65536)","256");if(!text)return;unsigned int radius;char extra;if(std::sscanf(text,"%u %c",&radius,&extra)!=1||radius>65536){ui_alert("Enter a radius 0..65536.");return;}auto r=e->snap_zero_crossings(radius);if(r!=XTC_OK)ui_alert("Selection unchanged: %s",xtc_result_string(r));},this); - auto *attacks=new UiButton(210,604,180,30,"Detect attacks...");attacks->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Attack threshold dBFS (-120..0), minimum gap ms (>0..10000)","-12 80");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<=0||ms>10000){ui_alert("Enter a threshold and positive minimum gap.");return;}auto r=e->detect_attacks(std::pow(10.,db/20),std::max(size_t(1),static_cast(std::min(static_cast(e->frames_),ms*e->rate_/1000))));if(r!=XTC_OK)ui_alert("Attack detection failed: %s",xtc_result_string(r));},this); - auto *slices=new UiButton(404,604,180,30,"Slice clip at attacks");slices->callback([](UiWidget *,void *c){auto r=static_cast(c)->slice_clip();if(r!=XTC_OK)ui_alert("Clip unchanged: %s",xtc_result_string(r));else static_cast(c)->hide();},this); - auto *undo=new UiButton(850,604,110,30,"Undo / redo");undo->callback([](UiWidget *,void *c){auto r=static_cast(c)->undo_sample();if(r!=XTC_OK)ui_alert("No local sample edit to undo.");},this); - auto *close=new UiButton(970,604,110,30,"Close");close->callback([](UiWidget *,void *c){static_cast(c)->hide();},this);callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - UiGroup::end();xtc_ui::style(this);refresh(); -} -xtc_result SampleEditor::select(size_t b,size_t e){if(b>=e||e>frames_)return XTC_ERR_INVALID_ARGUMENT;if(begin_!=b||end_!=e)attacks_.clear();begin_=b;end_=e;refresh();return XTC_OK;} -xtc_result SampleEditor::view(size_t b,unsigned int zoom){if(b>=frames_||!zoom||zoom>64)return XTC_ERR_INVALID_ARGUMENT;origin_=b;zoom_=zoom;refresh();return XTC_OK;} -void SampleEditor::refresh(){begin_input_->value(static_cast(begin_));end_input_->value(static_cast(end_));origin_input_->value(static_cast(origin_));xtc_sample_stats s{};xtc_sample_analyze(sample_.get(),begin_,end_,&s);char text[256];std::snprintf(text,sizeof(text),"%u Hz / %zu source frames / selection %zu..%zu (%0.6f s) / zoom %ux\nMin %.6f Max %.6f RMS %.6f DC %.6f",rate_,frames_,begin_,end_,static_cast(end_-begin_)/rate_,zoom_,s.minimum,s.maximum,s.rms,s.mean);stats_->copy_label(text);std::string attacks="Detected source frames (nearest tick used for slicing):";for(size_t frame:attacks_)attacks+=" "+std::to_string(frame);stats_->copy_tooltip(attacks.c_str());canvas_->redraw();} -void SampleEditor::run(){show();while(shown())Ui::wait();} - -xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC_ERR_INVALID_STATE;} - -xtc_result SampleEditor::save_copy(const char *path,bool use_in_project){if(use_in_project)return adopt ? adopt(sample_.get(),path) : XTC_ERR_INVALID_STATE;return xtc_sample_write(sample_.get(),0,frames_,path);} -xtc_result SampleEditor::export_selection(const char *path){return xtc_sample_write(sample_.get(),begin_,end_,path);} - -xtc_result SampleEditor::process(xtc_sample_operation operation,double amount) -{ - xtc_sample *next=nullptr;auto r=xtc_sample_process(sample_.get(),begin_,end_,operation,amount,&next);if(r!=XTC_OK)return r; - previous_=std::move(sample_);sample_.reset(next);attacks_.clear();copy_label("xtc — Sample editor / local sample changes; save a copy to keep");refresh();return XTC_OK; -} -xtc_result SampleEditor::undo_sample(){if(!previous_)return XTC_ERR_NOT_FOUND;sample_.swap(previous_);attacks_.clear();refresh();return XTC_OK;} - -xtc_result SampleEditor::select_audible(double threshold,size_t padding){size_t b=0,e=0;auto r=xtc_sample_audible_bounds(sample_.get(),begin_,end_,threshold,padding,&b,&e);return r==XTC_OK ? select(b,e) : r;} - -xtc_result SampleEditor::snap_zero_crossings(size_t radius){size_t b=0,e=0;auto r=xtc_sample_zero_crossing(sample_.get(),begin_,radius,&b);if(r==XTC_OK)r=xtc_sample_zero_crossing(sample_.get(),end_,radius,&e);return r==XTC_OK ? select(b,e) : r;} - -xtc_result SampleEditor::detect_attacks(double threshold,size_t gap){size_t positions[XTC_SAMPLE_MAX_TRANSIENTS],count=0;auto r=xtc_sample_transients(sample_.get(),begin_,end_,threshold,gap,positions,XTC_SAMPLE_MAX_TRANSIENTS,&count);if(r!=XTC_OK)return r;try{std::vector next(positions,positions+count);attacks_.swap(next);}catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;}refresh();return XTC_OK;} -xtc_result SampleEditor::slice_clip(){return slice&&!attacks_.empty() ? slice(rate_,attacks_) : XTC_ERR_INVALID_STATE;} diff --git a/src/gui/JUCE/sample_editor.hpp b/src/gui/JUCE/sample_editor.hpp deleted file mode 100644 index c2b35ac..0000000 --- a/src/gui/JUCE/sample_editor.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef XTC_SAMPLE_EDITOR_HPP -#define XTC_SAMPLE_EDITOR_HPP -#include "xtc/xtc_sample.h" -#include "ui.hpp" -#include -#include -#include -class UiValue_Input; -class UiBox; -class SampleCanvas; -class SampleEditor : public UiDouble_Window { -public: - /* Takes ownership of loaded snapshot. Modal edits never touch audio callbacks. */ - using Data=std::unique_ptr; - explicit SampleEditor(Data sample); - std::function trim; - xtc_result trim_clip(); - std::function adopt; - xtc_result save_copy(const char *path,bool use_in_project); - xtc_result export_selection(const char *path); - xtc_result process(xtc_sample_operation operation,double amount=0); - xtc_result undo_sample(); - xtc_result select_audible(double threshold,size_t padding); - xtc_result snap_zero_crossings(size_t radius); - std::function &)> slice; - xtc_result detect_attacks(double threshold,size_t gap); - xtc_result slice_clip(); - const std::vector &attacks() const {return attacks_;} - void run(); - xtc_result select(size_t begin,size_t end); - xtc_result view(size_t begin,unsigned int zoom); - const xtc_sample *sample() const {return sample_.get();} - size_t begin() const {return begin_;} size_t end() const {return end_;} - size_t frames() const {return frames_;} size_t origin() const {return origin_;} - unsigned int zoom() const {return zoom_;} - void refresh(); -private: - std::unique_ptr sample_{nullptr,xtc_sample_destroy}; - std::unique_ptr previous_{nullptr,xtc_sample_destroy}; - std::vector attacks_; - size_t frames_=0,begin_=0,end_=0,origin_=0;unsigned int rate_=0,zoom_=1; - UiValue_Input *begin_input_,*end_input_,*origin_input_;UiBox *stats_;SampleCanvas *canvas_; -}; -#endif diff --git a/src/gui/JUCE/sequencer_clip_widget.cpp b/src/gui/JUCE/sequencer_clip_widget.cpp deleted file mode 100644 index c493c43..0000000 --- a/src/gui/JUCE/sequencer_clip_widget.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "sequencer_clip_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "xtc/xtc_midi.h" -#include "ui.hpp" -#include - -SequencerClipWidget::SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip) - : UiWidget(0,0,1,46), owner_(owner), info_(clip) {} -void SequencerClipWidget::sync(const xtc_clip_info &clip, int top) -{ - info_=clip; - int left=owner_.x()+owner_.header_width_+static_cast(clip.start_tick*static_cast(owner_.scale_)/960U); - int width=std::max(4,static_cast(clip.length_tick*static_cast(owner_.scale_)/960U)); - resize(left,top+5,width,46); -} -void SequencerClipWidget::draw() -{ - const auto &c=info_; - int left=x(),py=y()-5,cx,cy,cw,ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - int a=std::max(left,cx),b=std::min(x()+w(),cx+cw); - if(b<=a) return; - size_t notes=0; xtc_project_note_count(owner_.project_,¬es); - UiColor color=xtc_ui::track(c.track_id); - ui_color(ui_color_average(color,xtc_ui::background(),owner_.clip_selected(c.id) ? .75f : .45f)); - ui_rectf(a,py+5,b-a,46); - ui_color(owner_.clip_selected(c.id) ? xtc_ui::text() : color); ui_rect(a,py+5,b-a,46); - ui_push_clip(a+1,py+6,std::max(0,b-a-2),44); - if (c.midi) { - /* Real note positions, pitch and duration; no placeholder waveform. */ - unsigned int low = 127, high = 0; - for (size_t n = 0; n < notes; ++n) { - xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); - if (note.clip_id == c.id) { low = std::min(low,note.pitch); high = std::max(high,note.pitch); } - } - unsigned int range = high >= low ? std::max(12U,high-low) : 12U; - ui_color(ui_color_average(color,UI_WHITE,.55f)); - for (size_t n = 0; n < notes; ++n) { - xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); - if (note.clip_id != c.id) continue; - uint64_t begin = std::max(note.start_tick,c.offset_tick); - uint64_t end = std::min(note.start_tick+note.length_tick,c.offset_tick+c.length_tick); - if (begin >= end) continue; - int nx = left+static_cast((begin-c.offset_tick)*static_cast(owner_.scale_)/960U); - int nw = std::max(2,static_cast((end-begin)*static_cast(owner_.scale_)/960U)); - ui_rectf(nx,py+26+static_cast((high-note.pitch)*18/range),nw,2); - } - } else owner_.draw_waveform(c,left,py); - ui_color(ui_color_average(color,xtc_ui::background(),.35f)); ui_rectf(a+1,py+6,std::max(0,b-a-2),15); - ui_font(UI_HELVETICA,11); ui_color(xtc_ui::text()); - ui_draw(c.name,left+7,py+18); - ui_pop_clip(); -} - -int SequencerClipWidget::handle(int event) -{ - if(event==UI_PUSH && (Ui::event_button()==UI_LEFT_MOUSE || Ui::event_button()==UI_RIGHT_MOUSE)) { - owner_.take_focus(); - uint64_t previous=owner_.clip_; - owner_.track_=info_.track_id; owner_.clip_=info_.id; - owner_.original_=info_; owner_.drag_x_=Ui::event_x(); - owner_.mode_=Ui::event_x()-x()<7 ? 2 : x()+w()-Ui::event_x()<7 ? 3 : 1; - if(Ui::event_state(UI_SHIFT)) { - owner_.clip_=previous; owner_.toggle_clip(info_.id); owner_.mode_=0; return 1; - } - owner_.selection_.clear(); - if(Ui::event_button()==UI_LEFT_MOUSE && Ui::event_clicks()) { - owner_.mode_=0; - if(owner_.open_clip) owner_.open_clip(owner_.context_); - return 1; - } - if(Ui::event_button()==UI_RIGHT_MOUSE) { owner_.mode_=0; owner_.context_menu(); } - owner_.redraw(); return 1; - } - if(event==UI_DRAG || event==UI_RELEASE) return owner_.handle(event); - return 0; -} diff --git a/src/gui/JUCE/sequencer_clip_widget.hpp b/src/gui/JUCE/sequencer_clip_widget.hpp deleted file mode 100644 index 7aea906..0000000 --- a/src/gui/JUCE/sequencer_clip_widget.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef XTC_SEQUENCER_CLIP_WIDGET_HPP -#define XTC_SEQUENCER_CLIP_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_project.h" -class SequencerWidget; -class SequencerClipWidget : public UiWidget { - SequencerWidget &owner_; - xtc_clip_info info_{}; - public: - SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip); - uint64_t clip_id() const { return info_.id; } - void sync(const xtc_clip_info &clip, int track_top); - int handle(int event) override; - protected: - void draw() override; -}; -#endif diff --git a/src/gui/JUCE/sequencer_commands.cpp b/src/gui/JUCE/sequencer_commands.cpp deleted file mode 100644 index 8fe10a9..0000000 --- a/src/gui/JUCE/sequencer_commands.cpp +++ /dev/null @@ -1,412 +0,0 @@ -#include "sequencer_widget.hpp" -#include "snap.hpp" -#include "xtc/xtc_midi.h" -#include "ui.hpp" -#include -#include -#include -#include -#include - -// User-facing editing commands use the master's single history/publication path. - -xtc_result SequencerWidget::add_track() -{ - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return r; - } - } - uint64_t id; - xtc_result r = xtc_project_add_track(project_, "Track", &id); - if (r == XTC_OK) { - track_ = id; - clip_ = UINT64_MAX;selection_.clear(); - } - changed(r); - return r; -} - -xtc_result SequencerWidget::add_region() -{ - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return r; - } - } - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - if (track_ == UINT64_MAX && tracks) { - xtc_track_info t{}; - xtc_project_get_track(project_, 0, &t); - track_ = t.id; - } - uint64_t id; - xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id); - if (r == XTC_OK) {clip_ = id;selection_.clear();} - changed(r); - return r; -} - -xtc_result SequencerWidget::add_midi_clip() -{ - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - uint64_t id; - if (r == XTC_OK) { - if (track_ == UINT64_MAX) { - xtc_track_info t{}; - r = xtc_project_get_track(project_, 0, &t); - track_ = r == XTC_OK ? t.id : UINT64_MAX; - } - if (r == XTC_OK) - r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id); - if (r == XTC_OK) {clip_ = id;selection_.clear();} - } - changed(r); - return r; -} - -xtc_result SequencerWidget::delete_selection() -{ - if(!selected_clips().empty())return transform_selection(XTC_CLIPS_DELETE,0); - xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_remove_track(project_,track_); - if(r==XTC_OK)track_=clip_=UINT64_MAX; - changed(r);return r; -} - -xtc_result SequencerWidget::merge_midi() -{ - auto ids=selected_clips();uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_merge_midi(project_,ids.data(),ids.size(),&id); - if(r==XTC_OK){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);select(c.track_id,id);} - changed(r);return r; -} - -xtc_result SequencerWidget::duplicate_track(const char *name) -{ - const uint64_t source=track_;uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_duplicate_track(project_,source,name,&id); - if(r==XTC_OK)select(id); - changed(r);return r; -} - -void SequencerWidget::duplicate_track_dialog() -{ - const char *name=ui_input("Name for complete track duplicate","Track copy");if(name)duplicate_track(name); -} - -xtc_result SequencerWidget::ripple_time(uint64_t length,bool remove) -{ - const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_ripple_time(project_,at,length,remove); - changed(r);return r; -} - -void SequencerWidget::ripple_dialog(bool remove) -{ - const char *input=ui_input(remove ? "Remove time from cursor: ticks (split crossing clips first)" : "Insert time at cursor: ticks (split crossing clips first)","3840");if(!input)return; - uint64_t length=0;char extra;std::istringstream values(input); - if(!(values>>length) || (values>>extra) || !length || length>XTC_PROJECT_MAX_TICK){ui_alert("Enter a positive bounded tick duration.");return;} - ripple_time(length,remove); -} - -xtc_result SequencerWidget::marker(const char *name) -{ - const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=name ? xtc_project_set_marker(project_,at,name) : xtc_project_remove_marker(project_,at); - changed(r);return r; -} - -void SequencerWidget::marker_dialog() -{ - const char *name=ui_input("Marker at cursor (1..63 UTF-8 bytes)","Section");if(name)marker(name); -} - -std::vector SequencerWidget::selected_clips() const -{ - std::vector ids;xtc_clip_info c{}; - for(auto id:selection_)if(xtc_project_find_clip(project_,id,&c)==XTC_OK)ids.push_back(id); - if(ids.empty() && clip_!=UINT64_MAX && xtc_project_find_clip(project_,clip_,&c)==XTC_OK)ids.push_back(clip_); - return ids; -} - -bool SequencerWidget::clip_selected(uint64_t id) const -{ - return selection_.empty() ? id==clip_ : std::find(selection_.begin(),selection_.end(),id)!=selection_.end(); -} - -bool SequencerWidget::toggle_clip(uint64_t id) -{ - xtc_clip_info c{};if(xtc_project_find_clip(project_,id,&c)!=XTC_OK)return false; - if(selection_.empty() && clip_!=UINT64_MAX)selection_.push_back(clip_); - auto found=std::find(selection_.begin(),selection_.end(),id); - if(found==selection_.end())selection_.push_back(id);else selection_.erase(found); - clip_=selection_.empty() ? UINT64_MAX : selection_.back(); - if(clip_!=UINT64_MAX)xtc_project_find_clip(project_,clip_,&c); - track_=c.track_id;redraw();return true; -} - -void SequencerWidget::select_all_clips() -{ - selection_.clear();size_t tracks=0,clips=0;xtc_project_get_counts(project_,&tracks,&clips); - for(size_t i=0;i output(ids.size()); - xtc_result r=ids.empty() ? XTC_ERR_NOT_FOUND : XTC_OK; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_transform_clips(project_,ids.data(),ids.size(),op,amount,output.data()); - if(r==XTC_OK) { - selection_=op==XTC_CLIPS_DELETE ? std::vector{} : output; - clip_=selection_.empty() ? UINT64_MAX : selection_.back(); - } - changed(r);return r; -} - -xtc_result SequencerWidget::transform_midi(const xtc_midi_transform &edit) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit); - changed(r);return r; -} - -void SequencerWidget::fades_dialog() -{ - const char *input=ui_input("Symmetric fade length in ticks (0 removes; at most half each clip)","10");if(!input)return; - int64_t ticks=0;char extra;std::istringstream values(input); - if(!(values>>ticks) || (values>>extra) || ticks<0 || ticks>static_cast(XTC_PROJECT_MAX_TICK)){ui_alert("Enter a valid nonnegative tick length.");return;} - transform_selection(XTC_CLIPS_FADE,ticks); -} - -xtc_result SequencerWidget::make_chord(const std::vector &intervals) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_chordify(project_,ids.data(),ids.size(),intervals.data(),intervals.size()); - changed(r);return r; -} - -xtc_result SequencerWidget::arpeggiate(uint64_t step,unsigned int gate,bool descending) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_arpeggiate(project_,ids.data(),ids.size(),step,gate,descending); - changed(r);return r; -} - -xtc_result SequencerWidget::normalize_audio(float target) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_normalize_audio(project_,ids.data(),ids.size(),target); - changed(r);return r; -} - -void SequencerWidget::normalize_dialog() -{ - const char *input=ui_input("Source sample peak target in dBFS (-60..0; excludes fades and mixer)","-1");if(!input)return; - float db=0;char extra;std::istringstream values(input); - if(!(values>>db) || (values>>extra) || !std::isfinite(db) || db < -60 || db>0){ui_alert("Enter a finite sample peak target from -60 to 0 dBFS.");return;} - normalize_audio(std::pow(10.0f,db/20.0f)); -} - -void SequencerWidget::arpeggio_dialog() -{ - const char *input=ui_input("Step ticks (1..3840), gate percent (1..100), direction (0=up, 1=down)","240 80 0");if(!input)return; - uint64_t step=0;unsigned int gate=0;int down=0;char extra;std::istringstream values(input); - if(!(values>>step>>gate>>down) || (values>>extra) || !step || step>3840 || !gate || gate>100 || down<0 || down>1){ui_alert("Enter a valid step, gate and direction.");return;} - arpeggiate(step,gate,down!=0); -} - -void SequencerWidget::chord_dialog() -{ - const char *input=ui_input("Additional semitone intervals: 1..8 unique nonzero values (-127..127)","4 7");if(!input)return; - std::istringstream values(input);std::vector intervals;int interval; - while(values>>interval){intervals.push_back(interval);if(intervals.size()>8)break;} - if(!values.eof() || intervals.empty() || intervals.size()>8){ui_alert("Enter 1 to 8 semitone intervals.");return;} - make_chord(intervals); -} - -void SequencerWidget::humanize_dialog() -{ - const char *input=ui_input("Timing variation ticks (0..960), velocity variation (0..127), seed","12 8 1");if(!input)return; - unsigned int timing=0,velocity=0,seed=0;char extra;std::istringstream values(input); - if(!(values>>timing>>velocity>>seed) || (values>>extra) || timing>960 || velocity>127){ui_alert("Enter bounded timing/velocity variation and a 32-bit seed.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_HUMANIZE;edit.timing_variation=timing;edit.velocity_variation=velocity;edit.seed=seed;transform_midi(edit); -} - -void SequencerWidget::velocity_dialog() -{ - const char *input=ui_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return; - unsigned int percent=0;int offset=0;char extra;std::istringstream values(input); - if(!(values>>percent>>offset) || (values>>extra) || percent>400 || offset < -127 || offset>127){ui_alert("Enter a valid percentage and velocity offset.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=percent;edit.velocity_offset=offset;transform_midi(edit); -} - -void SequencerWidget::scale_dialog() -{ - const char *input=ui_input("Root (0=C..11=B), scale (0=chromatic, 1=major, 2=natural minor)","0 1");if(!input)return; - unsigned int root=0,scale=0;char extra;std::istringstream values(input); - if(!(values>>root>>scale) || (values>>extra) || root>11 || scale>2){ui_alert("Enter a root from 0 to 11 and scale from 0 to 2.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=root;edit.scale=scale;transform_midi(edit); -} - -void SequencerWidget::gate_dialog() -{ - const char *input=ui_input("Note length percentage (1..400)","50");if(!input)return; - unsigned int percent=0;char extra;std::istringstream values(input); - if(!(values>>percent) || (values>>extra) || percent<1 || percent>400){ui_alert("Enter a whole percentage from 1 to 400.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=percent;transform_midi(edit); -} - -void SequencerWidget::swing_dialog() -{ - const char *input=ui_input("Grid ticks (1..3840), swing (0..100), strength (0..100)","240 50 100");if(!input)return; - int grid=0,swing=-1,strength=-1;char extra; - std::istringstream values(input); - if(!(values>>grid>>swing>>strength) || (values>>extra) || grid<1 || grid>3840 || swing<0 || swing>100 || strength<0 || strength>100){ui_alert("Enter a valid grid, swing and strength.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SWING;edit.grid=static_cast(grid);edit.swing=static_cast(swing);edit.strength=static_cast(strength);transform_midi(edit); -} - -xtc_result SequencerWidget::repeat_selection(unsigned int copies) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_repeat_clips(project_,ids.data(),ids.size(),copies); - changed(r);return r; -} - -void SequencerWidget::repeat_dialog() -{ - const char *input=ui_input("Additional phrase copies (1..1024)","3");if(!input)return; - char *end=nullptr;unsigned long copies=std::strtoul(input,&end,10); - if(end==input || *end || copies<1 || copies>1024){ui_alert("Enter a whole number from 1 to 1024.");return;} - repeat_selection(static_cast(copies)); -} - -xtc_result SequencerWidget::copy_selection() -{ - auto ids=selected_clips();xtc_clipboard *copy=nullptr; - xtc_result r=xtc_clipboard_create(project_,ids.data(),ids.size(),©); - if(r==XTC_OK)clipboard_.reset(copy); - else ui_alert("Copy unchanged: %s",xtc_result_string(r)); - return r; -} - -xtc_result SequencerWidget::paste_selection() -{ - const uint64_t start=cursor_,track=track_; - std::vector output(xtc_clipboard_count(clipboard_.get())); - xtc_result r=output.empty() ? XTC_ERR_INVALID_STATE : XTC_OK; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_clipboard_paste(clipboard_.get(),project_,track,start,output.data()); - if(r==XTC_OK){selection_=output;clip_=selection_.back();xtc_clip_info c{};xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;} - changed(r);return r; -} - -xtc_result SequencerWidget::nudge_selection(int direction) -{ - return transform_selection(XTC_CLIPS_MOVE,direction<0 ? -static_cast(grid_) : static_cast(grid_)); -} - -xtc_result SequencerWidget::duplicate_selection() -{ - auto ids=selected_clips();uint64_t begin=XTC_PROJECT_MAX_TICK,end=0; - for(auto id:ids){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);begin=std::min(begin,c.start_tick);end=std::max(end,c.start_tick+c.length_tick);} - return transform_selection(XTC_CLIPS_DUPLICATE,static_cast(end)-static_cast(begin)); -} - -xtc_result SequencerWidget::rename_selection(const char *name) -{ - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - if (r == XTC_OK) r = clip_ != UINT64_MAX ? xtc_project_rename_clip(project_,clip_,name) : xtc_project_rename_track(project_,track_,name); - changed(r); return r; -} - -xtc_result SequencerWidget::move_track(int direction) -{ - size_t tracks, clips, i; xtc_project_get_counts(project_,&tracks,&clips); - xtc_track_info track{}; - for (i = 0; i < tracks; ++i) { xtc_project_get_track(project_,i,&track); if (track.id == track_) break; } - if (i == tracks || (direction < 0 ? i == 0 : i+1 == tracks)) return XTC_ERR_NOT_FOUND; - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_move_track(project_,track_,direction < 0 ? i-1 : i+1); - changed(r); return r; -} - -xtc_result SequencerWidget::split_selection() -{ - uint64_t right; - const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead. - xtc_result result = before_edit ? before_edit(context_) : XTC_OK; - if (result == XTC_OK) { - xtc_clip_info c{};result=xtc_project_find_clip(project_,clip_,&c); - if(result==XTC_OK)result=c.midi ? xtc_project_split_midi(project_,clip_,split_tick,&right) : xtc_project_split_audio(project_,clip_,split_tick,&right); - } - if (result == XTC_OK) {clip_ = right;selection_.clear();} - changed(result); - return result; -} - -xtc_result SequencerWidget::audio_properties(float gain, uint64_t in, uint64_t out) -{ - xtc_result result = before_edit ? before_edit(context_) : XTC_OK; - if (result == XTC_OK) result = xtc_project_set_clip_audio(project_, clip_, gain, in, out); - changed(result); - return result; -} - -void SequencerWidget::audio_properties_dialog() -{ - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || clip.midi) { - ui_alert("Select an audio clip or silent region first."); - return; - } - char defaults[128]; - std::snprintf(defaults, sizeof(defaults), "%.9g %llu %llu", static_cast(clip.gain), - static_cast(clip.fade_in_tick), static_cast(clip.fade_out_tick)); - const char *text = ui_input("Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables)", defaults); - if (!text) return; - std::istringstream input(text); - float gain; uint64_t in, out; - if (!(input >> gain >> in >> out) || !(input >> std::ws).eof()) { - ui_alert("Enter gain and two fade durations in ticks."); return; - } - audio_properties(gain, in, out); -} - -xtc_result SequencerWidget::crossfade_selection() -{ - auto ids=selected_clips();xtc_result r=ids.size()==2 ? XTC_OK : XTC_ERR_INVALID_ARGUMENT; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_crossfade(project_,ids[0],ids[1]); - changed(r);return r; -} - -xtc_result SequencerWidget::strum_chords(uint64_t spacing,bool descending) -{ - auto ids=selected_clips();xtc_result r=XTC_OK; - if(before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_strum(project_,ids.data(),ids.size(),spacing,descending ? 1 : 0); - changed(r);return r; -} - -void SequencerWidget::strum_dialog() -{ - const char *input=ui_input("Spacing ticks (1..960), direction (0=up, 1=down)","30 0");if(!input)return; - uint64_t spacing=0;int down=0;char extra;std::istringstream values(input); - if(!(values>>spacing>>down) || (values>>extra) || !spacing || spacing>960 || down<0 || down>1){ui_alert("Enter a valid spacing and direction.");return;} - strum_chords(spacing,down!=0); -} diff --git a/src/gui/JUCE/sequencer_group_scope.hpp b/src/gui/JUCE/sequencer_group_scope.hpp deleted file mode 100644 index dabcb02..0000000 --- a/src/gui/JUCE/sequencer_group_scope.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef XTC_SEQUENCER_GROUP_SCOPE_HPP -#define XTC_SEQUENCER_GROUP_SCOPE_HPP -#include "ui.hpp" -class SequencerGroupScope { - UiGroup *previous_; - public: - explicit SequencerGroupScope(UiGroup *group) : previous_(UiGroup::current()) { UiGroup::current(group); } - ~SequencerGroupScope() { UiGroup::current(previous_); } - SequencerGroupScope(const SequencerGroupScope &) = delete; - SequencerGroupScope &operator=(const SequencerGroupScope &) = delete; -}; -#endif diff --git a/src/gui/JUCE/sequencer_instruments.cpp b/src/gui/JUCE/sequencer_instruments.cpp deleted file mode 100644 index bd5c330..0000000 --- a/src/gui/JUCE/sequencer_instruments.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "sequencer_widget.hpp" -#include "sequencer_track_widget.hpp" -#include -#include - -UiChoice *SequencerWidget::instrument_choice(uint64_t track) const -{ - auto *view=track_widget(track); - return view && !view->is_bus() ? view->instrument_choice() : nullptr; -} - -void SequencerWidget::refresh_instruments() try -{ - sync_views(); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - catalog_.clear(); - for (unsigned int i=0;i<2;++i) { xtc_plugin_spec s{}; if (xtc_plugin_probe(XTC_BUILTIN_PATH,i,48000,&s)==XTC_OK) catalog_.push_back(s); } - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info t{}; xtc_plugin_spec spec{}; - xtc_project_get_track(project_,i,&t); - if (xtc_project_get_instrument(project_,t.id,&spec) != XTC_OK) continue; - bool present = false; - for (const auto &item : catalog_) if (!std::strcmp(item.path,spec.path) && item.index == spec.index) present = true; - if (!present) catalog_.push_back(spec); - } - for(auto *view:track_widgets_) { - auto *choice=view->instrument_choice(); - xtc_track_info t{}; - for(size_t i=0;itrack_id()) break;} - if(t.is_bus) {choice->hide(); continue;} - choice->clear(); choice->add("Built-in sine"); - xtc_plugin_spec current{}; int selected = 0; - bool assigned = xtc_project_get_instrument(project_,t.id,¤t) == XTC_OK; - for (size_t j = 0; j < catalog_.size(); ++j) { - std::string name = std::string(xtc_plugin_format_name(catalog_[j].format))+": "+catalog_[j].label; - int item = choice->add("Instrument"); choice->replace(item,name.c_str()); - if (assigned && !std::strcmp(current.path,catalog_[j].path) && current.index == catalog_[j].index) selected = static_cast(j)+1; - } - choice->add("Load plugin..."); choice->value(selected); choice->show(); - } -} catch (const std::bad_alloc &) { - catalog_.clear(); - for(auto *view:track_widgets_) view->instrument_choice()->hide(); -} - -void SequencerWidget::instrument_callback(UiWidget *widget, void *context) -{ - auto *owner = static_cast(context); - auto *choice = static_cast(widget); - for (auto *view : owner->track_widgets_) if (view->instrument_choice() == choice) { - uint64_t track = view->track_id(); - int index = choice->value(); - bool browse = index == static_cast(owner->catalog_.size())+1; - xtc_plugin_spec spec{}; - if (index > 0 && !browse) { - if (static_cast(index) > owner->catalog_.size()) return; - spec = owner->catalog_[static_cast(index)-1]; - } - if (owner->instrument_select) owner->instrument_select(owner->context_,track,index > 0 && !browse ? &spec : nullptr,browse); - owner->refresh_instruments(); owner->redraw(); return; - } -} diff --git a/src/gui/JUCE/sequencer_ruler_widget.cpp b/src/gui/JUCE/sequencer_ruler_widget.cpp deleted file mode 100644 index 2370367..0000000 --- a/src/gui/JUCE/sequencer_ruler_widget.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "sequencer_ruler_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include - -SequencerRulerWidget::SequencerRulerWidget(SequencerWidget &owner) - : UiWidget(owner.x(),owner.y(),owner.w(),28), owner_(owner) {} -void SequencerRulerWidget::draw() -{ - int cx,cy,cw,ch; ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_push_clip(x(),y(),w(),h()); - ui_font(UI_HELVETICA,11); - int first=std::max(0,(cx-owner_.x()-owner_.header_width_)/(owner_.scale_*4)); - int last=(cx+cw-owner_.x()-owner_.header_width_)/(owner_.scale_*4)+1; - for(int bar=first;bar<=last;++bar) { - char label[32]; std::snprintf(label,sizeof(label),"%d",bar+1); - ui_color(xtc_ui::muted()); ui_draw(label,owner_.x()+owner_.header_width_+bar*owner_.scale_*4+6,y()+19); - } - uint64_t begin=owner_.mode_==4 ? std::min(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_begin_; - uint64_t end=owner_.mode_==4 ? std::max(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_end_; - int left=owner_.x()+owner_.header_width_+static_cast(begin*static_cast(owner_.scale_)/960U); - int right=owner_.x()+owner_.header_width_+static_cast(end*static_cast(owner_.scale_)/960U); - ui_color(owner_.loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); ui_rectf(left,y()+23,std::max(0,right-left),4); - size_t markers=0; xtc_project_marker_count(owner_.project_,&markers); - for(size_t i=0;i(marker.tick*static_cast(owner_.scale_)/960U); - if(pxcx+cw) continue; - ui_push_clip(px,y(),160,12); ui_font(UI_HELVETICA,10); ui_color(xtc_ui::accent()); ui_draw(marker.name,px+3,y()+10); ui_pop_clip(); - } - int playhead=owner_.x()+owner_.header_width_+static_cast(owner_.cursor_*static_cast(owner_.scale_)/960U); - ui_color(xtc_ui::accent()); ui_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6); - int pinned=owner_.header_left(); - ui_color(xtc_ui::background()); ui_rectf(pinned,y(),owner_.header_width_,28); - ui_color(xtc_ui::muted()); ui_font(UI_HELVETICA,10); ui_draw("TRACKS / drag edge to resize",pinned+10,y()+19); - ui_pop_clip(); -} -int SequencerRulerWidget::handle(int event) -{ - if (event==UI_DRAG && owner_.mode_==4) {owner_.loop_preview_=owner_.tick(Ui::event_x());owner_.redraw();return 1;} - if (event==UI_RELEASE && owner_.mode_==4) { - owner_.loop_preview_=owner_.tick(Ui::event_x()); owner_.mode_=0; - uint64_t begin=std::min(owner_.loop_anchor_,owner_.loop_preview_),end=std::max(owner_.loop_anchor_,owner_.loop_preview_); - if(end>begin && owner_.loop_select) { - xtc_result r=owner_.loop_select(owner_.context_,begin,end); - if(r!=XTC_OK) ui_alert("Loop unchanged: %s",xtc_result_string(r)); - } - owner_.redraw();return 1; - } - - if(event==UI_PUSH && (Ui::event_button()==UI_LEFT_MOUSE || Ui::event_button()==UI_RIGHT_MOUSE)) { - owner_.take_focus(); owner_.mode_=0; - if(Ui::event_x() -#include - -SequencerTrackWidget::SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track) - : UiGroup(owner.x(),owner.y()+28,owner.w(),56), owner_(owner), info_(track) -{ - resizable(nullptr); - clips_.reserve(XTC_PROJECT_MAX_CLIPS); - velocity_=new SequencerVelocityWidget(owner,track.id); - header_=new SequencerTrackHeader(*this); - end(); -} - -UiChoice *SequencerTrackWidget::instrument_choice() const { return header_->instrument_choice(); } -UiGroup *SequencerTrackWidget::header() const { return header_; } -SequencerClipWidget *SequencerTrackWidget::clip_widget(uint64_t clip) const -{ - for(auto *view:clips_) if(view->clip_id()==clip) return view; - return nullptr; -} - -void SequencerTrackWidget::sync(size_t row, const xtc_track_info &track) -{ - row_=row; info_=track; - resize(owner_.x(),owner_.track_top(row),owner_.w(),owner_.track_height(row)); - header_->resize(owner_.header_left(),y(),owner_.header_width_,h()); - instrument_choice()->resize(header_->x()+10,y()+30,header_->w()-22,20); - if(info_.is_bus) instrument_choice()->hide(); - velocity_->resize(owner_.x()+owner_.header_width_,y()+56,owner_.w()-owner_.header_width_,88); - if(owner_.track_expanded(info_.id)) velocity_->show(); else velocity_->hide(); - for(auto it=clips_.begin();it!=clips_.end();) { - xtc_clip_info c{}; - if(xtc_project_find_clip(owner_.project_,(*it)->clip_id(),&c)!=XTC_OK || c.track_id!=info_.id) { - auto *removed=*it; it=clips_.erase(it); - removed->hide(); remove(removed); Ui::delete_widget(removed); - } else ++it; - } - size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - SequencerGroupScope scope(this); - for(size_t i=0;isync(c,y()); - // Match dense model order for overlapping clip drawing and hit testing. - insert(*view,header_); - } -} - -void SequencerTrackWidget::draw() -{ - ui_push_clip(x(),y(),w(),h()); - draw_children(); - ui_pop_clip(); -} - -int SequencerTrackWidget::handle(int event) -{ - if(event==UI_PUSH) { - if(Ui::event_inside(header_)) return header_->handle(event); - if(velocity_->visible() && Ui::event_inside(velocity_)) return velocity_->handle(event); - // Preserve the full-row clip hit area, including its small vertical margins. - for(int i=children()-1;i>=0;--i) { - auto *view=dynamic_cast(child(i)); - if(view && Ui::event_x()>=view->x() && Ui::event_x()x()+view->w()) return view->handle(event); - } - owner_.take_focus(); owner_.track_=info_.id; owner_.clip_=UINT64_MAX; - owner_.selection_.clear(); owner_.mode_=0; - if(Ui::event_button()==UI_RIGHT_MOUSE) owner_.context_menu(); - owner_.redraw(); return 1; - } - if(event==UI_DRAG || event==UI_RELEASE) return owner_.handle(event); - return UiGroup::handle(event); -} - -SequencerTrackHeader::SequencerTrackHeader(SequencerTrackWidget &track) - : UiGroup(track.x(),track.y(),180,56), track_(track) -{ - resizable(nullptr); - instrument_=new UiChoice(x()+10,y()+30,w()-22,20); - xtc_ui::style(instrument_); instrument_->textsize(10); instrument_->textcolor(xtc_ui::text()); - instrument_->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin..."); - instrument_->callback(SequencerWidget::instrument_callback,&track.owner_); - end(); -} - -void SequencerTrackHeader::draw() -{ - auto &owner=track_.owner_; const auto &t=track_.info_; - ui_push_clip(x(),y(),w(),h()); - ui_color(t.id==owner.track_ ? ui_rgb_color(49,62,70) : xtc_ui::panel()); - ui_rectf(x(),y(),w()-2,h()-1); - ui_color(xtc_ui::track(t.id)); ui_rectf(x(),y(),3,h()-1); - ui_push_clip(x()+9,y(),w()-18,55); - ui_color(xtc_ui::text()); ui_font(UI_HELVETICA_BOLD,12); - if(owner.track_expanded(t.id)) ui_polygon(x()+10,y()+10,x()+20,y()+10,x()+15,y()+16); - else ui_polygon(x()+12,y()+8,x()+12,y()+18,x()+18,y()+13); - ui_draw(t.name,x()+26,y()+18); - if(t.is_bus) { - char detail[64]; std::snprintf(detail,sizeof(detail),"%02zu BUS%s%s",track_.row_+1,t.mute ? " MUTE" : "",t.solo ? " SOLO" : ""); - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); ui_draw(detail,x()+12,y()+42); - } - if(t.mute || t.solo) { - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); ui_draw(t.mute ? "M" : "S",x()+w()-16,y()+18); - } - ui_pop_clip(); - if(owner.track_expanded(t.id)) { - ui_color(xtc_ui::muted()); ui_font(UI_HELVETICA,11); - bool audio=track_.velocity_->audio(); - ui_draw(audio ? "AUDIO VELOCITY" : "VELOCITY 1..127",x()+12,y()+76); - ui_font(UI_HELVETICA,10); - ui_draw(audio ? "Level envelope 0..400%" : "Drag bars to edit notes",x()+12,y()+94); - char value[32]; if(track_.velocity_->preview_label(value,sizeof(value))) ui_draw(value,x()+12,y()+114); - } - draw_children(); - ui_pop_clip(); -} - -int SequencerTrackHeader::handle(int event) -{ - auto &owner=track_.owner_; - if(event==UI_PUSH) { - if(instrument_->visible() && Ui::event_inside(instrument_)) return UiGroup::handle(event); - owner.take_focus(); - if(Ui::event_button()==UI_LEFT_MOUSE && Ui::event_x()>=x()+7 && Ui::event_x()=y()+56) return 1; - owner.track_=track_.info_.id; owner.clip_=UINT64_MAX; owner.selection_.clear(); owner.mode_=0; - if(Ui::event_button()==UI_RIGHT_MOUSE) owner.context_menu(); - owner.redraw(); return 1; - } - return UiGroup::handle(event); -} diff --git a/src/gui/JUCE/sequencer_track_widget.hpp b/src/gui/JUCE/sequencer_track_widget.hpp deleted file mode 100644 index 45900a8..0000000 --- a/src/gui/JUCE/sequencer_track_widget.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef XTC_SEQUENCER_TRACK_WIDGET_HPP -#define XTC_SEQUENCER_TRACK_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_project.h" -#include -class SequencerWidget; -class SequencerClipWidget; -class SequencerVelocityWidget; -class SequencerTrackHeader; -class SequencerTrackWidget : public UiGroup { - SequencerWidget &owner_; - xtc_track_info info_{}; - size_t row_ = 0; - SequencerTrackHeader *header_; - SequencerVelocityWidget *velocity_; - std::vector clips_; // Borrowed; JUCE owns children. - friend class SequencerTrackHeader; - public: - SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track); - uint64_t track_id() const { return info_.id; } - bool is_bus() const { return info_.is_bus != 0; } - void sync(size_t row, const xtc_track_info &track); - UiChoice *instrument_choice() const; - SequencerClipWidget *clip_widget(uint64_t clip) const; - SequencerVelocityWidget *velocity_lane() const { return velocity_; } - UiGroup *header() const; - int handle(int event) override; - protected: - void draw() override; -}; -class SequencerTrackHeader : public UiGroup { - SequencerTrackWidget &track_; - UiChoice *instrument_; - public: - explicit SequencerTrackHeader(SequencerTrackWidget &track); - UiChoice *instrument_choice() const { return instrument_; } - int handle(int event) override; - protected: - void draw() override; -}; -#endif diff --git a/src/gui/JUCE/sequencer_velocity_widget.cpp b/src/gui/JUCE/sequencer_velocity_widget.cpp deleted file mode 100644 index 0b519a5..0000000 --- a/src/gui/JUCE/sequencer_velocity_widget.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "sequencer_velocity_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -SequencerVelocityWidget::SequencerVelocityWidget(SequencerWidget &owner, uint64_t track) - : UiWidget(0,0,1,88), owner_(owner), track_(track) {} -bool SequencerVelocityWidget::preview_label(char *text, size_t size) const -{ - if(owner_.active_velocity_!=this) return false; - if(owner_.mode_==7) std::snprintf(text,size,"Level: %.0f%%",audio_preview_.value*100); - else if(owner_.mode_==6) std::snprintf(text,size,"Velocity: %u",preview_); - else return false; - return true; -} -bool SequencerVelocityWidget::note_position(const xtc_note_info ¬e, int &px) const -{ - xtc_clip_info c{}; - if(xtc_project_find_clip(owner_.project_,note.clip_id,&c)!=XTC_OK || c.track_id!=track_ || !c.midi || - note.start_tick=c.length_tick) return false; - px=owner_.x()+owner_.header_width_+static_cast((c.start_tick+note.start_tick-c.offset_tick)*static_cast(owner_.scale_)/960U); - return true; -} - -bool SequencerVelocityWidget::audio() const -{ - size_t tracks=0, clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - bool midi=false; - for(size_t i=0;i(y()+h()-8-py)*4.0f/(h()-28),0.0f,4.0f); -} - -void SequencerVelocityWidget::draw_audio(const xtc_track_info &track, int left, int right) -{ - std::array points{}; - size_t count=0,total=0; xtc_project_automation_count(owner_.project_,&total); - for(size_t i=0;i(t*static_cast(owner_.scale_)/960U);}; - auto py=[&](float v){return y()+h()-8-static_cast(v*(h()-28)/4.0f);}; - ui_color(xtc_ui::track(track.id)); - if(!count) ui_line(left,py(track.gain),right,py(track.gain)); - else { - ui_line(left,py(points[0].value),px(points[0].tick),py(points[0].value)); - for(size_t i=1;iright+4) continue; - ui_rectf(point_x-4,py(points[i].value)-3,8,6); - } - } -} - -unsigned int SequencerVelocityWidget::note_value(int py) const -{ - return static_cast(std::clamp((y()+h()-8-py)*127/(h()-28),1,127)); -} - -void SequencerVelocityWidget::draw() -{ - int cx,cy,cw,ch; ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - int left=std::max(cx,owner_.header_left()+owner_.header_width_); - ui_push_clip(left,y(),std::max(0,cx+cw-left),h()); - ui_color(xtc_ui::border()); ui_line(x(),y(),x()+w(),y()); - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); - bool is_audio=audio(); - ui_draw(is_audio ? "Click anywhere to add a level point; drag vertically; right-click a point to remove" : "Drag a note bar to change velocity",left+8,y()+14); - xtc_track_info track{}; - size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - for(size_t i=0;i(value)*(h()-28)/127; - ui_color(editing ? xtc_ui::text() : xtc_ui::track(track_)); - ui_rectf(px-2,ny,4,bottom-ny); ui_rectf(px-4,ny-2,8,4); - } - ui_pop_clip(); -} - -int SequencerVelocityWidget::handle(int event) -{ - if(owner_.mode_==7) { - if(event==UI_KEYDOWN && Ui::event_key()==UI_Escape) { owner_.mode_=0; owner_.redraw(); return 1; } - if(event==UI_DRAG) { audio_preview_.value=audio_value(Ui::event_y()); owner_.redraw(); return 1; } - if(event==UI_RELEASE) { - owner_.mode_=0; - auto point=audio_preview_; - point.value=audio_value(Ui::event_y()); - if(audio_existing_) { - size_t count=0; xtc_project_automation_count(owner_.project_,&count); - for(size_t i=0;i(std::max(0,Ui::event_x()-owner_.x()-owner_.header_width_))*960/static_cast(owner_.scale_)); - audio_preview_={track_,position,XTC_AUTOMATION_GAIN,audio_value(Ui::event_y()),0}; - audio_existing_=false; - size_t count=0; xtc_project_automation_count(owner_.project_,&count); int distance=12; - for(size_t i=0;i(a.tick*static_cast(owner_.scale_)/960U); - int py=y()+h()-8-static_cast(a.value*(h()-28)/4.0f); - int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-py); - if(d(note.velocity)*(h()-28)/127; - int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-ny); - if(d -#include - -void SequencerWidget::refresh_waveforms() try -{ - // Control-thread loading only. Keep cached failures as well as successes; - // removed/relinked paths are evicted. Reopening rebuilds externally changed files. - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - std::vector next; - for (size_t i = 0; i < clips && next.size() < 64; ++i) { - xtc_clip_info c{}; - xtc_project_get_clip(project_, i, &c); - if (!c.asset_path[0]) continue; - bool present = false; - for (const auto &wave : next) if (wave.path == c.asset_path) { present = true; break; } - if (present) continue; - Wave entry; - entry.path = c.asset_path; - for (auto &wave : waves_) if (wave.path == entry.path) { - entry.data = std::move(wave.data); present = true; break; - } - if (!present) { - xtc_waveform *summary = nullptr; - (void)xtc_waveform_create(c.asset_path, &summary); - entry.data.reset(summary); - } - next.push_back(std::move(entry)); - } - waves_ = std::move(next); -} catch (const std::bad_alloc &) { - waves_.clear(); // Preview failure must not interrupt the model/history edit. -} - -void SequencerWidget::draw_waveform(const xtc_clip_info &c, int left, int top) -{ - const xtc_waveform *summary = nullptr; - for (const auto &wave : waves_) if (wave.path == c.asset_path) { summary = wave.data.get(); break; } - if (!summary) return; - unsigned int rate; size_t frames, bins; - if (xtc_waveform_info(summary, &rate, &frames, &bins) != XTC_OK) return; - double ticks_per_frame = static_cast(xtc_project_tempo(project_)) * 960 / (rate * 60000.0); - ui_color(ui_rgb_color(30, 65, 80)); - for (size_t bin = 0; bin < bins; ++bin) { - double start = static_cast(bin * frames / bins) * ticks_per_frame; - double end = static_cast((bin+1) * frames / bins) * ticks_per_frame; - double offset = static_cast(c.offset_tick); - if (end <= offset || start >= offset + static_cast(c.length_tick)) continue; - int px = left + static_cast((std::max(start, offset) - offset) * scale_ / 960); - int width = std::max(1, static_cast((end - std::max(start, offset)) * scale_ / 960)); - float low, high; - if (xtc_waveform_peak(summary, bin, &low, &high) != XTC_OK) continue; - int y1 = top + 28 - static_cast(std::clamp(high, -1.0f, 1.0f) * 19); - int y2 = top + 28 - static_cast(std::clamp(low, -1.0f, 1.0f) * 19); - ui_rectf(px, y1, width, std::max(1, y2-y1+1)); - } -} diff --git a/src/gui/JUCE/sequencer_widget.cpp b/src/gui/JUCE/sequencer_widget.cpp deleted file mode 100644 index d3ae57e..0000000 --- a/src/gui/JUCE/sequencer_widget.cpp +++ /dev/null @@ -1,382 +0,0 @@ -#include "sequencer_widget.hpp" -#include "sequencer_track_widget.hpp" -#include "sequencer_velocity_widget.hpp" -#include "sequencer_ruler_widget.hpp" -#include "sequencer_group_scope.hpp" -#include "theme.hpp" -#include "snap.hpp" -#include "ui.hpp" -#include -#include - -SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context) - : UiGroup(left, top, 6300, 3700), project_(project), seek_(seek), context_(context) -{ - expanded_tracks_.fill(UINT64_MAX); - resizable(nullptr); - track_widgets_.reserve(XTC_PROJECT_MAX_TRACKS); - ruler_=new SequencerRulerWidget(*this); - extent(); - end(); -} - -bool SequencerWidget::select(uint64_t track, uint64_t clip) -{ - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - bool found = false; - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info info{}; - if (xtc_project_get_track(project_,i,&info) == XTC_OK && info.id == track) found = true; - } - if (!found) return false; - if (clip != UINT64_MAX) { - xtc_clip_info info{}; - if (xtc_project_find_clip(project_,clip,&info) != XTC_OK || info.track_id != track) return false; - } - selection_.clear(); - track_ = track; clip_ = clip; mode_ = 0; redraw(); - return true; -} - -void SequencerWidget::set_project(xtc_project *project, bool reset, bool preserve_expansion) -{ - if(project_ != project) { mode_=0; active_velocity_=nullptr; } - project_ = project; - if (reset) { - if(!preserve_expansion) expanded_tracks_.fill(UINT64_MAX); - waves_.clear(); - selection_.clear(); - track_ = clip_ = UINT64_MAX; - cursor_ = 0; - mode_ = 0; - } - refresh_waveforms(); - extent(); - redraw(); -} - -int SequencerWidget::header_left() const -{ - auto *scroll=dynamic_cast(parent());return scroll ? std::max(x(),scroll->x()) : x(); -} - -void SequencerWidget::header_width(int width) -{ - width=std::clamp(width,180,360);if(width==header_width_)return; - auto *scroll=dynamic_cast(parent());int sx=scroll ? scroll->xposition() : 0,sy=scroll ? scroll->yposition() : 0; - header_width_=width;extent();if(scroll)scroll->scroll_to(sx,sy);redraw(); -} - -uint64_t SequencerWidget::tick(int px) const -{ - int64_t raw = static_cast(std::max(0,px-x()-header_width_))*960/scale_; - return std::min(XTC_PROJECT_MAX_TICK,static_cast(xtc_ui::snap_position(raw,grid_))); -} - -bool SequencerWidget::track_expanded(uint64_t track) const -{ - return track != UINT64_MAX && std::find(expanded_tracks_.begin(),expanded_tracks_.end(),track) != expanded_tracks_.end(); -} - -bool SequencerWidget::expand_track(uint64_t track, bool expanded) -{ - size_t tracks=0, clips=0; - xtc_project_get_counts(project_,&tracks,&clips); - bool found=false; - for(size_t i=0;i=top && py(i); - top=bottom; - } - return -1; -} - -void SequencerWidget::extent() -{ - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - for(auto &id:expanded_tracks_) { - if(id==UINT64_MAX) continue; - bool found=false; - for(size_t i=0;i(end * static_cast(scale_) / 960U + static_cast(header_width_+20)), - std::max(400, track_top(tracks)-y()+72)); - sync_views(); - refresh_instruments(); - if (parent()) - parent()->redraw(); -} - -void SequencerWidget::changed(xtc_result result) -{ - if (result != XTC_OK) - ui_alert("%s", xtc_result_string(result)); - else { - refresh_waveforms(); - extent(); - do_callback(); - } - redraw(); -} - -void SequencerWidget::zoom(int direction) -{ - scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); - extent(); - redraw(); -} - -xtc_result SequencerWidget::set_zoom(int scale) -{ - if(scale!=24 && scale!=48 && scale!=96 && scale!=192 && scale!=384) return XTC_ERR_INVALID_ARGUMENT; - scale_=scale;extent();redraw();return XTC_OK; -} - -void SequencerWidget::draw() -{ - // Reconcile on project/layout changes and input, never during JUCE painting. - ui_push_clip(x(),y(),w(),h()); - int cx, cy, cw, ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_color(xtc_ui::background()); ui_rectf(cx,cy,cw,ch); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - for (size_t i = 0; i < tracks; ++i) { - int py = track_top(i); - if (py+track_height(i) < cy || py > cy+ch) continue; - ui_color(i%2 ? ui_rgb_color(30,34,40) : ui_rgb_color(33,37,43)); - ui_rectf(x()+header_width_,py,w()-header_width_,track_height(i)-1); - } - ui_font(UI_HELVETICA,11); - uint64_t visual = grid_ == 1 ? 960 : grid_; - while (visual*static_cast(scale_)/960 < 6) visual *= 2; - int spacing = static_cast(visual*static_cast(scale_)/960); - int first = std::max(0,(cx-x()-header_width_)/spacing); - int last = (cx+cw-x()-header_width_)/spacing+1; - for (int step = first; step <= last; ++step) { - uint64_t tick = static_cast(step)*visual; - int px = x()+header_width_+static_cast(tick*static_cast(scale_)/960); - ui_color(tick%3840 == 0 ? ui_rgb_color(76,84,96) : tick%960 == 0 ? ui_rgb_color(56,63,73) : ui_rgb_color(42,48,56)); - ui_line(px,y()+28,px,y()+h()); - } - if (!tracks) { - ui_font(UI_HELVETICA_BOLD,18); ui_color(xtc_ui::text()); - ui_draw("Your next track starts here",x()+220,y()+105); - ui_font(UI_HELVETICA,13); ui_color(xtc_ui::muted()); - ui_draw("Create a MIDI track or import audio from the panel on the left.",x()+220,y()+135); - ui_draw("Arrange clips here. Open the piano roll to draw and edit MIDI notes.",x()+220,y()+161); - } - ui_pop_clip(); -} -void SequencerWidget::draw_overlay() -{ - int cx,cy,cw,ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_push_clip(x(),y(),w(),h()); - size_t markers=0;xtc_project_marker_count(project_,&markers); - for(size_t i=0;i(marker.tick*static_cast(scale_)/960U); - if(pxcx+cw)continue; - ui_color(xtc_ui::accent());ui_line(px,y()+24,px,y()+h()); - } - int playhead = x()+header_width_+static_cast(cursor_*static_cast(scale_)/960U); - if (playhead >= std::max(cx,header_left()+header_width_) && playhead <= cx+cw) { - ui_color(xtc_ui::accent()); ui_line(playhead,y(),playhead,y()+h()); - } - ui_color(xtc_ui::muted()); ui_rectf(header_left()+header_width_-3,cy,3,ch); - ui_pop_clip(); -} - -int SequencerWidget::handle(int event) -{ - if((mode_==6 || mode_==7) && active_velocity_ && - (event==UI_DRAG || event==UI_RELEASE || (event==UI_KEYDOWN && Ui::event_key()==UI_Escape))) - return active_velocity_->handle(event); - if(mode_==4 && (event==UI_DRAG || event==UI_RELEASE)) return ruler_->handle(event); - if(event==UI_PUSH && Ui::event_button()==UI_LEFT_MOUSE && Ui::event_x()>=header_left()+header_width_-5 && Ui::event_x()instrument_choice(); - if(((event==UI_DRAG || event==UI_RELEASE) && Ui::pushed()==choice) || - (event==UI_KEYDOWN && Ui::focus()==choice)) return choice->handle(event); - } - if(event==UI_PUSH) { - sync_views(); - if(Ui::event_y()handle(event); - mode_=0; - UiGroup::handle(event); - return 1; - } - if (event == UI_DRAG) - return mode_ != 0; - if (event == UI_RELEASE && mode_) { - int mode = mode_; - mode_ = 0; - int64_t delta = static_cast(Ui::event_x() - drag_x_) * 960 / scale_; - int64_t start = static_cast(original_.start_tick); - int64_t length = static_cast(original_.length_tick); - int64_t offset = static_cast(original_.offset_tick); - uint64_t target = original_.track_id; - if (mode == 1) { - start = xtc_ui::snap_position(start+delta,grid_); - int row = track_row(Ui::event_y()); - xtc_track_info t{}; - if (row >= 0 && xtc_project_get_track(project_, static_cast(row), &t) == XTC_OK) - target = t.id; - } else if (mode == 2) { - delta = xtc_ui::snap_position(start+delta,grid_)-start; - start += delta; - offset += delta; - length -= delta; - } else - length = xtc_ui::snap_position(start+length+delta,grid_)-start; - if (start < 0 || offset < 0 || length <= 0) - return 1; - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return 1; - } - } - changed(xtc_project_edit_clip(project_, clip_, target, static_cast(start), - static_cast(length), static_cast(offset))); - return 1; - } - // A child may forward a release after its gesture was cancelled. Do not - // send that release back through UiGroup to the same captured child. - if(event==UI_RELEASE) return 0; - if (event == UI_KEYDOWN && !Ui::event_state(UI_CTRL | UI_ALT)) { - if (Ui::event_key() == 's') { split_selection(); return 1; } - if (Ui::event_key() == 'a') { audio_properties_dialog(); return 1; } - } - if(event==UI_KEYDOWN && !Ui::event_state(UI_CTRL|UI_ALT|UI_SHIFT) && (Ui::event_key()==UI_Left || Ui::event_key()==UI_Right)) {nudge_selection(Ui::event_key()==UI_Left ? -1 : 1);return 1;} - if(event==UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key()=='c'){copy_selection();return 1;} - if(event==UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key()=='v'){paste_selection();return 1;} - if (event == UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key() == 'a') {select_all_clips();return 1;} - if (event == UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key() == 'd') { duplicate_selection(); return 1; } - if (event == UI_KEYDOWN && Ui::event_key() == UI_F+2) { - const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); return 1; - } - if (event == UI_KEYDOWN && Ui::event_state(UI_ALT) && - (Ui::event_key() == UI_Up || Ui::event_key() == UI_Down)) { move_track(Ui::event_key() == UI_Up ? -1 : 1); return 1; } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { - mode_ = 0; - return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Delete) { - delete_selection(); - return 1; - } - if (event == UI_FOCUS || event == UI_UNFOCUS) - return 1; - return UiGroup::handle(event); -} - -void SequencerWidget::context_menu() -{ - UiMenu_Item items[6]{}; - items[0].text = "Duplicate clip (Ctrl+D)"; items[1].text = "Rename (F2)"; items[2].text = "Delete (undoable)"; - items[3].text = "Split clip at cursor (S)"; items[4].text = "Audio gain and fades (A)"; - const UiMenu_Item *picked = items->popup(Ui::event_x(),Ui::event_y()); - if (picked == items) duplicate_selection(); - else if (picked == items+1) { const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); } - else if (picked == items+2) delete_selection(); - else if (picked == items+3) split_selection(); - else if (picked == items+4) audio_properties_dialog(); -} - -SequencerTrackWidget *SequencerWidget::track_widget(uint64_t track) const -{ - for(auto *view:track_widgets_) if(view->track_id()==track) return view; - return nullptr; -} - -void SequencerWidget::sync_views() try -{ - size_t tracks=0,clips=0; xtc_project_get_counts(project_,&tracks,&clips); - ruler_->resize(x(),y(),w(),28); - for(auto it=track_widgets_.begin();it!=track_widgets_.end();) { - bool found=false; - for(size_t i=0;itrack_id()) { found=true; break; } - } - if(!found) { - auto *removed=*it; it=track_widgets_.erase(it); - if(active_velocity_==removed->velocity_lane()) { active_velocity_=nullptr; mode_=0; } - removed->hide(); remove(removed); Ui::delete_widget(removed); - } else ++it; - } - SequencerGroupScope scope(this); - for(size_t i=0;isync(i,t); view->show(); - } -} catch(const std::bad_alloc &) { - // Rendering allocation failure must not invalidate an accepted C model edit. - active_velocity_=nullptr; mode_=0; - for(auto *view:track_widgets_) view->hide(); -} - -void SequencerWidget::resize(int left,int top,int width,int height) -{ - UiGroup::resize(left,top,width,height); - if(ruler_)sync_views(); // Includes pinned headers after native scrolling/reparenting. -} diff --git a/src/gui/JUCE/sequencer_widget.hpp b/src/gui/JUCE/sequencer_widget.hpp deleted file mode 100644 index ad5cd4a..0000000 --- a/src/gui/JUCE/sequencer_widget.hpp +++ /dev/null @@ -1,260 +0,0 @@ -#ifndef XTC_SEQUENCER_WIDGET_HPP -#define XTC_SEQUENCER_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_plugin.h" -#include "xtc/xtc_editing.h" -#include "xtc/xtc_mixer.h" -#include -#include "xtc/xtc_audio.h" -#include -#include -#include - -class SequencerTrackWidget; -class SequencerTrackHeader; -class SequencerClipWidget; -class SequencerVelocityWidget; -class SequencerRulerWidget; - -class SequencerWidget : public UiGroup { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using LoopSelect = xtc_result (*)(void *,uint64_t,uint64_t); - LoopSelect loop_select = nullptr; - uint64_t new_clip_ticks = 3840; - int header_width() const {return header_width_;} - void header_width(int width); - int header_left() const; - int zoom_level() const { return scale_; } - xtc_result set_zoom(int scale); - void loop_region(uint64_t start,uint64_t end,bool enabled) { loop_begin_=start; loop_end_=end; loop_enabled_=enabled; redraw(); } - using OpenClip = void (*)(void *); - using InstrumentSelect = void (*)(void *, uint64_t, const xtc_plugin_spec *, bool); - OpenClip open_clip = nullptr; - InstrumentSelect instrument_select = nullptr; - UiChoice *instrument_choice(uint64_t track) const; - SequencerTrackWidget *track_widget(uint64_t track) const; - void refresh_instruments(); - bool track_expanded(uint64_t track) const; - bool expand_track(uint64_t track, bool expanded); - int track_top(size_t row) const; - bool select(uint64_t track, uint64_t clip = UINT64_MAX); - xtc_result merge_midi(); - xtc_result duplicate_track(const char *name); - void duplicate_track_dialog(); - xtc_result ripple_time(uint64_t length,bool remove); - void ripple_dialog(bool remove); - xtc_result marker(const char *name); /* NULL removes at cursor. */ - void marker_dialog(); - bool toggle_clip(uint64_t clip); - void select_all_clips(); - bool clip_selected(uint64_t clip) const; - std::vector selected_clips() const; - xtc_result transform_selection(xtc_clip_operation operation,int64_t amount); - xtc_result transform_midi(const xtc_midi_transform &edit); - void swing_dialog(); - void gate_dialog(); - void scale_dialog(); - void velocity_dialog(); - void humanize_dialog(); - void strum_dialog(); - xtc_result strum_chords(uint64_t spacing,bool descending); - xtc_result make_chord(const std::vector &intervals); - void chord_dialog(); - xtc_result arpeggiate(uint64_t step,unsigned int gate,bool descending); - void arpeggio_dialog(); - xtc_result normalize_audio(float target); - void normalize_dialog(); - void fades_dialog(); - xtc_result crossfade_selection(); - xtc_result repeat_selection(unsigned int copies); - void repeat_dialog(); - xtc_result copy_selection(); - xtc_result paste_selection(); - xtc_result nudge_selection(int direction); - xtc_result duplicate_selection(); - xtc_result split_selection(); - xtc_result audio_properties(float gain, uint64_t fade_in, uint64_t fade_out); - void audio_properties_dialog(); - void refresh_waveforms(); - size_t waveform_count() const { return waves_.size(); } - - xtc_result rename_selection(const char *name); - xtc_result move_track(int direction); - xtc_result grid(uint64_t ticks); - uint64_t grid() const { return grid_; } - using BeforeEdit = xtc_result (*)(void *); - BeforeEdit before_edit = nullptr; - - /** - * @brief Returns the selected track identifier, or UINT64_MAX when none is selected. - * @return Stable track identifier, or UINT64_MAX if no track is selected. - */ - uint64_t selected_track() const - { - return track_; - } - - /** - * @brief Returns the selected clip identifier, or UINT64_MAX when none is selected. - * @return Stable clip identifier, or UINT64_MAX if no clip is selected. - */ - uint64_t selected_clip() const - { - return clip_; - } - - /** - * @brief Requests a transport seek using an opaque context and musical tick. - * - * @return XTC_OK on success, or a transport error. - */ - using Seek = xtc_result (*)(void *, uint64_t); - /** - * @brief Constructs the timeline widget with a borrowed project and seek callback. - * - * @param x Left coordinate in pixels. - * @param y Top coordinate in pixels. - * @param project Borrowed project; must outlive the widget's use of it. - * @param seek Callback used to request a transport seek. - * @param context Opaque callback context. - */ - SequencerWidget(int x, int y, xtc_project *project, Seek seek, void *context); - /** - * @brief Replaces the borrowed project and refreshes the widget's displayed state. - * - * @param project Borrowed project; must outlive the widget's use of it. - * @param reset Whether to clear selection, cursor, and drag state. - * @param preserve_expansion Keep disclosure state when installing history snapshots. - */ - void set_project(xtc_project *project, bool reset = true, bool preserve_expansion = false); - - /** - * @brief Returns the timeline cursor position in musical ticks. - * @return Cursor position in musical ticks. - */ - uint64_t position() const - { - return cursor_; - } - - /** - * @brief Moves the timeline cursor to the supplied musical tick and requests a redraw. - * - * @param tick Musical position in ticks. - */ - void position(uint64_t tick) - { - cursor_ = tick; - redraw(); - } - - /** - * @brief Creates and selects a track through the arrangement edit hook. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_track(); - /** - * @brief Creates and selects a one-bar silent region at the timeline cursor. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_region(); - /** - * @brief Creates and selects a one-bar MIDI clip at the timeline cursor. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_midi_clip(); - /** - * @brief Removes the selected clip, or the selected track when no clip is selected. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result delete_selection(); - /** - * @brief Adjusts timeline zoom within its supported bounds and updates the scroll extent. - * - * @param direction Zoom direction; positive zooms in, nonpositive zooms out. - */ - void zoom(int direction); - - /** - * @brief Returns the borrowed project currently displayed by the widget. - * @return Borrowed project pointer. - */ - xtc_project *project() const - { - return project_; - } - - /** - * @brief Handles JUCE input events and delegates unhandled events to the base widget. - * - * @param event JUCE event code. - * @return Nonzero if the event was handled, zero otherwise. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the widget's current model, selection, and interaction feedback. - */ - void resize(int,int,int,int)override; - void draw() override; - void draw_overlay() override; - - private: - static constexpr int velocity_height_ = 88; - std::array expanded_tracks_{}; - int track_height(size_t row) const; - int track_row(int py) const; - friend class SequencerTrackWidget; - friend class SequencerTrackHeader; - friend class SequencerClipWidget; - friend class SequencerVelocityWidget; - friend class SequencerRulerWidget; - std::vector track_widgets_; // JUCE owns children. - SequencerRulerWidget *ruler_ = nullptr; - SequencerVelocityWidget *active_velocity_ = nullptr; // Captured gesture, not ownership. - void sync_views(); - void context_menu(); - std::vector catalog_; - static void instrument_callback(UiWidget *, void *); - struct Wave { - std::string path; - std::unique_ptr data{nullptr, xtc_waveform_destroy}; - }; - std::vector waves_; - std::vector selection_; - std::unique_ptr clipboard_{nullptr,xtc_clipboard_destroy}; - void draw_waveform(const xtc_clip_info &clip, int left, int top); - xtc_project *project_; - Seek seek_; - void *context_; - uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0, grid_ = 240; - uint64_t loop_begin_=0,loop_end_=3840,loop_anchor_=0,loop_preview_=0; - bool loop_enabled_=false; - int scale_ = 48, drag_x_ = 0, mode_ = 0; - int header_width_=180,header_drag_x_=0,header_drag_original_=180; - xtc_clip_info original_{}; - /** - * @brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid. - * - * @param px Screen x coordinate in pixels. - * @return Snapped tick within the supported project range. - */ - uint64_t tick(int px) const; - /** - * @brief Reports an edit result, refreshes the widget, and notifies listeners on success. - * - * @param result Result of the attempted operation. - */ - void changed(xtc_result result); - /** - * @brief Resizes the timeline's scrollable area to fit its tracks and clip ends. - */ - void extent(); -}; -#endif diff --git a/src/gui/JUCE/snap.hpp b/src/gui/JUCE/snap.hpp deleted file mode 100644 index 4743e01..0000000 --- a/src/gui/JUCE/snap.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef XTC_JUCE_GUI_SNAP_HPP -#define XTC_JUCE_GUI_SNAP_HPP -#include -#include -namespace xtc_ui { -constexpr uint64_t snap_ticks[] = {1,120,160,240,320,480,960,1920,3840}; -constexpr const char *snap_labels[] = {"Off","1/32","1/16 triplet","1/16","1/8 triplet","1/8","1/4","1/2","1 bar"}; -constexpr size_t snap_count = sizeof(snap_ticks)/sizeof(snap_ticks[0]); -inline int snap_index(uint64_t ticks) { - for (size_t i = 0; i < snap_count; ++i) if (snap_ticks[i] == ticks) return static_cast(i); - return -1; -} -inline int64_t snap_position(int64_t ticks, uint64_t grid) { - if (ticks <= 0) return 0; - int64_t step = static_cast(grid); - return (ticks+step/2)/step*step; -} -} -#endif diff --git a/src/gui/JUCE/theme.hpp b/src/gui/JUCE/theme.hpp deleted file mode 100644 index 0c61cd3..0000000 --- a/src/gui/JUCE/theme.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef XTC_JUCE_GUI_THEME_HPP -#define XTC_JUCE_GUI_THEME_HPP - -#include -#include "ui.hpp" - -/* Shared flat palette. No gradients, shadows, textures or external assets. */ -namespace xtc_ui { -constexpr UiBoxtype flat = static_cast(UI_FREE_BOXTYPE); -constexpr UiBoxtype pressed = static_cast(UI_FREE_BOXTYPE+1); -inline UiColor background() { return ui_rgb_color(25,28,33); } -inline UiColor panel() { return ui_rgb_color(34,38,44); } -inline UiColor border() { return ui_rgb_color(58,64,73); } -inline UiColor text() { return ui_rgb_color(231,235,240); } -inline UiColor muted() { return ui_rgb_color(163,174,188); } -inline UiColor accent() { return ui_rgb_color(92,211,184); } -inline UiColor track(uint64_t id) { - static const unsigned char colors[][3] = {{74,155,184},{111,143,206},{160,123,191}, - {74,163,139},{191,145,78},{183,113,139}}; - const auto &c = colors[id % 6]; return ui_rgb_color(c[0],c[1],c[2]); -} -inline void flat_box(int x, int y, int w, int h, UiColor color) { - ui_color(color); ui_rectf(x,y,w,h); - ui_color(border()); ui_rect(x,y,w,h); -} -inline void pressed_box(int x, int y, int w, int h, UiColor color) { - flat_box(x,y,w,h,ui_darker(color)); -} -inline void configure_boxes() { - Ui::set_boxtype(flat,flat_box,1,1,2,2); - Ui::set_boxtype(pressed,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_UP_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_DOWN_BOX,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_THIN_UP_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_THIN_DOWN_BOX,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_ENGRAVED_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_BORDER_BOX,flat_box,1,1,2,2); -} -inline void initialize() { - static bool initialized = false; - if (initialized) return; - initialized = true; - Ui::background(34,38,44); Ui::background2(25,28,33); Ui::foreground(231,235,240); - Ui::set_color(UI_SELECTION_COLOR,53,116,111); - configure_boxes(); - Ui::scrollbar_size(12); -} -inline void style(UiWidget *widget) { - widget->labelfont(UI_HELVETICA); widget->labelsize(12); widget->labelcolor(text()); - widget->color(panel()); widget->selection_color(ui_rgb_color(53,116,111)); - if (widget->box() != UI_NO_BOX && widget->box() != UI_FLAT_BOX) widget->box(flat); - if (auto *button = dynamic_cast(widget)) { - button->box(flat); button->down_box(pressed); - button->color(ui_rgb_color(45,51,59)); - } - if (auto *group = dynamic_cast(widget)) { - for (int i = 0; i < group->children(); ++i) style(group->child(i)); - if (auto *scroll = dynamic_cast(group)) { - scroll->scrollbar.box(flat); scroll->hscrollbar.box(flat); - scroll->scrollbar.slider(UI_FLAT_BOX); scroll->hscrollbar.slider(UI_FLAT_BOX); - } - } -} -} -#endif diff --git a/src/gui/JUCE/track_inserts.hpp b/src/gui/JUCE/track_inserts.hpp deleted file mode 100644 index 54075dd..0000000 --- a/src/gui/JUCE/track_inserts.hpp +++ /dev/null @@ -1,206 +0,0 @@ -#ifndef XTC_TRACK_INSERTS_HPP -#define XTC_TRACK_INSERTS_HPP -#include "main_window.hpp" -#include "graph_widget.hpp" -#include "plugin_library.hpp" -#include "plugin_dialog.hpp" -#include "sequencer_widget.hpp" -#include "insert_meter.hpp" -#include "ui.hpp" -#include -#include -#include -class TrackInsertsPanel : public UiGroup { - MainWindow *owner_; - UiChoice *tracks_; - UiMenu_Button *actions_; - InsertMeter *meter_; - UiHold_Browser *slots_; - UiBox *message_; - UiValue_Input *wet_,*gain_; - GraphWidget *graph_; - UiScroll *scroll_; - xtc_graph *model_=nullptr; - uint64_t track_=UINT64_MAX,meter_track_=UINT64_MAX; - xtc_track_chain cached_{}; - xtc_track_chain clipboard_{}; - bool clipboard_ready_=false; - struct Comparison {xtc_track_chain chain{};uint64_t track=UINT64_MAX;bool ready=false;} comparisons_[2]; - std::vector ids_; - std::vector names_; - void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); } - public: - TrackInsertsPanel(int x,int y,int w,int h,MainWindow *owner):UiGroup(x,y,w,h,"Track inserts"),owner_(owner) { - tracks_=new UiChoice(x+64,y+12,240,28,"Track");tracks_->callback([](UiWidget *,void *c){auto *p=static_cast(c);int i=p->tracks_->value();if(i>=0 && static_cast(i)ids_.size()){p->track_=p->ids_[static_cast(i)];p->refresh(true);}},this); - auto *add=new UiMenu_Button(x+320,y+12,150,28,"Add effect"); - add->add("Internal Delay",0,[](UiWidget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,2);},this); - add->add("Internal Reverb",0,[](UiWidget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,3);},this); - add->add("Plugin library...",0,[](UiWidget *,void *c){auto *p=static_cast(c);std::string file;if(!xtc_choose_plugin_file(p->owner_->settings(),file))return;unsigned int index=0;if(xtc_choose_plugin(file.c_str(),p->owner_->sample_rate(),index))p->add(file.c_str(),index);},this); - for(unsigned int preset=0;preset<3;++preset) { - std::string label=std::string("Templates (replace)/")+xtc_track_chain_template_name(preset); - add->add(label.c_str(),0,[](UiWidget *w,void *c){auto *p=static_cast(c);auto *menu=static_cast(w);unsigned int index=0;for(;index<3;++index)if(!strcmp(menu->mvalue()->label(),xtc_track_chain_template_name(index)))break;p->result(p->apply_template(index));},this); - } - struct Action {const char *name;int command;}; - int left=x+486; - for(const auto &action:{Action{"Up",-1},Action{"Down",1},Action{"Bypass",2},Action{"Remove",3}}) { - auto *button=new UiButton(left,y+12,82,28,action.name);left+=90; - button->callback([](UiWidget *w,void *c){auto *p=static_cast(c);int command=!strcmp(w->label(),"Up") ? -1 : !strcmp(w->label(),"Down") ? 1 : !strcmp(w->label(),"Bypass") ? 2 : 3;p->result(p->edit_slot(command));},this); - } - auto *inspect=new UiButton(x+850,y+12,120,28,"Inspect / edit"); - inspect->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->inspect_slot());},this); - auto *presets=new UiMenu_Button(x+978,y+12,68,28,"Preset"); - presets->add("Save chain...",0,[](UiWidget *,void *c){static_cast(c)->preset(true);},this); - presets->add("Load chain...",0,[](UiWidget *,void *c){static_cast(c)->preset(false);},this); - wet_=new UiValue_Input(x+110,y+52,100,26,"Insert wet");wet_->bounds(0,1);wet_->step(.01);wet_->when(UI_WHEN_RELEASE); - gain_=new UiValue_Input(x+360,y+52,100,26,"Chain gain");gain_->bounds(0,4);gain_->step(.01);gain_->when(UI_WHEN_RELEASE); - for(auto *input:{wet_,gain_})input->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this); - actions_=new UiMenu_Button(x+486,y+52,150,26,"Rack actions"); - actions_->add("Enable all effects",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_ENABLE_ALL));},this); - actions_->add("Bypass all effects",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_BYPASS_ALL));},this); - actions_->add("Clear chain",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_CLEAR));},this); - actions_->add("Compare/Capture A",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(0,true));},this); - actions_->add("Compare/Recall A",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(0,false));},this,UI_MENU_INACTIVE); - actions_->add("Compare/Capture B",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(1,true));},this); - actions_->add("Compare/Recall B",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(1,false));},this,UI_MENU_INACTIVE); - actions_->add("Copy chain",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->copy_chain());},this); - actions_->add("Paste chain (replace)",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->paste_chain());},this,UI_MENU_INACTIVE); - actions_->add("Duplicate effect",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->duplicate_slot());},this); - meter_=new InsertMeter(x+650,y+52,w-666,26); - message_=new UiBox(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - slots_=new UiHold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](UiWidget *,void *c){static_cast(c)->update_mix();},this); - scroll_=new UiScroll(x+260,y+126,w-276,h-142); - graph_=new GraphWidget(x+260,y+126,2000,4000,nullptr,[](void *,const xtc_graph_edit &,size_t *){return XTC_ERR_UNSUPPORTED;},nullptr,message_); - scroll_->end();resizable(scroll_);end(); - } - ~TrackInsertsPanel() override { graph_->set_graph(nullptr);xtc_graph_destroy(model_); } - uint64_t selected_track() const {return track_;} - size_t selected_slot() const {return slots_->value()>0 ? static_cast(slots_->value()-1) : SIZE_MAX;} - void select_track(uint64_t id) {track_=id;refresh(true);} - const xtc_graph *graph() const {return model_;} - void add(const char *file,unsigned int index) { result(owner_->add_track_effect(track_,file,index)); } - xtc_result bulk(xtc_chain_action action) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK && !chain.count)return XTC_ERR_NOT_FOUND; - if(r==XTC_OK)r=xtc_track_chain_bulk(&chain,action,&chain); - return r==XTC_OK ? owner_->set_track_chain(track_,chain) : r; - } - xtc_result apply_template(unsigned int index) { - xtc_track_chain chain{};auto r=xtc_track_chain_template(index,xtc_project_tempo(owner_->project_.get()),&chain); - if(r==XTC_OK)r=owner_->set_track_chain(track_,chain); - return r; - } - InsertMeter *meter() const {return meter_;} - void meters(const xtc_meter_snapshot &snapshot,bool active,double seconds=.02) {meter_->feed(snapshot,track_,active,seconds);} - void clear_session() {meter_->feed(xtc_meter_snapshot{},UINT64_MAX,false,.02);clipboard_={};clipboard_ready_=false;for(auto &slot:comparisons_)slot=Comparison{};update_actions();} - bool clipboard_ready() const {return clipboard_ready_;} - bool comparison_ready(unsigned int slot) const {return slot<2 && comparisons_[slot].ready && comparisons_[slot].track==track_;} - xtc_result comparison(unsigned int slot,bool capture) { - if(slot>=2)return XTC_ERR_INVALID_ARGUMENT; - if(capture) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK){comparisons_[slot].chain=chain;comparisons_[slot].track=track_;comparisons_[slot].ready=true;update_actions();} - return r; - } - if(!comparison_ready(slot))return XTC_ERR_NOT_FOUND; - return owner_->set_track_chain(track_,comparisons_[slot].chain); - } - void update_actions() { - for(unsigned int slot=0;slot<2;++slot){int item=actions_->find_index(slot ? "Compare/Recall B" : "Compare/Recall A");if(item>=0)actions_->mode(item,comparison_ready(slot) ? 0 : UI_MENU_INACTIVE);} - int index=actions_->find_index("Paste chain (replace)"); - if(index>=0)actions_->mode(index,clipboard_ready_ ? 0 : UI_MENU_INACTIVE); - } - xtc_result copy_chain() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK){clipboard_=chain;clipboard_ready_=true;update_actions();}return r; - } - xtc_result paste_chain() { - if(!clipboard_ready_)return XTC_ERR_NOT_FOUND; - return owner_->set_track_chain(track_,clipboard_); - } - xtc_result duplicate_slot() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r==XTC_OK)r=xtc_track_chain_duplicate(&chain,index,&chain); - if(r==XTC_OK)r=owner_->set_track_chain(track_,chain); - if(r==XTC_OK){slots_->select(static_cast(index+2));update_mix();} - return r; - } - void update_mix() { - xtc_track_chain chain{};if(xtc_project_get_chain(owner_->project_.get(),track_,&chain)!=XTC_OK){wet_->deactivate();gain_->deactivate();return;} - gain_->activate();gain_->value(chain.output_gain);size_t index=selected_slot(); - if(indexactivate();wet_->value(chain.slots[index].wet);}else wet_->deactivate(); - } - xtc_result set_mix(double wet,double gain) { - if(!std::isfinite(wet) || wet<0 || wet>1 || !std::isfinite(gain) || gain<0 || gain>4)return XTC_ERR_INVALID_ARGUMENT; - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);if(r!=XTC_OK)return r; - size_t index=selected_slot();if(index(wet); - chain.output_gain=static_cast(gain);return owner_->set_track_chain(track_,chain); - } - xtc_result save_preset(const char *file) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - return r==XTC_OK ? xtc_track_chain_preset_save(&chain,file) : r; - } - xtc_result load_preset(const char *file) { - xtc_track_chain chain{};auto r=xtc_track_chain_preset_load(file,&chain); - return r==XTC_OK ? owner_->set_track_chain(track_,chain) : r; - } - void preset(bool save) { - UiNative_File_Chooser chooser;chooser.title(save ? "Save effect chain" : "Load effect chain"); - chooser.type(save ? UiNative_File_Chooser::BROWSE_SAVE_FILE : UiNative_File_Chooser::BROWSE_FILE); - chooser.filter("Effect chain project\t*.xtc");if(save)chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM); - if(chooser.show()!=0 || !chooser.filename())return; - result(save ? save_preset(chooser.filename()) : load_preset(chooser.filename())); - } - xtc_result inspect_slot() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r!=XTC_OK)return r; - if(index>=chain.count)return XTC_ERR_INVALID_STATE; - xtc_plugin_details details{};xtc_plugin_spec edited{}; - r=xtc_plugin_describe(&chain.slots[index].plugin,owner_->sample_rate(),&details);if(r!=XTC_OK)return r; - if(!xtc_edit_plugin(chain.slots[index].plugin,details,edited))return XTC_OK; - chain.slots[index].plugin=edited;return owner_->set_track_chain(track_,chain); - } - /* Commands: -1 up, 1 down, 2 toggle bypass, 3 remove. */ - xtc_result edit_slot(int command) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r!=XTC_OK)return r; - if(index>=chain.count)return XTC_ERR_INVALID_STATE; - size_t selected=index; - if(command==-1 || command==1) { - if((command==-1 && !index) || (command==1 && index+1>=chain.count))return XTC_ERR_INVALID_ARGUMENT; - selected=command==-1 ? index-1 : index+1;std::swap(chain.slots[index],chain.slots[selected]); - }else if(command==2)chain.slots[index].enabled=!chain.slots[index].enabled; - else if(command==3) {for(size_t i=index+1;iset_track_chain(track_,chain); - if(r==XTC_OK && chain.count)slots_->select(static_cast(std::min(selected,chain.count-1)+1)); - update_mix();return r; - } - void refresh(bool force=false) { - const xtc_project *project=owner_->project_.get();size_t count=0,clips=0;xtc_project_get_counts(project,&count,&clips); - std::vector ids;std::vector names; - for(size_t i=0;iclear();for(const auto &name:names) {tracks_->add("track");tracks_->replace(tracks_->size()-2,name.c_str());}} - if(std::find(ids_.begin(),ids_.end(),track_)==ids_.end())track_=ids_.empty() ? UINT64_MAX : ids_[0]; - for(size_t i=0;ivalue(static_cast(i)); - update_actions(); - if(track_!=meter_track_){meter_track_=track_;meters(xtc_meter_snapshot{},false);} - xtc_track_chain chain{};chain.output_gain=1; - if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain); - if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return; - cached_=chain;int selected=slots_->value();slots_->clear(); - for(size_t i=0;iadd(label.c_str());} - if(chain.count)slots_->select(selected>0 && static_cast(selected)<=chain.count ? selected : 1); - xtc_graph *next=nullptr;auto r=xtc_track_chain_graph(&chain,&next); - if(r==XTC_OK){graph_->set_graph(next);xtc_graph_destroy(model_);model_=next;graph_->auto_layout(); - size_t nodes=0,edges=0,max_level=0,max_row=0;std::array rows{}; - xtc_graph_get_counts(model_,&nodes,&edges); - for(size_t i=0;irouting(node.id,route))max_level=std::max(max_level,route.level);} - graph_->size(std::max(scroll_->w()-20,static_cast(max_level+1)*174+40),4000); - for(size_t i=0;irouting(node.id,route) && route.levelmove_node(node.id,20+static_cast(route.level)*174,24+static_cast(row)*104);}} - graph_->size(graph_->w(),std::max(scroll_->h()-20,static_cast(max_row+1)*104+48));scroll_->scroll_to(0,0); - message_->copy_label("Track input > inserts > output gain > track fader > bus/master. Scroll to inspect long chains.");} - update_mix();redraw(); - } -}; -#endif diff --git a/src/gui/JUCE/ui.cpp b/src/gui/JUCE/ui.cpp deleted file mode 100644 index 27594b9..0000000 --- a/src/gui/JUCE/ui.cpp +++ /dev/null @@ -1,1234 +0,0 @@ -#include "ui.hpp" -#include "xtc/xtc_version.h" -#include -namespace -{ -thread_local juce::Graphics *graphics = nullptr; -thread_local std::unique_ptr font; -thread_local float stroke = 1; -std::vector windows; -struct Timeout : juce::Timer -{ - void (*function)(void *); - void *context; - bool cancelled = false, executing = false; - Timeout(double seconds, void (*f)(void *), void *c) : function(f), context(c) - { - startTimer(std::max(1, static_cast(seconds * 1000))); - } - void timerCallback() override; -}; -std::vector> timers; -void Timeout::timerCallback() -{ - stopTimer(); - if (cancelled) - return; - cancelled = true; - executing = true; - function(context); - executing = false; -} -juce::Colour colour(UiColor c) { return juce::Colour(0xff000000U | c); } -std::string formatted(const char *text, va_list args) -{ - char buffer[8192]; - std::vsnprintf(buffer, sizeof(buffer), text, args); - return buffer; -} -int key_code(const juce::KeyPress &key) -{ - const int k = key.getKeyCode(); - if (k == juce::KeyPress::escapeKey) - return UI_Escape; - if (k == juce::KeyPress::returnKey) - return UI_Enter; - if (k == juce::KeyPress::deleteKey) - return UI_Delete; - if (k == juce::KeyPress::backspaceKey) - return UI_BackSpace; - if (k == juce::KeyPress::leftKey) - return UI_Left; - if (k == juce::KeyPress::rightKey) - return UI_Right; - if (k == juce::KeyPress::upKey) - return UI_Up; - if (k == juce::KeyPress::downKey) - return UI_Down; - if (k == juce::KeyPress::homeKey) - return UI_Home; - if (k >= juce::KeyPress::F1Key && k <= juce::KeyPress::F16Key) - return UI_F + k - juce::KeyPress::F1Key + 1; - return juce::CharacterFunctions::toLowerCase(static_cast(k)); -} -int modifiers(juce::ModifierKeys m) -{ - return (m.isCtrlDown() ? UI_CTRL : 0) | (m.isShiftDown() ? UI_SHIFT : 0) | - (m.isAltDown() ? UI_ALT : 0) | (m.isCommandDown() && !m.isCtrlDown() ? UI_META : 0); -} -} // namespace -int Ui::ex = 0, Ui::ey = 0, Ui::ek = 0, Ui::es = 0, Ui::eb = 0, Ui::clicks = 0; -UiWidget *Ui::pressed = nullptr; -UiGroup *UiGroup::current_ = nullptr; -bool Ui::event_inside(const UiWidget *w) -{ - return ex >= w->x() && ey >= w->y() && ex < w->x() + w->w() && ey < w->y() + w->h(); -} -UiWidget *Ui::focus() -{ - auto *c = juce::Component::getCurrentlyFocusedComponent(); - while (c) - { - if (auto *w = dynamic_cast(c)) - return w; - c = c->getParentComponent(); - } - return nullptr; -} -void Ui::delete_widget(UiWidget *w) -{ - juce::Component::SafePointer safe(w); - juce::MessageManager::callAsync( - [safe] - { - if (safe) - delete safe.getComponent(); - }); -} -void Ui::add_timeout(double s, void (*f)(void *), void *c) -{ - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); - timers.push_back(std::make_unique(s, f, c)); -} -void Ui::remove_timeout(void (*f)(void *), void *c) -{ - for (auto &t : timers) - if (t->function == f && t->context == c) - { - t->stopTimer(); - t->cancelled = true; - } - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); -} -double Ui::wait(double s) -{ - juce::MessageManager::getInstance()->runDispatchLoopUntil( - std::max(1, static_cast(s * 1000))); - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); - return s; -} -int Ui::run() -{ - for (;;) - { - bool any = false; - for (auto *w : windows) - any |= w->shown(); - if (!any) - break; - wait(); - } - timers.clear(); - return 0; -} -void Ui::flush() -{ - for (auto *w : windows) - if (auto *p = w->getPeer()) - p->performAnyPendingRepaintsNow(); -} -void Ui::screen_work_area(int &x, int &y, int &w, int &h, int) -{ - auto *d = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay(); - auto r = d ? d->userBounds.toNearestInt() : juce::Rectangle(0, 0, 1440, 960); - x = r.getX(); - y = r.getY(); - w = r.getWidth(); - h = r.getHeight(); -} -UiColor ui_rgb_color(unsigned char r, unsigned char g, unsigned char b) -{ - return (static_cast(r) << 16) | (static_cast(g) << 8) | b; -} -UiColor ui_darker(UiColor c) { return colour(c).darker(.25f).getARGB() & 0xffffff; } -UiColor ui_color_average(UiColor a, UiColor b, float weight) -{ - return colour(b).interpolatedWith(colour(a), weight).getARGB() & 0xffffff; -} -void ui_color(UiColor c) -{ - if (graphics) - graphics->setColour(colour(c)); -} -void ui_font(int f, int size) -{ - font = std::make_unique( - juce::FontOptions(static_cast(size), f ? juce::Font::bold : juce::Font::plain)); - if (f == UI_COURIER_BOLD) - font->setTypefaceName(juce::Font::getDefaultMonospacedFontName()); - if (graphics) - graphics->setFont(*font); -} -void ui_rectf(int x, int y, int w, int h) -{ - if (graphics && w > 0 && h > 0) - graphics->fillRect(x, y, w, h); -} -void ui_rect(int x, int y, int w, int h) -{ - if (graphics && w > 0 && h > 0) - graphics->drawRect(x, y, w, h); -} -void ui_line(int x, int y, int a, int b) -{ - if (graphics) - graphics->drawLine(static_cast(x), static_cast(y), static_cast(a), - static_cast(b), stroke); -} -void ui_line_style(int, int width) { stroke = static_cast(std::max(1, width)); } -void ui_polygon(int a, int b, int c, int d, int e, int f) -{ - if (graphics) - { - juce::Path p; - p.startNewSubPath(static_cast(a), static_cast(b)); - p.lineTo(static_cast(c), static_cast(d)); - p.lineTo(static_cast(e), static_cast(f)); - p.closeSubPath(); - graphics->fillPath(p); - } -} -void ui_pie(int x, int y, int w, int h, double, double) -{ - if (graphics) - graphics->fillEllipse(static_cast(x), static_cast(y), static_cast(w), - static_cast(h)); -} -void ui_draw(const char *s, int x, int y) -{ - if (graphics && s) - graphics->drawSingleLineText(juce::String::fromUTF8(s), x, y); -} -void ui_draw(const char *s, int x, int y, int w, int h, int align) -{ - if (!graphics || !s) - return; - auto justification = align & UI_ALIGN_LEFT - ? juce::Justification::centredLeft - : (align & UI_ALIGN_RIGHT ? juce::Justification::centredRight - : juce::Justification::centred); - if (align & UI_ALIGN_TOP) - justification = juce::Justification::topLeft; - graphics->drawFittedText(juce::String::fromUTF8(s), x, y, w, h, justification, - align & UI_ALIGN_WRAP ? std::max(1, h / 12) : 1); -} -void ui_push_clip(int x, int y, int w, int h) -{ - if (graphics) - { - graphics->saveState(); - graphics->reduceClipRegion(x, y, w, h); - } -} -void ui_pop_clip() -{ - if (graphics) - graphics->restoreState(); -} -int ui_clip_box(int x, int y, int w, int h, int &a, int &b, int &c, int &d) -{ - auto r = juce::Rectangle(x, y, w, h); - if (graphics) - r = r.getIntersection(graphics->getClipBounds()); - a = r.getX(); - b = r.getY(); - c = r.getWidth(); - d = r.getHeight(); - return !r.isEmpty(); -} -namespace -{ -void message_dialog(const std::string &text) -{ - UiWindow window(720, 240, "xtc"); - window.set_modal(); - auto *box = new UiBox(16, 16, 688, 164, text.c_str()); - box->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - auto *close = new UiReturn_Button(600, 194, 100, 30, "OK"); - close->callback([](UiWidget *, void *w) { static_cast(w)->hide(); }, &window); - window.callback([](UiWidget *w, void *) { w->hide(); }); - window.end(); - window.show(); - while (window.shown()) - Ui::wait(); -} -} // namespace -void ui_alert(const char *s, ...) -{ - va_list a; - va_start(a, s); - auto t = formatted(s, a); - va_end(a); - message_dialog(t); -} -void ui_message(const char *s, ...) -{ - va_list a; - va_start(a, s); - auto t = formatted(s, a); - va_end(a); - message_dialog(t); -} -int ui_choice(const char *s, const char *a, const char *b, const char *c, ...) -{ - va_list args; - va_start(args, c); - auto message = formatted(s, args); - va_end(args); - juce::AlertWindow dialog("xtc", message, juce::MessageBoxIconType::QuestionIcon); - if (a) - dialog.addButton(a, 1); - if (b) - dialog.addButton(b, 2); - if (c) - dialog.addButton(c, 3); - int result = dialog.runModalLoop(); - return result ? result - 1 : 0; -} -const char *ui_input(const char *s, const char *initial, ...) -{ - va_list args; - va_start(args, initial); - auto message = formatted(s, args); - va_end(args); - juce::AlertWindow dialog("xtc", message, juce::MessageBoxIconType::QuestionIcon); - dialog.addTextEditor("value", initial ? initial : ""); - dialog.addButton("OK", 1, juce::KeyPress(juce::KeyPress::returnKey)); - dialog.addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey)); - static std::string answer; - if (dialog.runModalLoop() != 1) - return nullptr; - answer = dialog.getTextEditorContents("value").toStdString(); - return answer.c_str(); -} - -UiWidget::UiWidget(int x, int y, int w, int h, const char *s) - : px_(x), py_(y), pw_(w), ph_(h), label_(s ? s : "") -{ - setWantsKeyboardFocus(true); - setVisible(true); - setName(label_); - if (UiGroup::current()) - UiGroup::current()->add(this); - else - setBounds(x, y, w, h); -} -UiWidget::~UiWidget() -{ - if (Ui::pressed == this) - Ui::pressed = nullptr; - if (parent_) - parent_->remove(this); -} -void UiWidget::resize(int x, int y, int w, int h) -{ - if (x == px_ && y == py_ && w == pw_ && h == ph_ && - getX() == x - (parent_ ? parent_->x() : 0) && getY() == y - (parent_ ? parent_->y() : 0) && - getWidth() == w && getHeight() == h) - return; - px_ = x; - py_ = y; - pw_ = std::max(1, w); - ph_ = std::max(1, h); - setBounds(x - (parent_ ? parent_->x() : 0), y - (parent_ ? parent_->y() : 0), pw_, ph_); -} -UiWindow *UiWidget::window() const -{ - auto *c = const_cast(this)->getTopLevelComponent(); - return dynamic_cast(c); -} -void UiWidget::paint(juce::Graphics &g) -{ - auto *previous = graphics; - graphics = &g; - g.saveState(); - g.setOrigin(-x(), -y()); - if (!font) - ui_font(0, 12); - g.setFont(*font); - draw(); - g.restoreState(); - graphics = previous; -} -void UiWidget::draw_box() -{ - if (box() != UI_NO_BOX) - { - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - if (box() != UI_FLAT_BOX) - { - ui_color(0x3a4049); - ui_rect(x(), y(), w(), h()); - } - } -} -void UiWidget::draw_label() -{ - ui_color(labelcolor()); - ui_font(labelfont(), labelsize()); - ui_draw(label(), x() + 4, y(), w() - 8, h(), align()); -} -void UiWidget::event(const juce::MouseEvent &e, int type) -{ - Ui::ex = x() + e.x; - Ui::ey = y() + e.y; - Ui::es = modifiers(e.mods); - Ui::eb = e.mods.isRightButtonDown() ? UI_RIGHT_MOUSE : UI_LEFT_MOUSE; - Ui::clicks = e.getNumberOfClicks() - 1; - if (type == UI_PUSH) - { - Ui::pressed = this; - take_focus(); - } - handle(type); - if (type == UI_RELEASE) - Ui::pressed = nullptr; -} -void UiWidget::mouseDown(const juce::MouseEvent &e) { event(e, UI_PUSH); } -void UiWidget::mouseDrag(const juce::MouseEvent &e) { event(e, UI_DRAG); } -void UiWidget::mouseUp(const juce::MouseEvent &e) { event(e, UI_RELEASE); } -void UiWidget::mouseDoubleClick(const juce::MouseEvent &e) { event(e, UI_PUSH); } -bool UiWidget::keyPressed(const juce::KeyPress &key) -{ - Ui::ek = key_code(key); - Ui::es = modifiers(key.getModifiers()); - if (handle(UI_KEYDOWN)) - return true; - if (auto *w = window()) - return w->handle(UI_SHORTCUT) != 0; - return false; -} -UiGroup::UiGroup(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) { begin(); } -UiGroup::~UiGroup() -{ - if (current_ == this) - current_ = parent(); - clear(); -} -void UiGroup::add(UiWidget *w) -{ - if (w->parent_ == this) - return; - if (w->parent_) - w->parent_->remove(w); - children_.push_back(w); - w->parent_ = this; - addChildComponent(w); - w->setVisible(true); - w->resize(w->x(), w->y(), w->w(), w->h()); -} -void UiGroup::remove(UiWidget *w) -{ - children_.erase(std::remove(children_.begin(), children_.end(), w), children_.end()); - removeChildComponent(w); - if (w->parent_ == this) - w->parent_ = nullptr; - if (resizable_ == w) - resizable_ = nullptr; -} -void UiGroup::clear() -{ - while (!children_.empty()) - delete children_.back(); -} -void UiGroup::resize(int nx, int ny, int nw, int nh) -{ - if (nx == x() && ny == y() && nw == w() && nh == h() && - getX() == nx - (parent() ? parent()->x() : 0) && - getY() == ny - (parent() ? parent()->y() : 0) && getWidth() == nw && getHeight() == nh) - return; - int dx = nx - x(), dy = ny - y(), dw = nw - w(), dh = nh - h(); - int rx = resizable_ ? resizable_->x() : x(), ry = resizable_ ? resizable_->y() : y(), - rw = resizable_ ? resizable_->w() : w(), rh = resizable_ ? resizable_->h() : h(); - UiWidget::resize(nx, ny, nw, nh); - auto edge = [](int p, int r, int size, int delta) - { - if (p <= r) - return p; - if (p >= r + size) - return p + delta; - return p + - static_cast(std::round(static_cast(p - r) * delta / std::max(1, size))); - }; - for (auto *c : children_) - { - int a = c->x(), b = c->y(), z = a + c->w(), t = b + c->h(); - if (resizable_) - { - a = edge(a, rx, rw, dw); - z = edge(z, rx, rw, dw); - b = edge(b, ry, rh, dh); - t = edge(t, ry, rh, dh); - } - c->resize(a + dx, b + dy, z - a, t - b); - } - redraw(); -} -UiWindow::UiWindow(int w, int h, const char *s) : UiWindow(50, 50, w, h, s) {} -UiWindow::UiWindow(int x, int y, int w, int h, const char *s) : UiGroup(0, 0, w, h, s) -{ - box(UI_FLAT_BOX); - if (parent()) - parent()->remove(this); - setBounds(x, y, w, h); - setVisible(false); - windows.push_back(this); -} -UiWindow::~UiWindow() -{ - windows.erase(std::remove(windows.begin(), windows.end(), this), windows.end()); - if (isCurrentlyModal()) - exitModalState(0); - removeFromDesktop(); -} -void UiWindow::show() -{ - if (!isOnDesktop()) - addToDesktop( - juce::ComponentPeer::windowHasTitleBar | juce::ComponentPeer::windowHasCloseButton | - juce::ComponentPeer::windowIsResizable | juce::ComponentPeer::windowHasMinimiseButton | - juce::ComponentPeer::windowHasMaximiseButton); - setVisible(true); - toFront(true); - if (modal_ && !isCurrentlyModal()) - enterModalState(true, nullptr, false); -} -void UiWindow::hide() -{ - if (isCurrentlyModal()) - exitModalState(0); - setVisible(false); -} -UiButton::UiButton(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - addChildComponent(button_); - button_.onClick = [this] { do_callback(); }; - sync_control(); - resized(); -} -void UiButton::sync_control() -{ - button_.setTooltip(getTooltip()); - button_.setButtonText(label()); - button_.setColour(juce::TextButton::buttonColourId, colour(color())); - button_.setColour(juce::TextButton::textColourOffId, colour(labelcolor())); - button_.setColour(juce::TextButton::buttonOnColourId, colour(selection_color())); -} -UiInput_::UiInput_(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - align(UI_ALIGN_LEFT); - addAndMakeVisible(editor); - editor.setMultiLine(false); - editor.onTextChange = [this] - { - if (when() & UI_WHEN_CHANGED) - do_callback(); - }; - editor.onReturnKey = [this] { do_callback(); }; - editor.onFocusLost = [this] - { - if (when() & UI_WHEN_RELEASE) - do_callback(); - }; - sync_control(); - resized(); -} -void UiInput_::value(const char *s) { editor.setText(juce::String::fromUTF8(s ? s : ""), false); } -const char *UiInput_::value() const -{ - text_ = editor.getText().toStdString(); - return text_.c_str(); -} -void UiInput_::sync_control() -{ - editor.setTooltip(getTooltip()); - editor.setColour(juce::TextEditor::backgroundColourId, colour(color())); - editor.setColour(juce::TextEditor::textColourId, colour(labelcolor())); -} -UiValue_Input::UiValue_Input(int x, int y, int w, int h, const char *s) - : UiWidget(x, y, w, h, s), input(x, y, w, h) -{ - if (input.parent()) - input.parent()->remove(&input); - addAndMakeVisible(input); - input.callback( - [](UiWidget *, void *p) - { - auto &v = *static_cast(p); - char *end = nullptr; - double d = std::strtod(v.input.value(), &end); - if (end && *end == 0 && std::isfinite(d)) - { - v.value(std::clamp(d, v.low_, v.high_)); - v.do_callback(); - } - else - v.value(v.value_); - }, - this); - input.when(UI_WHEN_ENTER_KEY | UI_WHEN_RELEASE); - resized(); -} -void UiValue_Input::value(double v) -{ - value_ = v; - char b[64]; - std::snprintf(b, sizeof(b), "%.8g", v); - input.value(b); -} -void UiValue_Input::sync_control() -{ - input.tooltip(getTooltip().toRawUTF8()); - input.color(color()); - input.labelcolor(labelcolor()); -} -UiValue_Slider::UiValue_Slider(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - addAndMakeVisible(slider_); - slider_.setSliderStyle(juce::Slider::LinearHorizontal); - slider_.setTextBoxStyle(juce::Slider::TextBoxRight, false, 60, 20); - slider_.setRange(0, 1); - slider_.setDoubleClickReturnValue(true, 0); - slider_.onValueChange = [this] - { - if ((when() & UI_WHEN_CHANGED) || !slider_.isMouseButtonDown(true)) - do_callback(); - }; - slider_.onDragStart = [this] { Ui::pressed = this; }; - slider_.onDragEnd = [this] - { - if (when() & UI_WHEN_RELEASE) - do_callback(); - Ui::pressed = nullptr; - }; - resized(); -} -void UiValue_Slider::bounds(double l, double h) -{ - slider_.setRange(std::min(l, h), std::max(l, h), slider_.getInterval()); -} -void UiValue_Slider::step(double s) -{ - slider_.setRange(slider_.getMinimum(), slider_.getMaximum(), s); -} -void UiValue_Slider::type(int t) -{ - UiWidget::type(t); - slider_.setSliderStyle(t == UI_VERT_SLIDER ? juce::Slider::LinearVertical - : t == UI_LINE_DIAL ? juce::Slider::RotaryHorizontalVerticalDrag - : juce::Slider::LinearHorizontal); - slider_.setTextBoxStyle( - t == UI_VERT_SLIDER ? juce::Slider::NoTextBox : juce::Slider::TextBoxRight, false, 60, 20); -} -void UiValue_Slider::sync_control() -{ - slider_.setTooltip(getTooltip()); - slider_.setColour(juce::Slider::backgroundColourId, colour(color())); - slider_.setColour(juce::Slider::thumbColourId, colour(selection_color())); -} -void UiProgress::draw() -{ - draw_box(); - ui_color(selection_color()); - ui_rectf(x(), y(), - static_cast( - w() * std::clamp((value_ - low_) / std::max(1e-12, high_ - low_), 0.0, 1.0)), - h()); - draw_label(); -} -const UiMenu_Item *UiMenu_Item::popup(int, int) const -{ - juce::PopupMenu menu; - int i = 0; - while (this[i].text) - { - menu.addItem(i + 1, juce::String::fromUTF8(this[i].text), this[i].active(), - (this[i].flags & UI_MENU_VALUE) != 0); - ++i; - } - int picked = menu.show(); - return picked > 0 ? this + picked - 1 : nullptr; -} -void UiMenu_::repair() -{ - for (size_t i = 0; i < labels_.size(); ++i) - items_[i].text = labels_[i].c_str(); - items_.back() = {}; -} -int UiMenu_::add(const char *s, int shortcut, UiCallback callback, void *context, int flags) -{ - std::string all = s ? s : ""; - size_t start = 0; - int first = static_cast(items_.size()) - 1; - do - { - size_t end = all.find('|', start); - std::string path = all.substr(start, end == std::string::npos ? end : end - start); - paths_.push_back(path); - size_t slash = path.find_last_of('/'); - labels_.push_back(path.substr(slash == std::string::npos ? 0 : slash + 1)); - items_.insert(items_.end() - 1, {nullptr, shortcut, callback, context, flags}); - if (end == std::string::npos) - break; - start = end + 1; - } while (start <= all.size()); - repair(); - return first; -} -void UiMenu_::replace(int index, const char *s) -{ - if (index < 0 || index >= static_cast(items_.size())) - return; - labels_[static_cast(index)] = s ? s : ""; - repair(); - redraw(); -} -void UiMenu_::clear() -{ - literal_ = false; - items_.assign(1, {}); - paths_.clear(); - labels_.clear(); - selected_ = -1; -} -void UiMenu_::copy(const UiMenu_Item *items) -{ - clear(); - literal_ = true; - for (size_t i = 0; items[i].text; ++i) - { - paths_.emplace_back(items[i].text); - labels_.emplace_back(items[i].text); - items_.insert(items_.end() - 1, items[i]); - } - repair(); -} -const UiMenu_Item *UiMenu_::menu() const { return items_.data(); } -const UiMenu_Item *UiMenu_::mvalue() const -{ - return selected_ >= 0 && selected_ < static_cast(labels_.size()) - ? &items_[static_cast(selected_)] - : nullptr; -} -const UiMenu_Item *UiMenu_::find_item(const char *s) const -{ - for (size_t i = 0; i < paths_.size(); ++i) - if (paths_[i] == s) - return &items_[i]; - return nullptr; -} -const UiMenu_Item *UiMenu_::picked(const UiMenu_Item *item) -{ - if (!item || !item->active()) - return nullptr; - selected_ = static_cast(item - items_.data()); - if (item->flags & UI_MENU_TOGGLE) - items_[static_cast(selected_)].flags ^= UI_MENU_VALUE; - auto callback = item->callback_; - auto *context = item->user_data_; - if (callback) - callback(this, context); - else - do_callback(); - redraw(); - return mvalue(); -} -const UiMenu_Item *UiMenu_::test_shortcut() -{ - for (auto &item : items_) - if (item.shortcut_ && item.shortcut_ == (Ui::ek | Ui::es) && item.active()) - return picked(&item); - return nullptr; -} -int UiMenu_::item_pathname(char *out, int size, const UiMenu_Item *item) const -{ - auto index = item - items_.data(); - if (index < 0 || index >= static_cast(paths_.size())) - return -1; - std::snprintf(out, static_cast(size), "%s", paths_[static_cast(index)].c_str()); - return 0; -} -void UiMenu_::show_popup(bool bar) -{ - juce::PopupMenu root; - if (literal_) - { - for (size_t i = 0; i < labels_.size(); ++i) - root.addItem(static_cast(i) + 1, juce::String::fromUTF8(labels_[i].c_str()), - items_[i].active()); - int result = root.showMenu(juce::PopupMenu::Options().withTargetComponent(this)); - if (result > 0) - picked(&items_[static_cast(result - 1)]); - return; - } - std::function build = - [&](juce::PopupMenu &menu, const std::string &prefix) - { - std::vector groups; - for (size_t i = 0; i < paths_.size(); ++i) - { - if (paths_[i].compare(0, prefix.size(), prefix)) - continue; - auto tail = paths_[i].substr(prefix.size()); - auto slash = tail.find('/'); - if (slash == std::string::npos) - menu.addItem(static_cast(i) + 1, juce::String::fromUTF8(labels_[i].c_str()), - items_[i].active(), (items_[i].flags & UI_MENU_VALUE) != 0); - else - { - auto name = tail.substr(0, slash); - if (std::find(groups.begin(), groups.end(), name) != groups.end()) - continue; - groups.push_back(name); - juce::PopupMenu sub; - build(sub, prefix + name + "/"); - menu.addSubMenu(name, sub); - } - } - }; - if (bar) - { - std::vector groups; - for (auto &path : paths_) - { - auto name = path.substr(0, path.find('/')); - if (std::find(groups.begin(), groups.end(), name) == groups.end()) - groups.push_back(name); - } - int selected = - std::clamp((Ui::ex - x()) / 90, 0, std::max(0, static_cast(groups.size()) - 1)); - if (!groups.empty()) - build(root, groups[static_cast(selected)] + "/"); - } - else - build(root, ""); - int result = root.showMenu(juce::PopupMenu::Options().withTargetComponent(this)); - if (result > 0) - picked(&items_[static_cast(result - 1)]); -} -int UiMenu_::handle(int e) -{ - if (e == UI_PUSH) - { - show_popup(); - return 1; - } - return UiWidget::handle(e); -} -void UiMenu_::draw() -{ - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - ui_color(labelcolor()); - ui_font(0, labelsize()); - ui_draw(mvalue() ? mvalue()->label() : label(), x() + 6, y(), w() - 20, h(), UI_ALIGN_LEFT); - ui_draw("v", x() + w() - 16, y(), 14, h()); -} -void UiMenu_Bar::draw() -{ - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - std::vector groups; - for (auto &p : paths_) - { - auto name = p.substr(0, p.find('/')); - if (std::find(groups.begin(), groups.end(), name) == groups.end()) - groups.push_back(name); - } - ui_color(labelcolor()); - ui_font(0, labelsize()); - for (size_t i = 0; i < groups.size(); ++i) - ui_draw(groups[i].c_str(), x() + static_cast(i) * 90 + 6, y(), 86, h(), UI_ALIGN_LEFT); -} -int UiMenu_Bar::handle(int e) -{ - if (e == UI_PUSH) - { - show_popup(true); - return 1; - } - return UiMenu_::handle(e); -} -UiInput_Choice::UiInput_Choice(int x, int y, int w, int h, const char *s) : UiGroup(x, y, w, h, s) -{ - input_ = new UiInput(x, y, w - 30, h); - menu_ = new UiMenu_Button(x + w - 30, y, 30, h, "v"); - menu_->callback( - [](UiWidget *w, void *p) - { - auto &c = *static_cast(p); - auto *m = static_cast(w); - if (m->text()) - c.input_->value(m->text()); - c.do_callback(); - }, - this); - end(); -} -UiHold_Browser::UiHold_Browser(int x, int y, int w, int h, const char *s) - : UiWidget(x, y, w, h, s), list_("", this) -{ - addAndMakeVisible(list_); - list_.setColour(juce::ListBox::backgroundColourId, colour(color())); - resized(); -} -void UiHold_Browser::paintListBoxItem(int row, juce::Graphics &g, int w, int h, bool selected) -{ - if (row < 0 || row >= static_cast(rows_.size())) - return; - g.fillAll(colour(selected ? selection_color() : color())); - g.setColour(colour(labelcolor())); - g.setFont(juce::FontOptions(static_cast(labelsize()))); - g.drawText(juce::String::fromUTF8(rows_[static_cast(row)].c_str()), 4, 0, w - 8, h, - juce::Justification::centredLeft); -} -UiScroll::UiScroll(int x, int y, int w, int h, const char *s) - : UiGroup(x, y, w, h, s), scrollbar(0, 0, 1, 1), hscrollbar(0, 0, 1, 1) -{ - remove(scrollbar); - remove(hscrollbar); - addAndMakeVisible(vertical_); - addAndMakeVisible(horizontal_); - vertical_.addListener(&listener_); - horizontal_.addListener(&listener_); -} -void UiScroll::Listener::scrollBarMoved(juce::ScrollBar *bar, double start) -{ - if (owner.scrolling_) - return; - owner.scroll_to(bar == &owner.horizontal_ ? static_cast(start) : owner.sx_, - bar == &owner.vertical_ ? static_cast(start) : owner.sy_); -} -void UiScroll::update_bars() -{ - int right = w() - 14, bottom = h() - 14; - for (int i = 0; i < children(); ++i) - { - right = std::max(right, child(i)->x() - x() + child(i)->w() + sx_); - bottom = std::max(bottom, child(i)->y() - y() + child(i)->h() + sy_); - } - scrolling_ = true; - vertical_.setBounds(w() - 12, 0, 12, h() - 12); - horizontal_.setBounds(0, h() - 12, w() - 12, 12); - vertical_.setRangeLimits(0, std::max(h() - 12, bottom)); - vertical_.setCurrentRange(sy_, std::max(1, h() - 12), juce::dontSendNotification); - horizontal_.setRangeLimits(0, std::max(w() - 12, right)); - horizontal_.setCurrentRange(sx_, std::max(1, w() - 12), juce::dontSendNotification); - if (getNumChildComponents() < 2 || - getChildComponent(getNumChildComponents() - 1) != &horizontal_ || - getChildComponent(getNumChildComponents() - 2) != &vertical_) - { - vertical_.toFront(false); - horizontal_.toFront(false); - } - scrolling_ = false; -} -void UiScroll::scroll_to(int x, int y) -{ - x = std::max(0, x); - y = std::max(0, y); - int dx = sx_ - x, dy = sy_ - y; - sx_ = x; - sy_ = y; - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - c->position(c->x() + dx, c->y() + dy); - } - update_bars(); - redraw(); -} -void UiScroll::mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &wheel) -{ - scroll_to(std::max(0, sx_ - static_cast(wheel.deltaX * 200)), - std::max(0, sy_ - static_cast(wheel.deltaY * 200))); -} -void UiTabs::update() -{ - auto *s = value(); - for (int i = 0; i < children(); ++i) - child(i)->setVisible(child(i) == s); - redraw(); -} -void UiTabs::draw() -{ - UiGroup::draw(); - auto *s = value(); - int left = x(); - const int tab_width = std::max(1, std::min(140, w() / std::max(1, children()))); - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - c->setVisible(c == s); - ui_color(c == s ? selection_color() : color()); - ui_rectf(left, y(), tab_width, 30); - ui_color(labelcolor()); - ui_font(0, 12); - ui_draw(c->label(), left + 6, y(), std::max(1, tab_width - 12), 30, UI_ALIGN_LEFT); - left += tab_width; - } -} -int UiTabs::handle(int e) -{ - if (e == UI_PUSH && Ui::ey < y() + 30) - { - const int tab_width = std::max(1, std::min(140, w() / std::max(1, children()))); - int n = (Ui::ex - x()) / tab_width; - if (n >= 0 && n < children()) - { - selected_ = child(n); - update(); - do_callback(); - return 1; - } - } - return UiGroup::handle(e); -} -int UiNative_File_Chooser::show() -{ - // Accept the retained labelled filters, including brace alternatives and - // callers which supply escaped tabs/newlines. JUCE expects semicolon globs. - auto filters = juce::String(filter_).replace("\\t", "\t").replace("\\n", "\n"); - juce::StringArray patterns; - for (auto line : juce::StringArray::fromLines(filters)) - { - auto pattern = line.containsChar('\t') ? line.fromFirstOccurrenceOf("\t", false, false) - : line; - if (pattern.containsChar('{') && pattern.containsChar('}')) - { - auto prefix = pattern.upToFirstOccurrenceOf("{", false, false); - auto alternatives = pattern.fromFirstOccurrenceOf("{", false, false) - .upToFirstOccurrenceOf("}", false, false); - auto suffix = pattern.fromFirstOccurrenceOf("}", false, false); - for (auto extension : juce::StringArray::fromTokens(alternatives, ",", "")) - patterns.add(prefix + extension + suffix); - } - else if (pattern.isNotEmpty()) - patterns.add(pattern); - } - auto wildcard = patterns.isEmpty() ? juce::String("*") : patterns.joinIntoString(";"); - juce::File start = directory_.empty() - ? juce::File::getSpecialLocation(juce::File::userHomeDirectory) - : juce::File(directory_); - if (!filename_.empty()) - start = start.getChildFile(filename_); - juce::FileChooser chooser(title_, start, wildcard, true); - bool ok = type_ == BROWSE_SAVE_FILE ? chooser.browseForFileToSave(true) - : chooser.browseForFileToOpen(); - if (!ok) - return 1; - filename_ = chooser.getResult().getFullPathName().toStdString(); - return 0; -} - -void UiWidget::mouseWheelMove(const juce::MouseEvent &e, const juce::MouseWheelDetails &wheel) -{ - if (auto *p = getParentComponent()) - p->mouseWheelMove(e.getEventRelativeTo(p), wheel); -} -void UiGroup::draw() -{ - draw_box(); - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - if (!c->visible() || (c->align() & UI_ALIGN_INSIDE) || !*c->label()) - continue; - ui_color(c->labelcolor()); - ui_font(c->labelfont(), c->labelsize()); - ui_draw(c->label(), x() + 4, c->y(), std::max(1, c->x() - x() - 12), c->h(), - UI_ALIGN_RIGHT); - } -} -void UiWindow::resize(int nx, int ny, int nw, int nh) -{ - UiGroup::resize(0, 0, std::max(min_w_, nw), std::max(min_h_, nh)); - setTopLeftPosition(nx, ny); -} -void UiWindow::resized() -{ - if (getWidth() != w() || getHeight() != h()) - resize(getX(), getY(), getWidth(), getHeight()); -} - -int &Ui::e_x = Ui::ex, &Ui::e_y = Ui::ey, &Ui::e_keysym = Ui::ek, &Ui::e_state = Ui::es, - &Ui::e_clicks = Ui::clicks; -UiWindow *Ui::first_window() -{ - for (auto i = windows.rbegin(); i != windows.rend(); ++i) - if ((*i)->shown()) - return *i; - return nullptr; -} -UiWindow *Ui::modal() -{ - return dynamic_cast(juce::Component::getCurrentlyModalComponent()); -} -void Ui::focus(UiWidget *w) -{ - if (w) - w->take_focus(); - else - juce::Component::unfocusAllComponents(); -} -int Ui::handle(int type, UiWindow *window) -{ - if (type == UI_KEYDOWN || type == UI_SHORTCUT) - return window->handle(type); - UiWidget *target = pressed; - if (type == UI_PUSH) - { - auto *c = window->getComponentAt(ex, ey); - while (c && !dynamic_cast(c)) - c = c->getParentComponent(); - target = dynamic_cast(c); - pressed = target; - } - if (!target) - return 0; - int result = target->handle(type); - if (type == UI_RELEASE) - pressed = nullptr; - return result; -} -void UiWidget::paintOverChildren(juce::Graphics &g) -{ - auto *previous = graphics; - graphics = &g; - g.saveState(); - g.setOrigin(-x(), -y()); - draw_overlay(); - g.restoreState(); - graphics = previous; -} - -void UiGroup::insert(UiWidget &w, UiWidget *before) -{ - add(w); - auto pos = std::find(children_.begin(), children_.end(), before); - if (pos != children_.end()) - { - children_.erase(std::remove(children_.begin(), children_.end(), &w), children_.end()); - pos = std::find(children_.begin(), children_.end(), before); - children_.insert(pos, &w); - w.toBehind(before); - } -} - -int UiGroup::handle(int event) -{ - if (event == UI_PUSH) - { - for (auto i = children_.rbegin(); i != children_.rend(); ++i) - { - auto *c = *i; - if (c->visible() && c->active() && Ui::event_inside(c) && c->handle(event)) - { - Ui::pressed = c; - return 1; - } - } - } - return event == UI_FOCUS || event == UI_UNFOCUS; -} - -namespace -{ -class WorkstationApplication : public juce::JUCEApplication -{ - public: - const juce::String getApplicationName() override { return "xtc JUCE"; } - const juce::String getApplicationVersion() override { return XTC_VERSION_STRING; } - void initialise(const juce::String &) override {} - void shutdown() override {} - void systemRequestedQuit() override - { - if (auto *w = Ui::first_window()) - w->do_callback(); - } -}; -juce::JUCEApplicationBase *create_workstation_application() { return new WorkstationApplication; } -} // namespace -struct UiInitialiser::State -{ - std::unique_ptr gui; - std::unique_ptr app; - std::unique_ptr tips; - State() - { - juce::JUCEApplicationBase::createInstance = create_workstation_application; - gui = std::make_unique(); - app.reset(create_workstation_application()); - tips = std::make_unique(nullptr, 700); - } - ~State() - { - tips.reset(); - app.reset(); - Ui::shutdown(); - gui.reset(); - juce::JUCEApplicationBase::createInstance = nullptr; - } -}; -UiInitialiser::UiInitialiser() : state_(std::make_unique()) {} -UiInitialiser::~UiInitialiser() = default; -void Ui::shutdown() -{ - timers.clear(); - if (juce::MessageManager::getInstanceWithoutCreating()) - juce::MessageManager::getInstance()->runDispatchLoopUntil(20); - font.reset(); -} - -int UiValue_Slider::handle(int event) -{ - if (event == UI_PUSH) - { - dragging_ = true; - drag_value_ = value(); - drag_x_ = Ui::event_x(); - drag_y_ = Ui::event_y(); - if (Ui::event_clicks()) - { - value(0); - drag_value_ = 0; - } - return 1; - } - if (event == UI_DRAG && dragging_) - { - double pixels = type() == UI_VERT_SLIDER ? drag_y_ - Ui::event_y() - : type() == UI_LINE_DIAL ? Ui::event_x() - drag_x_ + drag_y_ - Ui::event_y() - : Ui::event_x() - drag_x_; - double extent = type() == UI_VERT_SLIDER ? h() : type() == UI_LINE_DIAL ? 200 : w(); - value(drag_value_ + - pixels * (slider_.getMaximum() - slider_.getMinimum()) / std::max(1.0, extent)); - if (when() & UI_WHEN_CHANGED) - do_callback(); - return 1; - } - if (event == UI_RELEASE && dragging_) - { - dragging_ = false; - do_callback(); - return 1; - } - return UiWidget::handle(event); -} diff --git a/src/gui/JUCE/ui.hpp b/src/gui/JUCE/ui.hpp deleted file mode 100644 index 4f9393e..0000000 --- a/src/gui/JUCE/ui.hpp +++ /dev/null @@ -1,647 +0,0 @@ -#ifndef XTC_JUCE_UI_HPP -#define XTC_JUCE_UI_HPP -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// JUCE component adapter for the workstation's retained coordinates and commands. -// This layer has no FLTK headers, objects or runtime dependency. -using uchar = unsigned char; -using UiColor = unsigned int; -using UiBoxtype = int; -constexpr int UI_NO_BOX = 0, UI_FLAT_BOX = 1, UI_UP_BOX = 2, UI_DOWN_BOX = 3, UI_THIN_UP_BOX = 4, - UI_THIN_DOWN_BOX = 5, UI_ENGRAVED_BOX = 6, UI_BORDER_BOX = 7, UI_FREE_BOXTYPE = 16; -constexpr int UI_ALIGN_LEFT = 1, UI_ALIGN_RIGHT = 2, UI_ALIGN_TOP = 4, UI_ALIGN_INSIDE = 8, - UI_ALIGN_CLIP = 16, UI_ALIGN_WRAP = 32, UI_ALIGN_TOP_LEFT = 5; -constexpr int UI_HELVETICA = 0, UI_HELVETICA_BOLD = 1, UI_COURIER_BOLD = 2, UI_BOLD = 1, - UI_NO_LABEL = 0, UI_SOLID = 0, UI_LINE_DIAL = 1, UI_HOR_FILL_SLIDER = 2, - UI_VERT_SLIDER = 3; -constexpr UiColor UI_WHITE = 0xffffff, UI_RED = 0xff0000, UI_SELECTION_COLOR = 0x35746f; -constexpr int UI_CTRL = 1 << 20, UI_SHIFT = 1 << 21, UI_ALT = 1 << 22, UI_META = 1 << 23; -constexpr int UI_PUSH = 1, UI_DRAG = 2, UI_RELEASE = 3, UI_FOCUS = 4, UI_UNFOCUS = 5, - UI_KEYDOWN = 6, UI_SHORTCUT = 7; -constexpr int UI_LEFT_MOUSE = 1, UI_RIGHT_MOUSE = 3, UI_Button = 0x10000, UI_BUTTON1 = 1 << 24; -constexpr int UI_Escape = 27, UI_Enter = 13, UI_KP_Enter = 13, UI_BackSpace = 8, UI_Delete = 127, - UI_Left = 0x100, UI_Right = 0x101, UI_Up = 0x102, UI_Down = 0x103, UI_Home = 0x104, - UI_F = 0x200; -constexpr int UI_WHEN_CHANGED = 1, UI_WHEN_ENTER_KEY = 2, UI_WHEN_RELEASE = 4, UI_MENU_TOGGLE = 1, - UI_MENU_VALUE = 2, UI_MENU_INACTIVE = 4, UI_CURSOR_DEFAULT = 0, UI_CURSOR_WAIT = 1; -class UiWidget; -class UiGroup; -class UiWindow; -using UiCallback = void (*)(UiWidget *, void *); -class UiInitialiser -{ - struct State; - std::unique_ptr state_; - - public: - UiInitialiser(); - ~UiInitialiser(); -}; -struct Ui -{ - static int ex, ey, ek, es, eb, clicks; - static int &e_x, &e_y, &e_keysym, &e_state, &e_clicks; - static UiWindow *first_window(); - static UiWindow *modal(); - static void focus(UiWidget *); - static int handle(int, UiWindow *); - static UiWidget *pressed; - static int event_x() { return ex; } - static int event_y() { return ey; } - static int event_key() { return ek; } - static int event_state() { return es; } - static int event_state(int mask) { return es & mask; } - static int event_button() { return ek >= 0x10000 && ek < 0x10010 ? ek - 0x10000 : eb; } - static int event_clicks() { return clicks; } - static bool event_inside(const UiWidget *); - static UiWidget *focus(); - static UiWidget *pushed() { return pressed; } - static void delete_widget(UiWidget *); - static void add_timeout(double, void (*)(void *), void * = nullptr); - static void repeat_timeout(double s, void (*f)(void *), void *c) { add_timeout(s, f, c); } - static void remove_timeout(void (*)(void *), void *); - static void shutdown(); - static int run(); - static double wait(double seconds = .01); - static void check() { wait(.001); } - static void flush(); - static int screen_num(int, int, int, int) { return 0; } - static void screen_work_area(int &, int &, int &, int &, int); - static void background(int, int, int) {} - static void background2(int, int, int) {} - static void foreground(int, int, int) {} - static void set_color(int, int, int, int) {} - static void set_boxtype(int, void (*)(int, int, int, int, UiColor), int, int, int, int) {} - static void scrollbar_size(int) {} -}; -UiColor ui_rgb_color(unsigned char, unsigned char, unsigned char); -UiColor ui_darker(UiColor); -UiColor ui_color_average(UiColor, UiColor, float); -void ui_color(UiColor); -void ui_font(int, int); -void ui_rectf(int, int, int, int); -void ui_rect(int, int, int, int); -void ui_line(int, int, int, int); -void ui_polygon(int, int, int, int, int, int); -void ui_pie(int, int, int, int, double, double); -void ui_draw(const char *, int, int); -void ui_draw(const char *, int, int, int, int, int = 0); -void ui_push_clip(int, int, int, int); -void ui_pop_clip(); -int ui_clip_box(int, int, int, int, int &, int &, int &, int &); -void ui_line_style(int, int = 1); -void ui_alert(const char *, ...); -void ui_message(const char *, ...); -int ui_choice(const char *, const char *, const char *, const char *, ...); -const char *ui_input(const char *, const char * = "", ...); - -class UiWidget : public juce::Component, public juce::SettableTooltipClient -{ - friend class UiGroup; - int px_, py_, pw_, ph_; - std::string label_, tooltip_; - UiCallback callback_ = nullptr; - void *context_ = nullptr; - UiGroup *parent_ = nullptr; - UiColor colour_ = 0x22262c, label_colour_ = 0xe7ebf0, selection_ = 0x35746f; - int box_ = UI_NO_BOX, alignment_ = UI_ALIGN_INSIDE, label_size_ = 12, font_ = 0, - when_ = UI_WHEN_RELEASE, type_ = 0; - - protected: - friend struct Ui; - void paint(juce::Graphics &) override; - void paintOverChildren(juce::Graphics &) override; - void mouseDown(const juce::MouseEvent &) override; - void mouseDrag(const juce::MouseEvent &) override; - void mouseUp(const juce::MouseEvent &) override; - void mouseDoubleClick(const juce::MouseEvent &) override; - void mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &) override; - bool keyPressed(const juce::KeyPress &) override; - void focusGained(FocusChangeType) override { handle(UI_FOCUS); } - void focusLost(FocusChangeType) override { handle(UI_UNFOCUS); } - void event(const juce::MouseEvent &, int); - virtual void sync_control() {} - - public: - UiWidget(int, int, int, int, const char * = nullptr); - ~UiWidget() override; - virtual void draw() - { - draw_box(); - draw_label(); - } - virtual void draw_overlay() {} - virtual int handle(int) { return 0; } - int x() const { return px_; } - int y() const { return py_; } - int w() const { return pw_; } - int h() const { return ph_; } - virtual void resize(int, int, int, int); - void position(int x, int y) { resize(x, y, w(), h()); } - void size(int w, int h) { resize(x(), y(), w, h); } - UiGroup *parent() const { return parent_; } - UiWindow *window() const; - const char *label() const { return label_.c_str(); } - void label(const char *s) { copy_label(s); } - void copy_label(const char *s) - { - label_ = s ? s : ""; - setName(label_); - sync_control(); - redraw(); - } - void tooltip(const char *s) - { - tooltip_ = s ? s : ""; - setTooltip(tooltip_); - sync_control(); - } - void copy_tooltip(const char *s) { tooltip(s); } - void callback(UiCallback f, void *c = nullptr) - { - callback_ = f; - context_ = c; - } - void do_callback() - { - if (callback_) - callback_(this, context_); - } - void *user_data() const { return context_; } - void user_data(void *c) { context_ = c; } - virtual void show() { setVisible(true); } - virtual void hide() { setVisible(false); } - bool visible_r() const { return isShowing(); } - bool visible() const { return isVisible(); } - bool shown() const { return isVisible(); } - void activate() { setEnabled(true); } - void deactivate() { setEnabled(false); } - int active() const { return isEnabled(); } - int active_r() const { return isEnabled(); } - void redraw() { repaint(); } - virtual int take_focus() - { - if (!isShowing() || !isEnabled()) - return 0; - grabKeyboardFocus(); - return 1; - } - void color(UiColor c) - { - colour_ = c; - sync_control(); - redraw(); - } - UiColor color() const { return colour_; } - void labelcolor(UiColor c) - { - label_colour_ = c; - sync_control(); - redraw(); - } - UiColor labelcolor() const { return label_colour_; } - void selection_color(UiColor c) - { - selection_ = c; - sync_control(); - } - UiColor selection_color() const { return selection_; } - void box(int b) { box_ = b; } - int box() const { return box_; } - void slider(int) {} - void down_box(int) {} - void labeltype(int) {} - void align(int a) { alignment_ = a; } - int align() const { return alignment_; } - void labelsize(int s) - { - label_size_ = s; - sync_control(); - } - int labelsize() const { return label_size_; } - void labelfont(int f) { font_ = f; } - int labelfont() const { return font_; } - void textsize(int s) { labelsize(s); } - void textcolor(UiColor c) { labelcolor(c); } - void when(int w) { when_ = w; } - int when() const { return when_; } - virtual void type(int t) { type_ = t; } - int type() const { return type_; } - void draw_box(); - void draw_label(); -}; -class UiGroup : public UiWidget -{ - std::vector children_; - UiWidget *resizable_ = nullptr; - static UiGroup *current_; - - public: - UiGroup(int, int, int, int, const char * = nullptr); - ~UiGroup() override; - static UiGroup *current() { return current_; } - static void current(UiGroup *g) { current_ = g; } - void begin() { current_ = this; } - void end() { current_ = parent(); } - void add(UiWidget *); - void add(UiWidget &w) { add(&w); } - void insert(UiWidget &, UiWidget *); - void remove(UiWidget *); - void remove(UiWidget &w) { remove(&w); } - void clear(); - int children() const { return static_cast(children_.size()); } - UiWidget *child(int i) const { return children_.at(static_cast(i)); } - void resizable(UiWidget *w) { resizable_ = w; } - void resizable(UiWidget &w) { resizable_ = &w; } - UiWidget *resizable() const { return resizable_; } - int handle(int) override; - void init_sizes() {} - void draw_children() {} - void resize(int, int, int, int) override; - void draw() override; -}; -class UiWindow : public UiGroup -{ - bool modal_ = false; - int min_w_ = 100, min_h_ = 100; - - protected: - void userTriedToCloseWindow() override { do_callback(); } - - public: - UiWindow(int, int, const char * = nullptr); - UiWindow(int, int, int, int, const char * = nullptr); - ~UiWindow() override; - void show() override; - void show(int, char **) { show(); } - void hide() override; - void set_modal() { modal_ = true; } - void set_non_modal() { modal_ = false; } - void size_range(int w, int h, int = 0, int = 0) - { - min_w_ = w; - min_h_ = h; - } - int x() const { return getX(); } - int y() const { return getY(); } - void resize(int, int, int, int) override; - void resized() override; - void cursor(int c) - { - setMouseCursor(c == UI_CURSOR_WAIT ? juce::MouseCursor::WaitCursor - : juce::MouseCursor::NormalCursor); - } -}; -using UiDouble_Window = UiWindow; -class UiBox : public UiWidget -{ - public: - using UiWidget::UiWidget; -}; -class UiButton : public UiWidget -{ - juce::TextButton button_; - bool toggle_ = false; - - protected: - void sync_control() override; - - public: - UiButton(int, int, int, int, const char * = nullptr); - int value() const { return button_.getToggleState() ? 1 : 0; } - void value(int v) { button_.setToggleState(v != 0, juce::dontSendNotification); } - void set_toggle() - { - toggle_ = true; - button_.setClickingTogglesState(true); - } - void resized() override { button_.setBounds(getLocalBounds()); } - void draw() override { button_.setVisible(true); } - int handle(int e) override - { - if (e == UI_PUSH) - return 1; - if (e == UI_RELEASE) - { - if (toggle_) - value(!value()); - do_callback(); - return 1; - } - return UiWidget::handle(e); - } -}; -class UiCheck_Button : public UiButton -{ - public: - UiCheck_Button(int x, int y, int w, int h, const char *s = nullptr) : UiButton(x, y, w, h, s) - { - set_toggle(); - } -}; -using UiReturn_Button = UiButton; -class UiInput_ : public UiWidget -{ - mutable std::string text_; - - protected: - void sync_control() override; - - public: - juce::TextEditor editor; - UiInput_(int, int, int, int, const char * = nullptr); - void value(const char *); - const char *value() const; - void maximum_size(int n) { editor.setInputRestrictions(n); } - int take_focus() override - { - if (!isShowing() || !isEnabled()) - return 0; - editor.grabKeyboardFocus(); - return 1; - } - void resized() override { editor.setBounds(getLocalBounds()); } - void draw() override {} -}; -using UiInput = UiInput_; -class UiValue_Input : public UiWidget -{ - double value_ = 0, low_ = -1e12, high_ = 1e12, step_ = 0; - - protected: - void sync_control() override; - - public: - UiInput input; - UiValue_Input(int, int, int, int, const char * = nullptr); - void value(double); - double value() const { return value_; } - void bounds(double l, double h) - { - low_ = std::min(l, h); - high_ = std::max(l, h); - } - void range(double l, double h) { bounds(l, h); } - void step(double s) { step_ = s; } - void precision(int) {} - void soft(int) {} - void resized() override { input.resize(0, 0, w(), h()); } - void draw() override {} -}; -class UiValue_Slider : public UiWidget -{ - protected: - juce::Slider slider_; - double drag_value_ = 0; - int drag_x_ = 0, drag_y_ = 0; - bool dragging_ = false; - void sync_control() override; - - public: - UiValue_Slider(int, int, int, int, const char * = nullptr); - double value() const { return slider_.getValue(); } - void value(double v) { slider_.setValue(v, juce::dontSendNotification); } - void bounds(double, double); - void range(double l, double h) { bounds(l, h); } - int handle(int) override; - void step(double); - using UiWidget::type; - void type(int) override; - void slider(int) {} - void slider_size(double) {} - void resized() override { slider_.setBounds(getLocalBounds()); } - void draw() override {} -}; -class UiDial : public UiValue_Slider -{ - public: - using UiValue_Slider::UiValue_Slider; -}; -class UiProgress : public UiWidget -{ - double value_ = 0, low_ = 0, high_ = 1; - - public: - using UiWidget::UiWidget; - void minimum(double v) { low_ = v; } - void maximum(double v) { high_ = v; } - void value(double v) - { - value_ = v; - redraw(); - } - float value() const { return static_cast(value_); } - void draw() override; -}; -struct UiMenu_Item -{ - const char *text = nullptr; - int shortcut_ = 0; - UiCallback callback_ = nullptr; - void *user_data_ = nullptr; - int flags = 0; - int shortcut() const { return shortcut_; } - const char *label() const { return text; } - void set() { flags |= UI_MENU_VALUE; } - void clear() { flags &= ~UI_MENU_VALUE; } - bool active() const { return !(flags & UI_MENU_INACTIVE); } - bool submenu() const { return false; } - const UiMenu_Item *popup(int, int) const; -}; -class UiMenu_ : public UiWidget -{ - protected: - std::vector items_{UiMenu_Item{}}; - std::vector paths_, labels_; - int selected_ = -1; - bool literal_ = false; - void repair(); - void show_popup(bool bar = false); - - public: - using UiWidget::UiWidget; - int add(const char *, int = 0, UiCallback = nullptr, void * = nullptr, int = 0); - void replace(int, const char *); - void clear(); - void copy(const UiMenu_Item *); - int find_index(const char *s) const - { - auto *p = find_item(s); - return p ? static_cast(p - items_.data()) : -1; - } - void mode(int i, int flags) - { - if (i >= 0 && i < static_cast(labels_.size())) - items_[static_cast(i)].flags = flags; - } - int size() const { return static_cast(items_.size()); } - const UiMenu_Item *menu() const; - const UiMenu_Item *mvalue() const; - const UiMenu_Item *find_item(const char *) const; - const UiMenu_Item *picked(const UiMenu_Item *); - const UiMenu_Item *test_shortcut(); - int item_pathname(char *, int, const UiMenu_Item *) const; - void value(int n) - { - selected_ = n; - sync_control(); - redraw(); - } - int value() const { return selected_; } - const char *text() const { return mvalue() ? mvalue()->label() : nullptr; } - const char *text(int n) const - { - return n >= 0 && n < static_cast(items_.size()) - ? items_[static_cast(n)].label() - : nullptr; - } - int handle(int) override; - void draw() override; -}; -class UiMenu_Bar : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; - void draw() override; - int handle(int) override; -}; -class UiMenu_Button : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; -}; -class UiChoice : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; -}; -class UiInput_Choice : public UiGroup -{ - UiInput *input_; - UiMenu_Button *menu_; - - public: - UiInput_Choice(int, int, int, int, const char * = nullptr); - UiInput *input() { return input_; } - UiMenu_Button *menubutton() { return menu_; } - const char *value() const { return input_->value(); } - void value(const char *v) { input_->value(v); } -}; -class UiHold_Browser : public UiWidget, private juce::ListBoxModel -{ - juce::ListBox list_; - std::vector rows_; - int getNumRows() override { return static_cast(rows_.size()); } - void paintListBoxItem(int, juce::Graphics &, int, int, bool) override; - void selectedRowsChanged(int) override { do_callback(); } - void listBoxItemDoubleClicked(int, const juce::MouseEvent &) override - { - Ui::clicks = 1; - do_callback(); - Ui::clicks = 0; - } - - public: - UiHold_Browser(int, int, int, int, const char * = nullptr); - ~UiHold_Browser() override { list_.setModel(nullptr); } - void add(const char *s) - { - rows_.emplace_back(s ? s : ""); - list_.updateContent(); - } - void clear() - { - rows_.clear(); - list_.updateContent(); - } - int value() const { return list_.getSelectedRow() + 1; } - void select(int n) { list_.selectRow(n - 1); } - int size() const { return static_cast(rows_.size()); } - void format_char(int) {} - void textsize(int) {} - void resized() override { list_.setBounds(getLocalBounds()); } - void draw() override {} -}; -class UiScroll : public UiGroup -{ - juce::ScrollBar vertical_{true}, horizontal_{false}; - int sx_ = 0, sy_ = 0; - bool scrolling_ = false; - struct Listener : juce::ScrollBar::Listener - { - UiScroll &owner; - explicit Listener(UiScroll &o) : owner(o) {} - void scrollBarMoved(juce::ScrollBar *, double) override; - } listener_{*this}; - void update_bars(); - - public: - enum - { - HORIZONTAL_ALWAYS = 1 - }; - UiWidget scrollbar, hscrollbar; - UiScroll(int, int, int, int, const char * = nullptr); - void scroll_to(int, int); - int xposition() const { return sx_; } - int yposition() const { return sy_; } - void draw() override - { - UiGroup::draw(); - update_bars(); - } - void mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &) override; -}; -class UiTabs : public UiGroup -{ - UiWidget *selected_ = nullptr; - void update(); - - public: - using UiGroup::UiGroup; - void value(UiWidget *w) - { - selected_ = w; - update(); - } - UiWidget *value() - { - if (!selected_ && children()) - selected_ = child(0); - return selected_; - } - void draw() override; - int handle(int) override; -}; -class UiNative_File_Chooser -{ - std::string title_, filter_, filename_, directory_; - int type_ = 0; - - public: - enum - { - BROWSE_FILE = 0, - BROWSE_SAVE_FILE = 1, - SAVEAS_CONFIRM = 2 - }; - void title(const char *s) { title_ = s ? s : ""; } - void type(int n) { type_ = n; } - void filter(const char *s) { filter_ = s ? s : ""; } - void directory(const char *s) { directory_ = s ? s : ""; } - void preset_file(const char *s) { filename_ = s ? s : ""; } - void options(int) {} - int show(); - const char *filename() const { return filename_.empty() ? nullptr : filename_.c_str(); } - const char *errmsg() const { return "File chooser failed"; } -}; -#endif diff --git a/src/gui/JUCE/velocity_lane.hpp b/src/gui/JUCE/velocity_lane.hpp deleted file mode 100644 index aec5c9d..0000000 --- a/src/gui/JUCE/velocity_lane.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef XTC_JUCE_GUI_VELOCITY_LANE_HPP -#define XTC_JUCE_GUI_VELOCITY_LANE_HPP -#include "piano_roll.hpp" -#include "theme.hpp" -#include -#include - -/* Fixed-height controller lane, separate from the vertically scrolled keys. - * X coordinates follow the piano widget, including its horizontal scroll. */ -class VelocityLane : public UiWidget { - PianoRollWidget &piano_; - uint64_t editing_ = UINT64_MAX; - unsigned int preview_ = 100; - const xtc_project *editing_project_ = nullptr; - uint64_t editing_clip_ = UINT64_MAX; - unsigned int value(int py) const { - return static_cast(std::clamp((y()+h()-8-py)*127/std::max(1,h()-34),1,127)); - } - public: - VelocityLane(int x, int y, int w, int h, PianoRollWidget &piano) - : UiWidget(x,y,w,h,"Note velocity"), piano_(piano) {} - void draw() override { - ui_push_clip(x(),y(),w(),h()); - ui_color(xtc_ui::background()); ui_rectf(x(),y(),w(),h()); - ui_color(xtc_ui::border()); ui_line(x(),y(),x()+w(),y()); - ui_font(UI_HELVETICA,11); ui_color(xtc_ui::muted()); - ui_draw("VELOCITY 1..127 / Select a note, then drag its bar or use the velocity slider",x()+8,y()+17); - size_t count = 0; xtc_project_note_count(piano_.project(),&count); - for (int pass = 0; pass < 2; ++pass) for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e); - if (!piano_.note_visible(note) || (note.id == piano_.selected_note()) != (pass == 1)) continue; - int px = piano_.note_x(note.start_tick); - unsigned int velocity = editing_ == note.id ? preview_ : note.velocity; - int top = y()+h()-8-static_cast(velocity)*(h()-34)/127; - ui_color(note.id == piano_.selected_note() ? xtc_ui::accent() : ui_rgb_color(95,137,172)); - ui_rectf(px-2,top,4,y()+h()-8-top); ui_rectf(px-4,top-2,8,4); - } - ui_pop_clip(); - } - int handle(int event) override { - if (event == UI_PUSH && Ui::event_button() == UI_LEFT_MOUSE && Ui::event_y() >= y()+24) { - take_focus(); editing_ = UINT64_MAX; - size_t count = 0; int distance = 9; - xtc_project_note_count(piano_.project(),&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e); - if (!piano_.note_visible(note)) continue; - int d = std::abs(Ui::event_x()-piano_.note_x(note.start_tick)); - if (d < distance || (d == distance && note.id == piano_.selected_note())) { - distance = d; editing_ = note.id; - } - } - if (editing_ != UINT64_MAX) { - editing_project_ = piano_.project(); editing_clip_ = piano_.clip(); - piano_.select_note(editing_); preview_ = value(Ui::event_y()); - } - redraw(); piano_.redraw(); return 1; - } - if (event == UI_DRAG && editing_ != UINT64_MAX) { - preview_ = value(Ui::event_y()); redraw(); return 1; - } - if (event == UI_RELEASE && editing_ != UINT64_MAX) { - uint64_t id = editing_; editing_ = UINT64_MAX; - if (editing_project_ == piano_.project() && editing_clip_ == piano_.clip()) - (void)piano_.set_velocity(id,value(Ui::event_y())); - redraw(); return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { editing_ = UINT64_MAX; redraw(); return 1; } - if (event == UI_FOCUS || event == UI_UNFOCUS) return 1; - return UiWidget::handle(event); - } -}; -#endif diff --git a/tests/integration/README.md b/tests/integration/README.md index 064c8ea..80646e3 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -254,29 +254,3 @@ It requires an existing JACK/PipeWire server, changes no user preferences and connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include `playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent input/output/both, returning devices and genuine backend failures. - -The optional JUCE workstation builds the shared GUI regression source against -`src/gui/JUCE` as `test_juce_gui`; `juce-gui-*` CTests cover the same retained -commands, fault injection, dialogs, hierarchy and gestures without FLTK. Build -all test fixtures before running them. The real-backend `test_juce_playback` -delivers JUCE MouseEvents to native transport buttons, resumes with a JUCE -KeyPress, and checks finite nonzero stereo samples through a private JACK probe -with missing saved MIDI devices. It writes no user preferences and connects no -physical ports. Run it from the repository root under Xvfb after building: - -```sh -cmake --build build/juce-gui --parallel 4 -ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1 -xvfb-run -a ./build/juce-gui/test_juce_playback -``` - -`XTC_ENABLE_LIVE_TESTS` registers the latter as `juce-playback`. An optional -`XTC_JUCE_SCREENSHOT` absolute output path captures the actual workstation -component during that live regression. - -Linux qualification used JUCE 8.0.15: all 12 JUCE GUI workflows passed (with -focused reruns after native focus/blur fixes), alongside all 57 existing -FLTK/core tests. Native Play/Stop/Space, expanded-track rendering and finite -stereo output passed with deliberately unavailable saved MIDI devices. Bare -Xvfb can emit JUCE X11 BadAtom diagnostics without a window manager; these do -not indicate a failed assertion in the workflow checks. diff --git a/tests/integration/test_juce_playback.cpp b/tests/integration/test_juce_playback.cpp deleted file mode 100644 index 7cc693f..0000000 --- a/tests/integration/test_juce_playback.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/* Native JUCE GUI playback regression. Connects only to a private software probe; - * neither user preferences nor physical audio/MIDI routes are changed. */ -#include "../../src/gui/JUCE/main_window.hpp" -#include "../../src/gui/JUCE/sequencer_track_widget.hpp" -#include "../../src/gui/JUCE/sequencer_widget.hpp" -#include "../../src/gui/JUCE/ui.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define REQUIRE(c) \ - do \ - { \ - if (!(c)) \ - throw std::runtime_error(#c); \ - } while (0) -namespace -{ -struct Probe -{ - jack_client_t *client = nullptr; - jack_port_t *ports[2]{}; - std::atomic heard[2]{}; - std::atomic invalid{false}; - ~Probe() - { - if (client) - jack_client_close(client); - } - static int process(jack_nframes_t frames, void *data) - { - auto &probe = *static_cast(data); - for (int channel = 0; channel < 2; ++channel) - { - auto *samples = - static_cast(jack_port_get_buffer(probe.ports[channel], frames)); - for (jack_nframes_t i = 0; i < frames; ++i) - { - if (!std::isfinite(samples[i])) - probe.invalid.store(true, std::memory_order_relaxed); - if (std::fabs(samples[i]) > .001f) - probe.heard[channel].store(true, std::memory_order_relaxed); - } - } - return 0; - } -}; -struct TemporaryDirectory -{ - std::string path; - TemporaryDirectory() - { - char name[] = "/tmp/xtc-playback-XXXXXX"; - REQUIRE(mkdtemp(name)); - path = name; - } - ~TemporaryDirectory() { std::filesystem::remove_all(path); } -}; -UiWidget *find(UiGroup &group, const char *text, bool exact = true) -{ - for (int i = 0; i < group.children(); ++i) - { - auto *child = group.child(i); - if (child->label() && (exact ? !std::strcmp(child->label(), text) - : std::strstr(child->label(), text) != nullptr)) - return child; - if (auto *nested = dynamic_cast(child)) - if (auto *result = find(*nested, text, exact)) - return result; - } - return nullptr; -} -SequencerWidget *timeline(UiGroup &group) -{ - for (int i = 0; i < group.children(); ++i) - { - if (auto *s = dynamic_cast(group.child(i))) - return s; - if (auto *nested = dynamic_cast(group.child(i))) - if (auto *s = timeline(*nested)) - return s; - } - return nullptr; -} -void pump(int milliseconds) -{ - auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); - while (std::chrono::steady_clock::now() < end) - Ui::wait(.01); -} -void mouse_click(juce::Component *target, juce::Point point) -{ - REQUIRE(target); - auto now = juce::Time::getCurrentTime(); - juce::MouseEvent down(juce::Desktop::getInstance().getMainMouseSource(), point, - juce::ModifierKeys(juce::ModifierKeys::leftButtonModifier), 1, 0, 0, 0, 0, - target, target, now, point, now, 1, false); - target->mouseDown(down); - juce::MouseEvent up(juce::Desktop::getInstance().getMainMouseSource(), point, - juce::ModifierKeys(), 1, 0, 0, 0, 0, target, target, now, point, now, 1, - false); - target->mouseUp(up); -} -void click(MainWindow &, UiWidget *button) -{ - REQUIRE(button && button->active()); - // Deliver JUCE MouseEvents to the native TextButton child. - juce::Component *target = nullptr; - for (int i = 0; i < button->getNumChildComponents(); ++i) - if (dynamic_cast(button->getChildComponent(i))) - target = button->getChildComponent(i); - REQUIRE(target); - mouse_click(target, target->getLocalBounds().toFloat().getCentre()); -} - -} // namespace -int main() -{ - UiInitialiser initialise; - try - { - TemporaryDirectory directory; - xtc_settings settings{}; - xtc_settings_defaults(&settings); - std::strcpy(settings.midi_input_port, "xtc-missing-production-keyboard:out"); - std::strcpy(settings.midi_output_port, "xtc-missing-production-synth:in"); - std::snprintf(settings.client_name, sizeof(settings.client_name), "xtc-smoke-%ld", - static_cast(getpid())); - settings.main_x = 0; - settings.main_y = 0; - settings.width = 1280; - settings.height = 800; - settings.transport_detached = settings.sequencer_detached = settings.graph_detached = - settings.mixer_detached = 0; - settings.loop_enabled = 0; - settings.stop_after_tail = 0; - std::string config = directory.path + "/preferences.conf"; - REQUIRE(xtc_settings_save(&settings, config.c_str()) == XTC_OK); - Probe probe; - jack_status_t status{}; - probe.client = jack_client_open("xtc-playback-probe", JackNoStartServer, &status); - REQUIRE(probe.client); - probe.ports[0] = - jack_port_register(probe.client, "left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - probe.ports[1] = - jack_port_register(probe.client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - REQUIRE(probe.ports[0] && probe.ports[1]); - REQUIRE(jack_set_process_callback(probe.client, Probe::process, &probe) == 0 && - jack_activate(probe.client) == 0); - MainWindow window(false, config.c_str()); - window.show(); - pump(100); - REQUIRE(window.open_project("examples/projects/playback-test.xtc") == XTC_OK); - auto *s = timeline(window); - REQUIRE(s); - click(window, find(window, "Play")); - pump(300); - if (!find(window, "running", false)) - { - if (auto *error = find(window, "Start failed", false)) - std::fprintf(stderr, "Observed: %s\n", error->label()); - } - REQUIRE(find(window, "running", false)); - REQUIRE(find(window, "MIDI device unavailable", false)); - for (int channel = 0; channel < 2; ++channel) - { - std::string source = - std::string(settings.client_name) + (channel ? ":output_right" : ":output"); - int code = -1; - for (int attempt = 0; attempt < 50; ++attempt) - { - code = jack_connect(probe.client, source.c_str(), - jack_port_name(probe.ports[channel])); - if (code == 0 || code == EEXIST) - break; - pump(10); - } - REQUIRE(code == 0 || code == EEXIST); - } - auto start = s->position(); - pump(700); - REQUIRE(s->position() > start); - REQUIRE(probe.heard[0].load() && probe.heard[1].load() && !probe.invalid.load()); - click(window, find(window, "Stop")); - pump(300); - auto paused = s->position(); - pump(200); - REQUIRE(s->position() == paused); - // Space resumes after a sequencer child has taken focus. - s->take_focus(); - REQUIRE(static_cast(s)->keyPressed(juce::KeyPress(' '))); - pump(300); - REQUIRE(s->position() > paused); - // A child at local (0,0) must paint consistently when composed into - // the full workstation; this catches retained-coordinate origin errors. - xtc_track_info track{}; - REQUIRE(xtc_project_get_track(s->project(), 0, &track) == XTC_OK); - auto *header = s->track_widget(track.id)->header(); - auto *menu = window.child(0); - REQUIRE(header->isShowing() && menu->isShowing()); - mouse_click(header, {14, 12}); - pump(60); - REQUIRE(s->track_expanded(track.id)); - auto menu_image = menu->createComponentSnapshot(menu->getLocalBounds()); - auto header_image = header->createComponentSnapshot(header->getLocalBounds()); - auto image = window.createComponentSnapshot(window.getLocalBounds()); - REQUIRE(image.getPixelAt(2, 2) == menu_image.getPixelAt(2, 2)); - REQUIRE(image.getPixelAt(header->x() + 5, header->y() + 5) == - header_image.getPixelAt(5, 5)); - if (const char *capture = std::getenv("XTC_JUCE_SCREENSHOT")) - { - juce::FileOutputStream stream{juce::File(capture)}; - REQUIRE(stream.openedOk()); - juce::PNGImageFormat png; - REQUIRE(png.writeImageToStream(image, stream)); - } - window.do_callback(); - REQUIRE(!window.shown()); - xtc_settings saved{}; - REQUIRE(xtc_settings_load(&saved, config.c_str()) == XTC_OK); - REQUIRE(!std::strcmp(saved.midi_input_port, settings.midi_input_port) && - !std::strcmp(saved.midi_output_port, settings.midi_output_port)); - std::puts("Live Play/Stop/Space and stereo playback passed with unavailable saved MIDI " - "devices; preferences retained."); - return 0; - } - catch (const std::exception &error) - { - std::fprintf(stderr, "Playback regression failed: %s\n", error.what()); - return 1; - } -} diff --git a/tools/install.py b/tools/install.py index 37a2924..f6aabde 100644 --- a/tools/install.py +++ b/tools/install.py @@ -11,13 +11,12 @@ parser = argparse.ArgumentParser(description=__doc__) arguments(parser) parser.add_argument('--build-dir', type=Path, required=True) parser.add_argument('--gui', action='store_true') -parser.add_argument('--juce-gui', action='store_true') args = parser.parse_args() source = Path(__file__).resolve().parent.parent entries = [] for destination, original in inventory(source, layout(args)): name = destination.name - if (name == 'xtc-juce' and not args.juce_gui) or name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')): + if name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')): continue if original is None: original = args.build_dir / name @@ -33,7 +32,7 @@ for destination, original in entries: try: with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file: shutil.copyfileobj(input_file, output) - os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check', 'xtc-juce') else 0o644) + os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check') else 0o644) os.replace(pending, destination) finally: Path(pending).unlink(missing_ok=True) diff --git a/tools/install_layout.py b/tools/install_layout.py index 7d11c4d..ff1ce27 100644 --- a/tools/install_layout.py +++ b/tools/install_layout.py @@ -30,7 +30,6 @@ def directory(value): def inventory(source, paths): entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None), ('bindir', 'xtc-check', None), ('bindir', 'xtc', None), - ('bindir', 'xtc-juce', None), ('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'), ('licensedir', 'LICENSE', source / 'LICENSE'), ('licensedir', 'CLAP-LICENSE', source / 'third_party/clap/LICENSE'),