Revert "Added an alternative GUI based on JUCE; I like FLTK more..."
This reverts commit cd2f87a8b3.
This commit is contained in:
parent
cd2f87a8b3
commit
d6e46f67d7
58 changed files with 10 additions and 11136 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
# Build products
|
# Build products
|
||||||
/build/
|
/build/
|
||||||
/build-juce-gui/
|
|
||||||
/cmake-build-*/
|
/cmake-build-*/
|
||||||
/CMakeUserPresets.json
|
/CMakeUserPresets.json
|
||||||
/*.o
|
/*.o
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter)
|
||||||
find_package(PkgConfig QUIET)
|
find_package(PkgConfig QUIET)
|
||||||
option(XTC_WITH_JACK "Discover the optional JACK backend" ON)
|
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_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_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_ENABLE_LIVE_TESTS "Register tests requiring an existing JACK server" OFF)
|
||||||
option(XTC_BUILD_JUCE "Build optional externally supplied JUCE examples" OFF)
|
option(XTC_BUILD_JUCE "Build optional externally supplied JUCE examples" OFF)
|
||||||
|
|
@ -123,7 +122,6 @@ foreach(preview quality builtin)
|
||||||
endforeach()
|
endforeach()
|
||||||
include(cmake/Tests.cmake)
|
include(cmake/Tests.cmake)
|
||||||
include(cmake/Gui.cmake)
|
include(cmake/Gui.cmake)
|
||||||
include(cmake/JuceGui.cmake)
|
|
||||||
include(cmake/Developer.cmake)
|
include(cmake/Developer.cmake)
|
||||||
include(cmake/Install.cmake)
|
include(cmake/Install.cmake)
|
||||||
if(XTC_BUILD_STEINBERG)
|
if(XTC_BUILD_STEINBERG)
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@
|
||||||
|
|
||||||
- **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph.
|
- **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.
|
- **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).
|
- **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.
|
- **Linux first:** keep backend-specific details behind an interface.
|
||||||
- **Small increments:** build and test each milestone before adding features.
|
- **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
|
## 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
|
```sh
|
||||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
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.
|
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
|
## Run
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# Retain the audited owned-file inventory and symlink-safe staging implementation.
|
# 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")
|
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})
|
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_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")
|
set(XTC_INSTALL_EXAMPLESDIR "${CMAKE_INSTALL_DATAROOTDIR}/xtc/examples" CACHE STRING "Example install directory")
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,6 @@ else()
|
||||||
if("@XTC_INSTALL_GUI@")
|
if("@XTC_INSTALL_GUI@")
|
||||||
list(APPEND extra --gui)
|
list(APPEND extra --gui)
|
||||||
endif()
|
endif()
|
||||||
if("@XTC_INSTALL_JUCE_GUI@")
|
|
||||||
list(APPEND extra --juce-gui)
|
|
||||||
endif()
|
|
||||||
set(jack)
|
set(jack)
|
||||||
if("@JACK_FOUND@" AND "@XTC_WITH_JACK@")
|
if("@JACK_FOUND@" AND "@XTC_WITH_JACK@")
|
||||||
set(jack jack)
|
set(jack jack)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
"$<$<CONFIG:Debug>:DEBUG=1;_DEBUG=1>" "$<$<NOT:$<CONFIG:Debug>>:_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 <FL/[^>]+>" "#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" $<TARGET_FILE:test_juce_gui>)
|
|
||||||
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" $<TARGET_FILE:test_juce_playback>)
|
|
||||||
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" $<TARGET_FILE:test_juce_gui>)
|
|
||||||
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=$<TARGET_FILE:clap-fixture>" "XTC_TEST_VST2=$<TARGET_FILE:vst2-fixture>" "XTC_TEST_VST3=$<TARGET_FILE:vst3-fixture>")
|
|
||||||
xtc_juce_gui_variant(instruments "XTC_TEST_SYNTH_CLAP=$<TARGET_FILE:clap-synth-fixture>" "XTC_TEST_SYNTH_VST2=$<TARGET_FILE:vst2-synth-fixture>" "XTC_TEST_SYNTH_VST3=$<TARGET_FILE:vst3-synth-fixture>")
|
|
||||||
if(XTC_LADSPA_AVAILABLE)
|
|
||||||
xtc_juce_gui_variant(plugins "XTC_TEST_PLUGIN=$<TARGET_FILE:plugin-fixture>")
|
|
||||||
xtc_juce_gui_variant(track-fx "XTC_TEST_TRACK_FX_ONLY=1" "XTC_TEST_PLUGIN=$<TARGET_FILE:plugin-fixture>" "XTC_TEST_CLAP=$<TARGET_FILE:clap-fixture>" "XTC_TEST_VST2=$<TARGET_FILE:vst2-fixture>" "XTC_TEST_VST3=$<TARGET_FILE:vst3-fixture>")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/tests/integration/test_gui.cpp")
|
|
||||||
|
|
@ -11,8 +11,7 @@ the graph editor is an advanced view of the same project.
|
||||||
|
|
||||||
## Layers
|
## Layers
|
||||||
|
|
||||||
1. **C++ GUI:** input, drawing, windows, view state. FLTK is the default;
|
1. **FLTK/C++ GUI:** input, drawing, windows, view state.
|
||||||
`src/gui/JUCE` is an independently optional JUCE frontend using the same C API.
|
|
||||||
2. **C core:** project model, commands, parameters, persistence, validation.
|
2. **C core:** project model, commands, parameters, persistence, validation.
|
||||||
3. **C engine:** prepared processing graph, DSP, transport timing, event
|
3. **C engine:** prepared processing graph, DSP, transport timing, event
|
||||||
delivery.
|
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,
|
0008. Release tooling lives outside the runtime, with source manifests,
|
||||||
deterministic exports and staged installation/consumer checks. See RELEASING.md
|
deterministic exports and staged installation/consumer checks. See RELEASING.md
|
||||||
for reproducibility scope.
|
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).
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ lists, dependencies, SDK and packaging rules in CMake.
|
||||||
| Cache option | Default | Meaning |
|
| Cache option | Default | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `BUILD_TESTING` | `ON` | Build/register the test suite |
|
| `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_BUILD_GUI` | `ON` | Discover FLTK and build the application when available |
|
||||||
| `XTC_WITH_JACK` | `ON` | Discover/build the JACK/PipeWire backend |
|
| `XTC_WITH_JACK` | `ON` | Discover/build the JACK/PipeWire backend |
|
||||||
| `XTC_WITH_LADSPA` | `ON` | Enable LADSPA when the system header exists |
|
| `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.
|
remove only the selected build directory. Never configure in the source tree.
|
||||||
Historical validation entries in ROADMAP describe the build used at that time;
|
Historical validation entries in ROADMAP describe the build used at that time;
|
||||||
Phase 108 records the CMake migration and its verified results.
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
# GUI Guidelines
|
# GUI Guidelines
|
||||||
|
|
||||||
The default frontend uses FLTK/C++ for windows, menus, dialogs, and the event loop.
|
Use FLTK/C++ for windows, menus, dialogs, and the event loop. Keep all audio
|
||||||
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
|
|
||||||
engine and project-domain logic in C.
|
engine and project-domain logic in C.
|
||||||
|
|
||||||
Main views:
|
Main views:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# JUCE integration
|
# JUCE integration (Phase 107)
|
||||||
|
|
||||||
JUCE supplies an [optional workstation frontend](../src/gui/JUCE/README.md)
|
JUCE is used by optional, runnable examples alongside xtc's C99 engine and FLTK
|
||||||
(`XTC_BUILD_JUCE_GUI`) as well as runnable plugin examples (`XTC_BUILD_JUCE`)
|
application. It is an external SDK dependency, not vendored, fetched, linked by
|
||||||
alongside xtc's C99 engine and FLTK application. The two options are independent. It is an external SDK dependency, not vendored, fetched, linked by
|
|
||||||
normal builds or automatically installed. The integration uses:
|
normal builds or automatically installed. The integration uses:
|
||||||
|
|
||||||
| JUCE capability | Implemented use |
|
| JUCE capability | Implemented use |
|
||||||
|
|
|
||||||
|
|
@ -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
|
an installed binary is distinct from SDK, trademark and plugin redistribution
|
||||||
rights. Review those separately for any proposed distribution. Source archives
|
rights. Review those separately for any proposed distribution. Source archives
|
||||||
retain upstream notices; binary installs also ship both header licenses.
|
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).
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
## Recommended Execution Order
|
||||||
|
|
||||||
Reorder the phases when the audit reveals a blocking dependency. Do not mark a
|
Reorder the phases when the audit reveals a blocking dependency. Do not mark a
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ endif()
|
||||||
message(STATUS "JUCE is AGPLv3/commercial: review the external SDK LICENSE.md before distributing these binaries")
|
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_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
set(JUCE_BUILD_EXTRAS 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)
|
||||||
add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL)
|
|
||||||
endif()
|
|
||||||
get_filename_component(XTC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
get_filename_component(XTC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
||||||
if(NOT TARGET xtc-core)
|
if(NOT TARGET xtc-core)
|
||||||
set(XTC_BUILD_GUI OFF CACHE BOOL "" FORCE)
|
set(XTC_BUILD_GUI OFF CACHE BOOL "" FORCE)
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,6 @@ See [capture notes and reproduction instructions](README.md) for image provenanc
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The optional JUCE workstation
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -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;
|
configuration are not overwritten. Black margins are the virtual desktop;
|
||||||
floating views show the actual detached windows together.
|
floating views show the actual detached windows together.
|
||||||
|
|
||||||
## Optional JUCE workstation (Phase 113)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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)
|
## Continuous audio velocity editing (Phase 110)
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
|
|
@ -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).
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
#ifndef XTC_COMMAND_PALETTE_HPP
|
|
||||||
#define XTC_COMMAND_PALETTE_HPP
|
|
||||||
#include "theme.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
/* UI command search only; execute the chosen action after this modal is gone. */
|
|
||||||
class CommandPalette : public UiDouble_Window
|
|
||||||
{
|
|
||||||
std::vector<std::string> commands_, visible_;
|
|
||||||
UiInput *search_;
|
|
||||||
UiHold_Browser *list_;
|
|
||||||
std::string selected_;
|
|
||||||
static std::string lower(std::string value)
|
|
||||||
{
|
|
||||||
for (auto &c : value)
|
|
||||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<size_t>(i) <= visible_.size()) {
|
|
||||||
selected_ = visible_[static_cast<size_t>(i - 1)];
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit CommandPalette(std::vector<std::string> 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<CommandPalette *>(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<CommandPalette *>(p)->accept();
|
|
||||||
},
|
|
||||||
this);
|
|
||||||
auto *cancel = new UiButton(450, 390, 100, 30, "Cancel");
|
|
||||||
cancel->callback([](UiWidget *, void *p) { static_cast<CommandPalette *>(p)->hide(); }, this);
|
|
||||||
auto *run = new UiButton(564, 390, 116, 30, "Run command");
|
|
||||||
run->callback([](UiWidget *, void *p) { static_cast<CommandPalette *>(p)->accept(); }, this);
|
|
||||||
callback([](UiWidget *, void *p) { static_cast<CommandPalette *>(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<int>(visible_.size())));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UiDouble_Window::handle(event);
|
|
||||||
}
|
|
||||||
std::string run()
|
|
||||||
{
|
|
||||||
show();
|
|
||||||
search_->take_focus();
|
|
||||||
while (shown())
|
|
||||||
Ui::wait();
|
|
||||||
return selected_;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,645 +0,0 @@
|
||||||
#include "theme.hpp"
|
|
||||||
#include "graph_widget.hpp"
|
|
||||||
#include "xtc/xtc_plugin.h"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
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<int>(slot % 8U) * 210;
|
|
||||||
position.top = 40 + static_cast<int>(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<int>(ux*9-uy*5), ay-static_cast<int>(uy*9+ux*5));
|
|
||||||
ui_line(ax, ay, ax-static_cast<int>(ux*9+uy*5), ay-static_cast<int>(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<double>(node.value));
|
|
||||||
else if (node.type == XTC_NODE_GAIN)
|
|
||||||
std::snprintf(text, sizeof(text), "x %.2f", static_cast<double>(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<size_t, 256> 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<int>(info.level) * 210;
|
|
||||||
positions_[i].top = 40 + static_cast<int>(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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
#ifndef XTC_GRAPH_WIDGET_HPP
|
|
||||||
#define XTC_GRAPH_WIDGET_HPP
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <array>
|
|
||||||
#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<Position, 256> positions_{};
|
|
||||||
size_t count_ = 0U, routing_count_ = 0U;
|
|
||||||
std::array<xtc_routing_info, 256> 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
|
|
||||||
|
|
@ -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 <cstdio>
|
|
||||||
#include <cmath>
|
|
||||||
#include <algorithm>
|
|
||||||
/* 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<InsertMeter *>(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<snapshot.count && i<XTC_PROJECT_MAX_TRACKS;++i)if(snapshot.tracks[i].id==track){l=snapshot.tracks[i].left;r=snapshot.tracks[i].right;found=true;break;}
|
|
||||||
if(!active || !found){left_.db=right_.db=-90;}
|
|
||||||
else {
|
|
||||||
(void)xtc_peak_meter_update(&left_,std::isfinite(l) && l>=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<double>(left_.db),static_cast<double>(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<int>(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<int>(static_cast<float>(width)*std::min(1.f,std::max(0.f,(db+90)/90))),4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#include "ui.hpp"
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <exception>
|
|
||||||
#include "main_window.hpp"
|
|
||||||
#include "xtc/xtc_version.h"
|
|
||||||
#include "xtc/xtc.h"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,557 +0,0 @@
|
||||||
#ifndef XTC_MAIN_WINDOW_HPP
|
|
||||||
#define XTC_MAIN_WINDOW_HPP
|
|
||||||
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#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<std::string> 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<size_t> &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<xtc_mix_snapshot,decltype(&xtc_mix_snapshot_destroy)>;
|
|
||||||
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<UiDouble_Window> window;bool detached=false;
|
|
||||||
};
|
|
||||||
EditorPanel editors_[2];
|
|
||||||
UiGroup *piano_page_=nullptr,*piano_content_=nullptr,*piano_placeholder_=nullptr;
|
|
||||||
bool piano_detached_=true;
|
|
||||||
std::unique_ptr<UiDouble_Window> 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<TransportItem> 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<xtc_history, decltype(&xtc_history_destroy)> 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<xtc_project, decltype(&xtc_project_destroy)> project_{nullptr, xtc_project_destroy};
|
|
||||||
std::unique_ptr<UiDouble_Window> 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
|
|
||||||
|
|
@ -1,856 +0,0 @@
|
||||||
#include "theme.hpp"
|
|
||||||
#include "mixer_widget.hpp"
|
|
||||||
#include "xtc/xtc_control.h"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
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<unsigned long long>(mixer_.lane_start() + static_cast<uint64_t>(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<xtc_automation_point> 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<int>((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<float>(static_cast<double>(tick - a.tick) / static_cast<double>(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<uint64_t>(px - 1) * lane_ticks / static_cast<uint64_t>(w() - 1);
|
|
||||||
uint64_t b =
|
|
||||||
mixer_.lane_start() + static_cast<uint64_t>(px) * lane_ticks / static_cast<uint64_t>(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<int>((a.tick - mixer_.lane_start()) * static_cast<uint64_t>(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<uint64_t>(std::clamp(Ui::event_x() - x(), 0, w() - 1)) *
|
|
||||||
lane_ticks / static_cast<uint64_t>(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<float>(std::clamp(y() + h() - 6 - Ui::event_y(), 0, h() - 28)) /
|
|
||||||
static_cast<float>(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<uint64_t>(8) * lane_ticks / static_cast<uint64_t>(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<int>((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<uint64_t> destinations_;
|
|
||||||
static void control(UiWidget *widget, void *context)
|
|
||||||
{
|
|
||||||
auto *s = static_cast<MixerStrip *>(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<float>(std::pow(10, s->gain_->value() / 20))))
|
|
||||||
: track.gain;
|
|
||||||
const float pan = widget == s->pan_ ? static_cast<float>(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<MixerStrip *>(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<MixerStrip *>(c);
|
|
||||||
int i = s->route_->value();
|
|
||||||
if (i < 0 || static_cast<size_t>(i) >= s->destinations_.size())
|
|
||||||
return;
|
|
||||||
auto bus = s->destinations_[static_cast<size_t>(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<UiWidget *>(gain_), static_cast<UiWidget *>(pan_),
|
|
||||||
static_cast<UiWidget *>(mute_), static_cast<UiWidget *>(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<double>(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<int>(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<double>(meter[0].db),
|
|
||||||
static_cast<double>(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<int>((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<int>((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<MixerWidget *>(c);
|
|
||||||
int index = m->routes_->value();
|
|
||||||
if (index >= 0 && static_cast<size_t>(index) < m->route_ids_.size())
|
|
||||||
m->route(m->route_ids_[static_cast<size_t>(index)]);
|
|
||||||
},
|
|
||||||
this);
|
|
||||||
bus_ = new UiButton(x + 820, y + 8, 110, 26, "Add bus");
|
|
||||||
bus_->callback([](UiWidget *, void *c) { static_cast<MixerWidget *>(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<MixerWidget *>(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<uint64_t>(
|
|
||||||
std::clamp(start_->value(), 0.0, static_cast<double>(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<int>(i);
|
|
||||||
}
|
|
||||||
tracks_->value(n ? index : -1);
|
|
||||||
selected_id_=UINT64_MAX;
|
|
||||||
if(n){xtc_track_info track{};xtc_project_get_track(p,static_cast<size_t>(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<size_t>(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<int>(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<double>(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<double>(master_[0].db),static_cast<double>(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<unsigned int>(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<MixerWidget *>(c);
|
|
||||||
if(widget==m->tracks_){xtc_track_info track{};
|
|
||||||
if(xtc_project_get_track(m->project_,static_cast<size_t>(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<UiInput_ *>(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<size_t>(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<int>(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<double>(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<double>(master_[0].db),static_cast<double>(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<int>(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<size_t>(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<int>(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();
|
|
||||||
}
|
|
||||||
|
|
@ -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 <chrono>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
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<uint64_t> route_ids_;
|
|
||||||
UiProgress *left_, *right_;
|
|
||||||
UiScroll *bank_;
|
|
||||||
UiGroup *master_panel_;
|
|
||||||
UiButton *bus_, *reset_;
|
|
||||||
UiBox *hint_, *master_readout_;
|
|
||||||
std::vector<MixerStrip *> 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
|
|
||||||
|
|
@ -1,455 +0,0 @@
|
||||||
#include "theme.hpp"
|
|
||||||
#include "snap.hpp"
|
|
||||||
#include "piano_roll.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int key_width = 64, row_height = 16, scale = 96, header = 28;
|
|
||||||
template <typename... Values> 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<int>(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<uint64_t>(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<uint64_t>(120, grid_);
|
|
||||||
int spacing = static_cast<int>(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<int>(static_cast<uint64_t>(step) * visual * scale / 960);
|
|
||||||
ui_color((static_cast<uint64_t>(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<int>(n.start_tick * scale / 960);
|
|
||||||
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
|
||||||
int py = y() + header + (127 - static_cast<int>(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<uchar>(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<unsigned long long>(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<int>(n.start_tick * scale / 960);
|
|
||||||
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
|
||||||
int top = y() + header + (127 - static_cast<int>(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<int>(low_) && pitch <= static_cast<int>(high_) && xtc_project_find_clip(project_, clip_, &c) == XTC_OK &&
|
|
||||||
start < c.source_length_tick)
|
|
||||||
add_note(start, std::min<uint64_t>(new_note_ticks, c.source_length_tick - start),
|
|
||||||
static_cast<unsigned int>(pitch), new_note_velocity, channel_filter_ < 0 ? new_note_channel : static_cast<unsigned int>(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<int64_t>(Ui::event_x() - drag_x_) * 960 / scale / static_cast<int64_t>(grid_) * static_cast<int64_t>(grid_);
|
|
||||||
xtc_clip_info c{};
|
|
||||||
if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK)
|
|
||||||
return 1;
|
|
||||||
int64_t start = static_cast<int64_t>(original_.start_tick),
|
|
||||||
length = static_cast<int64_t>(original_.length_tick);
|
|
||||||
unsigned int pitch = original_.pitch;
|
|
||||||
if (mode == 1) {
|
|
||||||
start = std::clamp<int64_t>(start + delta, 0, static_cast<int64_t>(c.source_length_tick) - length);
|
|
||||||
pitch = static_cast<unsigned int>(
|
|
||||||
std::clamp(static_cast<int>(pitch) - (Ui::event_y() - drag_y_) / row_height, 0, 127));
|
|
||||||
} else {
|
|
||||||
int64_t maximum = static_cast<int64_t>(c.source_length_tick) - start;
|
|
||||||
length = std::clamp<int64_t>(length + delta, std::min<int64_t>(static_cast<int64_t>(grid_), maximum), maximum);
|
|
||||||
}
|
|
||||||
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
|
||||||
if (r == XTC_OK)
|
|
||||||
r = xtc_project_edit_note(project_, selected_, static_cast<uint64_t>(start), static_cast<uint64_t>(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<unsigned int>(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<uint64_t>(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;
|
|
||||||
}
|
|
||||||
|
|
@ -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<int>(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
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
#include "theme.hpp"
|
|
||||||
#include "plugin_dialog.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <array>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
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<Dialog *>(data); d->accepted = true; d->hide(); }
|
|
||||||
static void cancel(UiWidget *, void *data) { static_cast<Dialog *>(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<unsigned int> 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<size_t>(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<UiValue_Input *,XTC_PLUGIN_CONTROLS> inputs{};
|
|
||||||
for (unsigned int i = 0; i < spec.control_count; ++i) {
|
|
||||||
int y = 150 + static_cast<int>(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<UiValue_Input *,XTC_PLUGIN_CONTROLS> *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<Reset *>(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<float>(value);
|
|
||||||
}
|
|
||||||
if (reset.used) candidate.point_count = 0;
|
|
||||||
edited = candidate; return true;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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 <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <set>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
inline std::vector<std::string> xtc_plugin_files(const xtc_settings &settings)
|
|
||||||
{
|
|
||||||
namespace fs=std::filesystem;
|
|
||||||
std::set<std::string> 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<char>(std::tolower(static_cast<unsigned char>(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<std::string> 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<UiWidget *>(search),static_cast<UiWidget *>(format)})w->callback([](UiWidget *,void *c){static_cast<Library *>(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<Library *>(c);d->browse=true;d->hide();},this);
|
|
||||||
auto *cancel=new UiButton(650,506,100,32,"Cancel");cancel->callback([](UiWidget *,void *c){static_cast<Library *>(c)->hide();},this);
|
|
||||||
choose=new UiButton(764,506,120,32,"Inspect plugin");choose->callback([](UiWidget *,void *c){auto *d=static_cast<Library *>(c);d->accepted=true;d->hide();},this);
|
|
||||||
callback([](UiWidget *,void *c){static_cast<Library *>(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<size_t>(dialog.list->value()-1)];return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
@ -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 <functional>
|
|
||||||
#include <cstring>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class PreferencesDialog : public UiDouble_Window {
|
|
||||||
xtc_settings original_;
|
|
||||||
std::function<xtc_result(const xtc_settings &)> apply_;
|
|
||||||
std::vector<UiInput *> inputs_;
|
|
||||||
UiBox *message_;
|
|
||||||
std::function<std::vector<std::string>(bool)> devices_;
|
|
||||||
std::vector<std::string> midi_names_[2];
|
|
||||||
UiInput_Choice *midi_input_=nullptr,*midi_output_=nullptr;
|
|
||||||
void refresh_devices() {
|
|
||||||
for(auto pair:{std::pair<UiInput_Choice *,bool>{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<std::string>{};
|
|
||||||
names.insert(names.begin(),"");std::vector<UiMenu_Item> items(names.size()+1);
|
|
||||||
for(size_t i=0;i<names.size();++i)items[i].text=i ? names[i].c_str() : "(None)";
|
|
||||||
pair.first->menubutton()->copy(items.data());pair.first->value(value.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void apply_callback(UiWidget *,void *context) {
|
|
||||||
auto *d=static_cast<PreferencesDialog *>(context);
|
|
||||||
xtc_settings candidate=d->original_; size_t count=0;
|
|
||||||
const xtc_setting_info *schema=xtc_settings_schema(&count);
|
|
||||||
for(size_t i=0;i<count;++i) if(xtc_settings_set(&candidate,i,d->inputs_[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<xtc_result(const xtc_settings &)> apply,
|
|
||||||
std::function<std::vector<std::string>(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;i<count;++i) {
|
|
||||||
if(section!=schema[i].section) {
|
|
||||||
if(scroll) { scroll->end(); 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<UiInput_Choice *>(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<int>(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<PreferencesDialog *>(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<PreferencesDialog *>(c)->hide();},this);
|
|
||||||
callback([](UiWidget *,void *c){static_cast<PreferencesDialog *>(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
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
#include "sample_editor.hpp"
|
|
||||||
#include "theme.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <string>
|
|
||||||
#include <stdexcept>
|
|
||||||
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<size_t>(std::max(0,std::min(w()-1,px-x())))*width/static_cast<size_t>(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<w();++px){size_t b=editor_.origin()+static_cast<size_t>(px)*width/static_cast<size_t>(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast<size_t>(px+1)*width/static_cast<size_t>(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1;
|
|
||||||
if(b<editor_.end() && e>editor_.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<int>(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast<int>(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<editor_.origin()||frame-editor_.origin()>=width)continue;int px=x()+static_cast<int>((frame-editor_.origin())*static_cast<size_t>(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<double>(frames_));input->when(UI_WHEN_RELEASE);input->callback([](UiWidget *,void *c){auto *e=static_cast<SampleEditor *>(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_&&o<e->frames_){e->select(static_cast<size_t>(b),static_cast<size_t>(z));e->view(static_cast<size_t>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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_operation>(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<SampleEditor *>(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<SampleEditor *>(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<size_t>(std::min(static_cast<double>(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<SampleEditor *>(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<SampleEditor *>(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<size_t>(std::min(static_cast<double>(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<SampleEditor *>(c)->slice_clip();if(r!=XTC_OK)ui_alert("Clip unchanged: %s",xtc_result_string(r));else static_cast<SampleEditor *>(c)->hide();},this);
|
|
||||||
auto *undo=new UiButton(850,604,110,30,"Undo / redo");undo->callback([](UiWidget *,void *c){auto r=static_cast<SampleEditor *>(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<SampleEditor *>(c)->hide();},this);callback([](UiWidget *,void *c){static_cast<SampleEditor *>(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<double>(begin_));end_input_->value(static_cast<double>(end_));origin_input_->value(static_cast<double>(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<double>(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<size_t> 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;}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
#ifndef XTC_SAMPLE_EDITOR_HPP
|
|
||||||
#define XTC_SAMPLE_EDITOR_HPP
|
|
||||||
#include "xtc/xtc_sample.h"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
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<xtc_sample,decltype(&xtc_sample_destroy)>;
|
|
||||||
explicit SampleEditor(Data sample);
|
|
||||||
std::function<xtc_result(unsigned int,size_t,size_t)> trim;
|
|
||||||
xtc_result trim_clip();
|
|
||||||
std::function<xtc_result(const xtc_sample *,const char *)> 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<xtc_result(unsigned int,const std::vector<size_t> &)> slice;
|
|
||||||
xtc_result detect_attacks(double threshold,size_t gap);
|
|
||||||
xtc_result slice_clip();
|
|
||||||
const std::vector<size_t> &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<xtc_sample,decltype(&xtc_sample_destroy)> sample_{nullptr,xtc_sample_destroy};
|
|
||||||
std::unique_ptr<xtc_sample,decltype(&xtc_sample_destroy)> previous_{nullptr,xtc_sample_destroy};
|
|
||||||
std::vector<size_t> 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
|
|
||||||
|
|
@ -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 <algorithm>
|
|
||||||
|
|
||||||
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<int>(clip.start_tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
|
||||||
int width=std::max(4,static_cast<int>(clip.length_tick*static_cast<unsigned int>(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<int>((begin-c.offset_tick)*static_cast<unsigned int>(owner_.scale_)/960U);
|
|
||||||
int nw = std::max(2,static_cast<int>((end-begin)*static_cast<unsigned int>(owner_.scale_)/960U));
|
|
||||||
ui_rectf(nx,py+26+static_cast<int>((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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,412 +0,0 @@
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "snap.hpp"
|
|
||||||
#include "xtc/xtc_midi.h"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <sstream>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
// 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<uint64_t> SequencerWidget::selected_clips() const
|
|
||||||
{
|
|
||||||
std::vector<uint64_t> 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<clips;++i){xtc_clip_info c{};xtc_project_get_clip(project_,i,&c);selection_.push_back(c.id);}
|
|
||||||
if(!selection_.empty()){xtc_clip_info c{};clip_=selection_.back();xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;}
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
xtc_result SequencerWidget::grid(uint64_t ticks)
|
|
||||||
{
|
|
||||||
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
|
|
||||||
grid_ = ticks; redraw(); return XTC_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
xtc_result SequencerWidget::transform_selection(xtc_clip_operation op,int64_t amount)
|
|
||||||
{
|
|
||||||
auto ids=selected_clips();std::vector<uint64_t> 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<uint64_t>{} : 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<int64_t>(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<int> &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<int> 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<uint64_t>(grid);edit.swing=static_cast<unsigned int>(swing);edit.strength=static_cast<unsigned int>(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<unsigned int>(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<uint64_t> 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<int64_t>(grid_) : static_cast<int64_t>(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<int64_t>(end)-static_cast<int64_t>(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<double>(clip.gain),
|
|
||||||
static_cast<unsigned long long>(clip.fade_in_tick), static_cast<unsigned long long>(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);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "sequencer_track_widget.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
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;i<tracks;++i) {xtc_project_get_track(project_,i,&t); if(t.id==view->track_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<int>(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<SequencerWidget *>(context);
|
|
||||||
auto *choice = static_cast<UiChoice *>(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<int>(owner->catalog_.size())+1;
|
|
||||||
xtc_plugin_spec spec{};
|
|
||||||
if (index > 0 && !browse) {
|
|
||||||
if (static_cast<size_t>(index) > owner->catalog_.size()) return;
|
|
||||||
spec = owner->catalog_[static_cast<size_t>(index)-1];
|
|
||||||
}
|
|
||||||
if (owner->instrument_select) owner->instrument_select(owner->context_,track,index > 0 && !browse ? &spec : nullptr,browse);
|
|
||||||
owner->refresh_instruments(); owner->redraw(); return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
#include "sequencer_ruler_widget.hpp"
|
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "theme.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
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<int>(begin*static_cast<unsigned int>(owner_.scale_)/960U);
|
|
||||||
int right=owner_.x()+owner_.header_width_+static_cast<int>(end*static_cast<unsigned int>(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<markers;++i) {
|
|
||||||
xtc_marker marker{}; xtc_project_get_marker(owner_.project_,i,&marker);
|
|
||||||
int px=owner_.x()+owner_.header_width_+static_cast<int>(marker.tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
|
||||||
if(px<cx || px>cx+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<int>(owner_.cursor_*static_cast<unsigned int>(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()<owner_.header_left()+owner_.header_width_) return 1;
|
|
||||||
if(Ui::event_button()==UI_LEFT_MOUSE && Ui::event_state(UI_SHIFT)) {
|
|
||||||
owner_.loop_anchor_=owner_.loop_preview_=owner_.tick(Ui::event_x()); owner_.mode_=4; owner_.redraw(); return 1;
|
|
||||||
}
|
|
||||||
uint64_t target=owner_.tick(Ui::event_x());
|
|
||||||
xtc_result r=owner_.seek_(owner_.context_,target);
|
|
||||||
if(r==XTC_OK) owner_.cursor_=target;
|
|
||||||
else ui_alert("Stop playback before seeking: %s",xtc_result_string(r));
|
|
||||||
owner_.redraw(); return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#ifndef XTC_SEQUENCER_RULER_WIDGET_HPP
|
|
||||||
#define XTC_SEQUENCER_RULER_WIDGET_HPP
|
|
||||||
#include "ui.hpp"
|
|
||||||
class SequencerWidget;
|
|
||||||
class SequencerRulerWidget : public UiWidget {
|
|
||||||
SequencerWidget &owner_;
|
|
||||||
public:
|
|
||||||
explicit SequencerRulerWidget(SequencerWidget &owner);
|
|
||||||
int handle(int event) override;
|
|
||||||
protected:
|
|
||||||
void draw() override;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
#include "sequencer_track_widget.hpp"
|
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "sequencer_clip_widget.hpp"
|
|
||||||
#include "sequencer_velocity_widget.hpp"
|
|
||||||
#include "sequencer_group_scope.hpp"
|
|
||||||
#include "theme.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
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;i<clips;++i) {
|
|
||||||
xtc_clip_info c{}; xtc_project_get_clip(owner_.project_,i,&c);
|
|
||||||
if(c.track_id!=info_.id) continue;
|
|
||||||
auto *view=clip_widget(c.id);
|
|
||||||
if(!view) {
|
|
||||||
view=new SequencerClipWidget(owner_,c);
|
|
||||||
clips_.push_back(view); // Capacity reserved before creating child widgets.
|
|
||||||
}
|
|
||||||
view->sync(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<SequencerClipWidget *>(child(i));
|
|
||||||
if(view && Ui::event_x()>=view->x() && Ui::event_x()<view->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()<x()+24 && Ui::event_y()<y()+26) {
|
|
||||||
owner.expand_track(track_.info_.id,!owner.track_expanded(track_.info_.id)); return 1;
|
|
||||||
}
|
|
||||||
// The velocity label area does not change clip selection.
|
|
||||||
if(Ui::event_y()>=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);
|
|
||||||
}
|
|
||||||
|
|
@ -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 <vector>
|
|
||||||
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<SequencerClipWidget *> 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
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
#include "sequencer_velocity_widget.hpp"
|
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "theme.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
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.offset_tick || note.start_tick-c.offset_tick>=c.length_tick) return false;
|
|
||||||
px=owner_.x()+owner_.header_width_+static_cast<int>((c.start_tick+note.start_tick-c.offset_tick)*static_cast<unsigned int>(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<clips;++i) {
|
|
||||||
xtc_clip_info c{}; xtc_project_get_clip(owner_.project_,i,&c);
|
|
||||||
if(c.track_id!=track_) continue;
|
|
||||||
if(!c.midi) return true;
|
|
||||||
midi=true;
|
|
||||||
}
|
|
||||||
return !midi;
|
|
||||||
}
|
|
||||||
|
|
||||||
float SequencerVelocityWidget::audio_value(int py) const
|
|
||||||
{
|
|
||||||
return std::clamp(static_cast<float>(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<xtc_automation_point,XTC_MAX_AUTOMATION_POINTS+1> points{};
|
|
||||||
size_t count=0,total=0; xtc_project_automation_count(owner_.project_,&total);
|
|
||||||
for(size_t i=0;i<total;++i) {
|
|
||||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
|
||||||
if(a.track_id!=track.id || a.parameter!=XTC_AUTOMATION_GAIN) continue;
|
|
||||||
if(owner_.mode_==7 && owner_.active_velocity_==this && a.tick==audio_preview_.tick) continue;
|
|
||||||
points[count++]=a;
|
|
||||||
}
|
|
||||||
if(owner_.mode_==7 && owner_.active_velocity_==this) points[count++]=audio_preview_;
|
|
||||||
std::sort(points.begin(),points.begin()+count,[](const auto &a,const auto &b){return a.tick<b.tick;});
|
|
||||||
auto px=[&](uint64_t t){return owner_.x()+owner_.header_width_+static_cast<int>(t*static_cast<unsigned int>(owner_.scale_)/960U);};
|
|
||||||
auto py=[&](float v){return y()+h()-8-static_cast<int>(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;i<count;++i) {
|
|
||||||
const auto &a=points[i-1]; const auto &b=points[i];
|
|
||||||
if(a.step) {
|
|
||||||
ui_line(px(a.tick),py(a.value),px(b.tick),py(a.value));
|
|
||||||
ui_line(px(b.tick),py(a.value),px(b.tick),py(b.value));
|
|
||||||
} else ui_line(px(a.tick),py(a.value),px(b.tick),py(b.value));
|
|
||||||
}
|
|
||||||
ui_line(px(points[count-1].tick),py(points[count-1].value),right,py(points[count-1].value));
|
|
||||||
for(size_t i=0;i<count;++i) {
|
|
||||||
int point_x=px(points[i].tick);
|
|
||||||
if(point_x<left-4 || point_x>right+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<unsigned int>(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<tracks;++i) {xtc_project_get_track(owner_.project_,i,&track); if(track.id==track_) break;}
|
|
||||||
if(is_audio) draw_audio(track,left,cx+cw);
|
|
||||||
size_t notes=0; xtc_project_note_count(owner_.project_,¬es);
|
|
||||||
for(size_t n=0;!is_audio && n<notes;++n) {
|
|
||||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); int px=0;
|
|
||||||
if(!note_position(note,px)) continue;
|
|
||||||
bool editing=owner_.active_velocity_==this && owner_.mode_==6 && original_.id==note.id;
|
|
||||||
unsigned int value=editing ? preview_ : note.velocity;
|
|
||||||
int bottom=y()+h()-8,ny=bottom-static_cast<int>(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<count;++i) {
|
|
||||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
|
||||||
if(a.track_id==point.track_id && a.parameter==point.parameter && a.tick==point.tick && a.value==point.value) { owner_.redraw(); return 1; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
|
||||||
if(r==XTC_OK) r=xtc_project_set_automation(owner_.project_,&point);
|
|
||||||
owner_.changed(r); return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(owner_.mode_==6) {
|
|
||||||
if(event==UI_KEYDOWN && Ui::event_key()==UI_Escape) { owner_.mode_=0; owner_.redraw(); return 1; }
|
|
||||||
if(event==UI_DRAG) { preview_=note_value(Ui::event_y()); owner_.redraw(); return 1; }
|
|
||||||
if(event==UI_RELEASE) {
|
|
||||||
owner_.mode_=0;
|
|
||||||
unsigned int value=note_value(Ui::event_y());
|
|
||||||
if(value==original_.velocity) { owner_.redraw(); return 1; }
|
|
||||||
xtc_note_info note=original_;
|
|
||||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
|
||||||
if(r==XTC_OK) r=xtc_project_edit_note(owner_.project_,note.id,note.start_tick,note.length_tick,note.pitch,value,note.channel);
|
|
||||||
owner_.changed(r); return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(event==UI_PUSH && (Ui::event_button()==UI_LEFT_MOUSE || Ui::event_button()==UI_RIGHT_MOUSE)) {
|
|
||||||
owner_.take_focus();
|
|
||||||
owner_.active_velocity_=this;
|
|
||||||
if(audio()) {
|
|
||||||
owner_.track_=track_; owner_.clip_=UINT64_MAX; owner_.selection_.clear();
|
|
||||||
uint64_t position=std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(std::max(0,Ui::event_x()-owner_.x()-owner_.header_width_))*960/static_cast<unsigned int>(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<count;++i) {
|
|
||||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
|
||||||
if(a.track_id!=track_ || a.parameter!=XTC_AUTOMATION_GAIN) continue;
|
|
||||||
int px=owner_.x()+owner_.header_width_+static_cast<int>(a.tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
|
||||||
int py=y()+h()-8-static_cast<int>(a.value*(h()-28)/4.0f);
|
|
||||||
int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-py);
|
|
||||||
if(d<distance) { distance=d; audio_preview_=a; audio_existing_=true; }
|
|
||||||
}
|
|
||||||
if(Ui::event_button()==UI_RIGHT_MOUSE) {
|
|
||||||
if(audio_existing_) {
|
|
||||||
auto point=audio_preview_;
|
|
||||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
|
||||||
if(r==XTC_OK) r=xtc_project_remove_automation(owner_.project_,point.track_id,point.parameter,point.tick);
|
|
||||||
owner_.changed(r);
|
|
||||||
}
|
|
||||||
} else if(Ui::event_button()==UI_LEFT_MOUSE) owner_.mode_=7;
|
|
||||||
owner_.redraw(); return 1;
|
|
||||||
}
|
|
||||||
if(Ui::event_button()!=UI_LEFT_MOUSE) return 1;
|
|
||||||
size_t notes=0; xtc_project_note_count(owner_.project_,¬es);
|
|
||||||
int distance=10; bool hit=false;
|
|
||||||
|
|
||||||
for(size_t n=0;n<notes;++n) {
|
|
||||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); int px=0;
|
|
||||||
if(!note_position(note,px)) continue;
|
|
||||||
int ny=y()+h()-8-static_cast<int>(note.velocity)*(h()-28)/127;
|
|
||||||
int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-ny);
|
|
||||||
if(d<distance) { distance=d; original_=note; hit=true; }
|
|
||||||
}
|
|
||||||
if(hit) { owner_.track_=track_; owner_.clip_=original_.clip_id; owner_.mode_=6; preview_=original_.velocity; }
|
|
||||||
owner_.redraw(); return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#ifndef XTC_SEQUENCER_VELOCITY_WIDGET_HPP
|
|
||||||
#define XTC_SEQUENCER_VELOCITY_WIDGET_HPP
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include "xtc/xtc_mixer.h"
|
|
||||||
#include "xtc/xtc_midi.h"
|
|
||||||
class SequencerWidget;
|
|
||||||
class SequencerVelocityWidget : public UiWidget {
|
|
||||||
SequencerWidget &owner_;
|
|
||||||
uint64_t track_;
|
|
||||||
xtc_note_info original_{};
|
|
||||||
unsigned int preview_ = 100;
|
|
||||||
xtc_automation_point audio_preview_{};
|
|
||||||
bool audio_existing_ = false;
|
|
||||||
bool note_position(const xtc_note_info ¬e, int &px) const;
|
|
||||||
unsigned int note_value(int py) const;
|
|
||||||
float audio_value(int py) const;
|
|
||||||
void draw_audio(const xtc_track_info &track, int left, int right);
|
|
||||||
public:
|
|
||||||
SequencerVelocityWidget(SequencerWidget &owner, uint64_t track);
|
|
||||||
bool audio() const;
|
|
||||||
bool preview_label(char *text, size_t size) const;
|
|
||||||
int handle(int event) override;
|
|
||||||
protected:
|
|
||||||
void draw() override;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#include "sequencer_widget.hpp"
|
|
||||||
#include "ui.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
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<Wave> 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<double>(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<double>(bin * frames / bins) * ticks_per_frame;
|
|
||||||
double end = static_cast<double>((bin+1) * frames / bins) * ticks_per_frame;
|
|
||||||
double offset = static_cast<double>(c.offset_tick);
|
|
||||||
if (end <= offset || start >= offset + static_cast<double>(c.length_tick)) continue;
|
|
||||||
int px = left + static_cast<int>((std::max(start, offset) - offset) * scale_ / 960);
|
|
||||||
int width = std::max(1, static_cast<int>((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<int>(std::clamp(high, -1.0f, 1.0f) * 19);
|
|
||||||
int y2 = top + 28 - static_cast<int>(std::clamp(low, -1.0f, 1.0f) * 19);
|
|
||||||
ui_rectf(px, y1, width, std::max(1, y2-y1+1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 <algorithm>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
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<UiScroll *>(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<UiScroll *>(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<int64_t>(std::max(0,px-x()-header_width_))*960/scale_;
|
|
||||||
return std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(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<tracks;++i) {
|
|
||||||
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
|
|
||||||
if(t.id==track) found=true;
|
|
||||||
}
|
|
||||||
if(!found) return false;
|
|
||||||
if(track_expanded(track)==expanded) return true;
|
|
||||||
for(auto &id:expanded_tracks_) if(id==(expanded ? UINT64_MAX : track)) {
|
|
||||||
id=expanded ? track : UINT64_MAX;
|
|
||||||
mode_=0; extent(); redraw(); return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SequencerWidget::track_height(size_t row) const
|
|
||||||
{
|
|
||||||
xtc_track_info t{};
|
|
||||||
return 56+(xtc_project_get_track(project_,row,&t)==XTC_OK && track_expanded(t.id) ? velocity_height_ : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int SequencerWidget::track_top(size_t row) const
|
|
||||||
{
|
|
||||||
int top=y()+28;
|
|
||||||
for(size_t i=0;i<row;++i) top+=track_height(i);
|
|
||||||
return top;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SequencerWidget::track_row(int py) const
|
|
||||||
{
|
|
||||||
size_t tracks=0, clips=0; xtc_project_get_counts(project_,&tracks,&clips);
|
|
||||||
int top=y()+28;
|
|
||||||
for(size_t i=0;i<tracks;++i) {
|
|
||||||
int bottom=top+track_height(i);
|
|
||||||
if(py>=top && py<bottom) return static_cast<int>(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<tracks;++i) {
|
|
||||||
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
|
|
||||||
if(t.id==id) found=true;
|
|
||||||
}
|
|
||||||
if(!found) id=UINT64_MAX;
|
|
||||||
}
|
|
||||||
uint64_t end = 32U * 3840U;
|
|
||||||
for (size_t i = 0; i < clips; ++i) {
|
|
||||||
xtc_clip_info c{};
|
|
||||||
xtc_project_get_clip(project_, i, &c);
|
|
||||||
end = std::max(end, c.start_tick + c.length_tick + 3840U);
|
|
||||||
}
|
|
||||||
size_t markers=0;
|
|
||||||
if(xtc_project_marker_count(project_,&markers)==XTC_OK && markers) {
|
|
||||||
xtc_marker marker{};
|
|
||||||
if(xtc_project_get_marker(project_,markers-1,&marker)==XTC_OK)
|
|
||||||
end=std::max(end,std::min(XTC_PROJECT_MAX_TICK,marker.tick)+UINT64_C(3840));
|
|
||||||
}
|
|
||||||
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + static_cast<unsigned int>(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<unsigned int>(scale_)/960 < 6) visual *= 2;
|
|
||||||
int spacing = static_cast<int>(visual*static_cast<unsigned int>(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<uint64_t>(step)*visual;
|
|
||||||
int px = x()+header_width_+static_cast<int>(tick*static_cast<unsigned int>(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<markers;++i) {
|
|
||||||
xtc_marker marker{};xtc_project_get_marker(project_,i,&marker);
|
|
||||||
int px=x()+header_width_+static_cast<int>(marker.tick*static_cast<unsigned int>(scale_)/960U);
|
|
||||||
if(px<std::max(cx,header_left()+header_width_) || px>cx+cw)continue;
|
|
||||||
ui_color(xtc_ui::accent());ui_line(px,y()+24,px,y()+h());
|
|
||||||
}
|
|
||||||
int playhead = x()+header_width_+static_cast<int>(cursor_*static_cast<unsigned int>(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()<header_left()+header_width_) {
|
|
||||||
mode_=5;header_drag_x_=Ui::event_x();header_drag_original_=header_width_;return 1;
|
|
||||||
}
|
|
||||||
if(mode_==5 && (event==UI_DRAG || event==UI_RELEASE)) {
|
|
||||||
header_width(header_drag_original_+Ui::event_x()-header_drag_x_);if(event==UI_RELEASE)mode_=0;return 1;
|
|
||||||
}
|
|
||||||
if(mode_==5 && event==UI_KEYDOWN && Ui::event_key()==UI_Escape) {header_width(header_drag_original_);mode_=0;return 1;}
|
|
||||||
for(auto *track:track_widgets_) {
|
|
||||||
auto *choice=track->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()<y()+28) return ruler_->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<int64_t>(Ui::event_x() - drag_x_) * 960 / scale_;
|
|
||||||
int64_t start = static_cast<int64_t>(original_.start_tick);
|
|
||||||
int64_t length = static_cast<int64_t>(original_.length_tick);
|
|
||||||
int64_t offset = static_cast<int64_t>(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<size_t>(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<uint64_t>(start),
|
|
||||||
static_cast<uint64_t>(length), static_cast<uint64_t>(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;i<tracks;++i) {
|
|
||||||
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
|
|
||||||
if(t.id==(*it)->track_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;i<tracks;++i) {
|
|
||||||
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
|
|
||||||
auto *view=track_widget(t.id);
|
|
||||||
if(!view) { view=new SequencerTrackWidget(*this,t); track_widgets_.push_back(view); }
|
|
||||||
view->sync(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.
|
|
||||||
}
|
|
||||||
|
|
@ -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 <array>
|
|
||||||
#include "xtc/xtc_audio.h"
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
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<uint64_t> 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<int> &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<uint64_t,XTC_PROJECT_MAX_TRACKS> 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<SequencerTrackWidget *> track_widgets_; // JUCE owns children.
|
|
||||||
SequencerRulerWidget *ruler_ = nullptr;
|
|
||||||
SequencerVelocityWidget *active_velocity_ = nullptr; // Captured gesture, not ownership.
|
|
||||||
void sync_views();
|
|
||||||
void context_menu();
|
|
||||||
std::vector<xtc_plugin_spec> catalog_;
|
|
||||||
static void instrument_callback(UiWidget *, void *);
|
|
||||||
struct Wave {
|
|
||||||
std::string path;
|
|
||||||
std::unique_ptr<xtc_waveform, decltype(&xtc_waveform_destroy)> data{nullptr, xtc_waveform_destroy};
|
|
||||||
};
|
|
||||||
std::vector<Wave> waves_;
|
|
||||||
std::vector<uint64_t> selection_;
|
|
||||||
std::unique_ptr<xtc_clipboard,decltype(&xtc_clipboard_destroy)> 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
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#ifndef XTC_JUCE_GUI_SNAP_HPP
|
|
||||||
#define XTC_JUCE_GUI_SNAP_HPP
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstddef>
|
|
||||||
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<int>(i);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
inline int64_t snap_position(int64_t ticks, uint64_t grid) {
|
|
||||||
if (ticks <= 0) return 0;
|
|
||||||
int64_t step = static_cast<int64_t>(grid);
|
|
||||||
return (ticks+step/2)/step*step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
#ifndef XTC_JUCE_GUI_THEME_HPP
|
|
||||||
#define XTC_JUCE_GUI_THEME_HPP
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include "ui.hpp"
|
|
||||||
|
|
||||||
/* Shared flat palette. No gradients, shadows, textures or external assets. */
|
|
||||||
namespace xtc_ui {
|
|
||||||
constexpr UiBoxtype flat = static_cast<UiBoxtype>(UI_FREE_BOXTYPE);
|
|
||||||
constexpr UiBoxtype pressed = static_cast<UiBoxtype>(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<UiButton *>(widget)) {
|
|
||||||
button->box(flat); button->down_box(pressed);
|
|
||||||
button->color(ui_rgb_color(45,51,59));
|
|
||||||
}
|
|
||||||
if (auto *group = dynamic_cast<UiGroup *>(widget)) {
|
|
||||||
for (int i = 0; i < group->children(); ++i) style(group->child(i));
|
|
||||||
if (auto *scroll = dynamic_cast<UiScroll *>(group)) {
|
|
||||||
scroll->scrollbar.box(flat); scroll->hscrollbar.box(flat);
|
|
||||||
scroll->scrollbar.slider(UI_FLAT_BOX); scroll->hscrollbar.slider(UI_FLAT_BOX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
@ -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 <cstring>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
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<uint64_t> ids_;
|
|
||||||
std::vector<std::string> 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<TrackInsertsPanel *>(c);int i=p->tracks_->value();if(i>=0 && static_cast<size_t>(i)<p->ids_.size()){p->track_=p->ids_[static_cast<size_t>(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<TrackInsertsPanel *>(c)->add(XTC_BUILTIN_PATH,2);},this);
|
|
||||||
add->add("Internal Reverb",0,[](UiWidget *,void *c){static_cast<TrackInsertsPanel *>(c)->add(XTC_BUILTIN_PATH,3);},this);
|
|
||||||
add->add("Plugin library...",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(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<TrackInsertsPanel *>(c);auto *menu=static_cast<UiMenu_Button *>(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<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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<TrackInsertsPanel *>(c)->preset(true);},this);
|
|
||||||
presets->add("Load chain...",0,[](UiWidget *,void *c){static_cast<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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<TrackInsertsPanel *>(c);p->result(p->bulk(XTC_CHAIN_ENABLE_ALL));},this);
|
|
||||||
actions_->add("Bypass all effects",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->bulk(XTC_CHAIN_BYPASS_ALL));},this);
|
|
||||||
actions_->add("Clear chain",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->bulk(XTC_CHAIN_CLEAR));},this);
|
|
||||||
actions_->add("Compare/Capture A",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->comparison(0,true));},this);
|
|
||||||
actions_->add("Compare/Recall A",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->comparison(0,false));},this,UI_MENU_INACTIVE);
|
|
||||||
actions_->add("Compare/Capture B",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->comparison(1,true));},this);
|
|
||||||
actions_->add("Compare/Recall B",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->comparison(1,false));},this,UI_MENU_INACTIVE);
|
|
||||||
actions_->add("Copy chain",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->copy_chain());},this);
|
|
||||||
actions_->add("Paste chain (replace)",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->paste_chain());},this,UI_MENU_INACTIVE);
|
|
||||||
actions_->add("Duplicate effect",0,[](UiWidget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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<size_t>(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<int>(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(index<chain.count){wet_->activate();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<chain.count)chain.slots[index].wet=static_cast<float>(wet);
|
|
||||||
chain.output_gain=static_cast<float>(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;i<chain.count;++i)chain.slots[i-1]=chain.slots[i];chain.slots[--chain.count]={};}
|
|
||||||
else return XTC_ERR_INVALID_ARGUMENT;
|
|
||||||
r=owner_->set_track_chain(track_,chain);
|
|
||||||
if(r==XTC_OK && chain.count)slots_->select(static_cast<int>(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<uint64_t> ids;std::vector<std::string> names;
|
|
||||||
for(size_t i=0;i<count;++i){xtc_track_info t{};xtc_project_get_track(project,i,&t);if(!t.is_bus){ids.push_back(t.id);names.emplace_back(t.name);}}
|
|
||||||
bool rebuild=ids!=ids_ || names!=names_ || force;
|
|
||||||
if(rebuild){ids_=ids;names_=names;tracks_->clear();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;i<ids_.size();++i)if(ids_[i]==track_)tracks_->value(static_cast<int>(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;i<chain.count;++i){std::string label=std::to_string(i+1)+" "+chain.slots[i].plugin.label+(chain.slots[i].enabled ? "" : " [bypass]");slots_->add(label.c_str());}
|
|
||||||
if(chain.count)slots_->select(selected>0 && static_cast<size_t>(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<size_t,256> rows{};
|
|
||||||
xtc_graph_get_counts(model_,&nodes,&edges);
|
|
||||||
for(size_t i=0;i<nodes;++i){xtc_node_info node{};xtc_routing_info route{};xtc_graph_get_node(model_,i,&node);if(graph_->routing(node.id,route))max_level=std::max(max_level,route.level);}
|
|
||||||
graph_->size(std::max(scroll_->w()-20,static_cast<int>(max_level+1)*174+40),4000);
|
|
||||||
for(size_t i=0;i<nodes;++i){xtc_node_info node{};xtc_routing_info route{};xtc_graph_get_node(model_,i,&node);if(graph_->routing(node.id,route) && route.level<rows.size()) {size_t row=rows[route.level]++;max_row=std::max(max_row,row);graph_->move_node(node.id,20+static_cast<int>(route.level)*174,24+static_cast<int>(row)*104);}}
|
|
||||||
graph_->size(graph_->w(),std::max(scroll_->h()-20,static_cast<int>(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
|
|
||||||
1234
src/gui/JUCE/ui.cpp
1234
src/gui/JUCE/ui.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -1,647 +0,0 @@
|
||||||
#ifndef XTC_JUCE_UI_HPP
|
|
||||||
#define XTC_JUCE_UI_HPP
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdarg>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
#include <functional>
|
|
||||||
#include <juce_gui_extra/juce_gui_extra.h>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
// 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> 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<UiWidget *> 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<int>(children_.size()); }
|
|
||||||
UiWidget *child(int i) const { return children_.at(static_cast<size_t>(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<float>(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<UiMenu_Item> items_{UiMenu_Item{}};
|
|
||||||
std::vector<std::string> 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<int>(p - items_.data()) : -1;
|
|
||||||
}
|
|
||||||
void mode(int i, int flags)
|
|
||||||
{
|
|
||||||
if (i >= 0 && i < static_cast<int>(labels_.size()))
|
|
||||||
items_[static_cast<size_t>(i)].flags = flags;
|
|
||||||
}
|
|
||||||
int size() const { return static_cast<int>(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<int>(items_.size())
|
|
||||||
? items_[static_cast<size_t>(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<std::string> rows_;
|
|
||||||
int getNumRows() override { return static_cast<int>(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<int>(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
|
|
||||||
|
|
@ -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 <algorithm>
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
/* 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<unsigned int>(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<int>(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
|
|
||||||
|
|
@ -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
|
connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include
|
||||||
`playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent
|
`playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent
|
||||||
input/output/both, returning devices and genuine backend failures.
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -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 <atomic>
|
|
||||||
#include <cerrno>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <jack/jack.h>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#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<bool> heard[2]{};
|
|
||||||
std::atomic<bool> invalid{false};
|
|
||||||
~Probe()
|
|
||||||
{
|
|
||||||
if (client)
|
|
||||||
jack_client_close(client);
|
|
||||||
}
|
|
||||||
static int process(jack_nframes_t frames, void *data)
|
|
||||||
{
|
|
||||||
auto &probe = *static_cast<Probe *>(data);
|
|
||||||
for (int channel = 0; channel < 2; ++channel)
|
|
||||||
{
|
|
||||||
auto *samples =
|
|
||||||
static_cast<const float *>(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<UiGroup *>(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<SequencerWidget *>(group.child(i)))
|
|
||||||
return s;
|
|
||||||
if (auto *nested = dynamic_cast<UiGroup *>(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<float> 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<juce::Button *>(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<long>(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<juce::Component *>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,13 +11,12 @@ parser = argparse.ArgumentParser(description=__doc__)
|
||||||
arguments(parser)
|
arguments(parser)
|
||||||
parser.add_argument('--build-dir', type=Path, required=True)
|
parser.add_argument('--build-dir', type=Path, required=True)
|
||||||
parser.add_argument('--gui', action='store_true')
|
parser.add_argument('--gui', action='store_true')
|
||||||
parser.add_argument('--juce-gui', action='store_true')
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
source = Path(__file__).resolve().parent.parent
|
source = Path(__file__).resolve().parent.parent
|
||||||
entries = []
|
entries = []
|
||||||
for destination, original in inventory(source, layout(args)):
|
for destination, original in inventory(source, layout(args)):
|
||||||
name = destination.name
|
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
|
continue
|
||||||
if original is None:
|
if original is None:
|
||||||
original = args.build_dir / name
|
original = args.build_dir / name
|
||||||
|
|
@ -33,7 +32,7 @@ for destination, original in entries:
|
||||||
try:
|
try:
|
||||||
with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file:
|
with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file:
|
||||||
shutil.copyfileobj(input_file, output)
|
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)
|
os.replace(pending, destination)
|
||||||
finally:
|
finally:
|
||||||
Path(pending).unlink(missing_ok=True)
|
Path(pending).unlink(missing_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ def directory(value):
|
||||||
def inventory(source, paths):
|
def inventory(source, paths):
|
||||||
entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None),
|
entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None),
|
||||||
('bindir', 'xtc-check', None), ('bindir', 'xtc', None),
|
('bindir', 'xtc-check', None), ('bindir', 'xtc', None),
|
||||||
('bindir', 'xtc-juce', None),
|
|
||||||
('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'),
|
('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'),
|
||||||
('licensedir', 'LICENSE', source / 'LICENSE'),
|
('licensedir', 'LICENSE', source / 'LICENSE'),
|
||||||
('licensedir', 'CLAP-LICENSE', source / 'third_party/clap/LICENSE'),
|
('licensedir', 'CLAP-LICENSE', source / 'third_party/clap/LICENSE'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue