Revert "Added an alternative GUI based on JUCE; I like FLTK more..."

This reverts commit cd2f87a8b3.
This commit is contained in:
Johannes Findeisen 2026-11-06 23:22:44 +01:00
commit d6e46f67d7
58 changed files with 10 additions and 11136 deletions

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
# Build products
/build/
/build-juce-gui/
/cmake-build-*/
/CMakeUserPresets.json
/*.o

View file

@ -13,7 +13,6 @@ find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter)
find_package(PkgConfig QUIET)
option(XTC_WITH_JACK "Discover the optional JACK backend" ON)
option(XTC_WITH_LADSPA "Enable LADSPA when its system header is available" ON)
option(XTC_BUILD_JUCE_GUI "Build the optional JUCE workstation frontend" OFF)
option(XTC_BUILD_GUI "Build the FLTK application when FLTK is available" ON)
option(XTC_ENABLE_LIVE_TESTS "Register tests requiring an existing JACK server" OFF)
option(XTC_BUILD_JUCE "Build optional externally supplied JUCE examples" OFF)
@ -123,7 +122,6 @@ foreach(preview quality builtin)
endforeach()
include(cmake/Tests.cmake)
include(cmake/Gui.cmake)
include(cmake/JuceGui.cmake)
include(cmake/Developer.cmake)
include(cmake/Install.cmake)
if(XTC_BUILD_STEINBERG)

View file

@ -57,7 +57,6 @@
- **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph.
- **C core:** project model, graph, DSP and audio engine are implemented in C.
- **Optional JUCE GUI:** build `xtc-juce` with `-DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE`. Keep or disable FLTK independently with `XTC_BUILD_GUI`. See [the JUCE frontend](src/gui/JUCE/README.md) for build/run instructions and SDK licensing.
- **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API. The sequencer coordinates nested ruler, track, header, clip and velocity widgets; see [GUI ownership](docs/ARCHITECTURE.md#sequencer-widget-ownership).
- **Linux first:** keep backend-specific details behind an interface.
- **Small increments:** build and test each milestone before adding features.
@ -88,7 +87,7 @@ The JACK backend has passed software-loopback testing; physical-device playback
## Build
Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The default optional GUI needs C++17 and FLTK; a separately selectable JUCE frontend uses an external JUCE SDK; JACK and LADSPA development files enable their optional integrations.
Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The optional GUI needs C++17 and FLTK; JACK and LADSPA development files enable their optional integrations.
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@ -108,7 +107,7 @@ make clean # clean products; keep the CMake cache
Override `BUILD_DIR`, `BUILD_TYPE` or `JOBS` as needed, for example `make BUILD_DIR=build/release BUILD_TYPE=Release JOBS=8`. Pass additional options with `CMAKE_ARGS` and `CTEST_ARGS`. CMake remains the authoritative build system.
This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration.
This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration.
## Run

View file

@ -1,6 +1,5 @@
# Retain the audited owned-file inventory and symlink-safe staging implementation.
set(XTC_INSTALL_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "Directory containing built install inputs")
option(XTC_INSTALL_JUCE_GUI "Include the optional JUCE workstation in installation" ${XTC_BUILD_JUCE_GUI})
option(XTC_INSTALL_GUI "Include the GUI in installation" ${XTC_GUI_AVAILABLE})
set(XTC_INSTALL_LICENSEDIR "${CMAKE_INSTALL_DATAROOTDIR}/licenses/xtc" CACHE STRING "License install directory")
set(XTC_INSTALL_EXAMPLESDIR "${CMAKE_INSTALL_DATAROOTDIR}/xtc/examples" CACHE STRING "Example install directory")

View file

@ -22,9 +22,6 @@ else()
if("@XTC_INSTALL_GUI@")
list(APPEND extra --gui)
endif()
if("@XTC_INSTALL_JUCE_GUI@")
list(APPEND extra --juce-gui)
endif()
set(jack)
if("@JACK_FOUND@" AND "@XTC_WITH_JACK@")
set(jack jack)

View file

@ -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")

View file

@ -11,8 +11,7 @@ the graph editor is an advanced view of the same project.
## Layers
1. **C++ GUI:** input, drawing, windows, view state. FLTK is the default;
`src/gui/JUCE` is an independently optional JUCE frontend using the same C API.
1. **FLTK/C++ GUI:** input, drawing, windows, view state.
2. **C core:** project model, commands, parameters, persistence, validation.
3. **C engine:** prepared processing graph, DSP, transport timing, event
delivery.
@ -219,16 +218,3 @@ files; unsupported native saves preserve files. See NATIVE_API.md and decision
0008. Release tooling lives outside the runtime, with source manifests,
deterministic exports and staged installation/consumer checks. See RELEASING.md
for reproducibility scope.
## JUCE frontend ownership
The optional `xtc-juce` application ports the complete retained GUI command
structure under `src/gui/JUCE`, without linking FLTK. `ui.*` supplies JUCE
components, native controls, graphics, event dispatch, timers, scrolling and
dialogs while preserving the original C-facing command boundaries. The master
sequencer owns JUCE ruler/track groups; tracks own header, instrument, clip and
velocity components. Group ownership, borrowed indexes, immutable copied C
metadata and deferred deletion follow the same rules as the FLTK frontend.
JUCE renders overlays after child components and routes callbacks on the message
thread. The audio backend and callbacks remain in C. See the
[JUCE source map](../src/gui/JUCE/README.md#components-and-maintenance).

View file

@ -79,7 +79,6 @@ lists, dependencies, SDK and packaging rules in CMake.
| Cache option | Default | Meaning |
| --- | --- | --- |
| `BUILD_TESTING` | `ON` | Build/register the test suite |
| `XTC_BUILD_JUCE_GUI` | `OFF` | Build the JUCE workstation frontend (`xtc-juce`), independently of FLTK; requires `XTC_JUCE_ROOT` |
| `XTC_BUILD_GUI` | `ON` | Discover FLTK and build the application when available |
| `XTC_WITH_JACK` | `ON` | Discover/build the JACK/PipeWire backend |
| `XTC_WITH_LADSPA` | `ON` | Enable LADSPA when the system header exists |
@ -192,21 +191,3 @@ Clean products with `cmake --build build --target clean`; to discard the cache,
remove only the selected build directory. Never configure in the source tree.
Historical validation entries in ROADMAP describe the build used at that time;
Phase 108 records the CMake migration and its verified results.
## Optional JUCE workstation frontend
See [src/gui/JUCE/README.md](../src/gui/JUCE/README.md) for the component map and
licence requirements. The SDK is supplied by the user; no download occurs.
```sh
cmake -S . -B build/juce-gui -DCMAKE_BUILD_TYPE=Debug \
-DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE
cmake --build build/juce-gui --parallel 4 --target xtc-juce test_juce_gui
./build/juce-gui/xtc-juce examples/projects/playback-test.xtc
ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1
```
`XTC_BUILD_GUI=ON` builds both frontends. `XTC_BUILD_JUCE` continues to mean
JUCE plugin/processor examples and can be enabled independently or alongside
the workstation frontend. A missing SDK is a configuration error only when a
JUCE option is enabled. The existing default FLTK build is unchanged.

View file

@ -1,10 +1,6 @@
# GUI Guidelines
The default frontend uses FLTK/C++ for windows, menus, dialogs, and the event loop.
An optional JUCE port is available under `src/gui/JUCE`; build `xtc-juce` with
`XTC_BUILD_JUCE_GUI=ON` and an external `XTC_JUCE_ROOT`. Both frontends can be
built together and use the same project files and C API. See
[JUCE build and component details](../src/gui/JUCE/README.md). Keep all audio
Use FLTK/C++ for windows, menus, dialogs, and the event loop. Keep all audio
engine and project-domain logic in C.
Main views:

View file

@ -1,8 +1,7 @@
# JUCE integration
# JUCE integration (Phase 107)
JUCE supplies an [optional workstation frontend](../src/gui/JUCE/README.md)
(`XTC_BUILD_JUCE_GUI`) as well as runnable plugin examples (`XTC_BUILD_JUCE`)
alongside xtc's C99 engine and FLTK application. The two options are independent. It is an external SDK dependency, not vendored, fetched, linked by
JUCE is used by optional, runnable examples alongside xtc's C99 engine and FLTK
application. It is an external SDK dependency, not vendored, fetched, linked by
normal builds or automatically installed. The integration uses:
| JUCE capability | Implemented use |

View file

@ -79,11 +79,3 @@ repository code; no Steinberg VST2 SDK or third-party plugin is bundled. Hosting
an installed binary is distinct from SDK, trademark and plugin redistribution
rights. Review those separately for any proposed distribution. Source archives
retain upstream notices; binary installs also ship both header licenses.
## Optional JUCE workstation frontend
`XTC_BUILD_JUCE_GUI` links the user-supplied JUCE SDK under its AGPLv3 or
commercial licence. It is off by default and separate from the MIT C core and
FLTK frontend. No SDK source is copied into this repository or downloaded by
normal configuration. Review the supplied SDK's licence and its graphics/text
dependencies when distributing `xtc-juce`. See [the frontend guide](../src/gui/JUCE/README.md).

View file

@ -3739,47 +3739,6 @@ Play/Stop/Space/stereo check also passed. See the integration-test guide for com
---
## Phase 113 — Optional JUCE workstation frontend
### Tasks and acceptance criteria
- [x] Preserve the FLTK sources and default build; add an independently optional
`XTC_BUILD_JUCE_GUI` workstation under `src/gui/JUCE`.
- [x] Require a user-supplied SDK through `XTC_JUCE_ROOT`, with an explicit error
when enabled without the SDK and no SDK discovery/download in normal builds.
- [x] Port the main window, sequencer/master and child views, graph, mixer,
automation, piano roll, velocity editing, sample editor, plugin dialogs,
insert chains, preferences, searchable commands and support widgets.
- [x] Use JUCE components, controls, graphics, native events, dialogs and timers;
keep project/history/DSP/backend/plugin APIs in C and preserve `.xtc` files.
- [x] Allow JUCE-only, FLTK-only, both and core-only configurations; share the
externally supplied SDK with the independently optional plugin examples.
- [x] Include the JUCE binary in the owned install/uninstall inventory when selected.
- [x] Pass the retained GUI regression workflows against JUCE, including failure
rollback, child ownership, gestures, preferences and MIDI device fallback.
- [x] Verify native JUCE transport events and finite stereo playback with the
bundled test song and missing saved MIDI devices.
- [x] Validate the original FLTK build/tests and document final build results.
### Implementation notes
The frontend retains the original C-facing command structure and filenames.
`ui.*` maps the retained coordinates and callback conventions to JUCE, with no
FLTK headers or runtime dependency. Native controls may look different. Both
frontends continue to use the existing JACK/PipeWire backend. See the
[frontend source and build guide](../src/gui/JUCE/README.md).
JUCE is an optional external AGPLv3/commercial SDK; normal builds do not link it.
Qualification is for Linux, rather than a new cross-platform audio backend.
The original 57 FLTK/core tests, core-only build, missing-SDK diagnostic, shared
SDK configuration and staged install/uninstall checks passed. The frontend was
built with warnings enabled against external JUCE 8.0.15. Native Play/Stop/Space,
expanded track rendering and finite stereo playback passed with missing saved
MIDI devices. All 12 JUCE GUI workflows passed, with focused reruns after the
native focus/blur adjustments. The [workstation capture](../screenshots/juce-workstation.png)
comes from this real-backend regression, with no physical routes changed.
---
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a

View file

@ -11,9 +11,7 @@ endif()
message(STATUS "JUCE is AGPLv3/commercial: review the external SDK LICENSE.md before distributing these binaries")
set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE)
if(NOT TARGET juce::juce_audio_utils)
add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL)
endif()
add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL)
get_filename_component(XTC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
if(NOT TARGET xtc-core)
set(XTC_BUILD_GUI OFF CACHE BOOL "" FORCE)

View file

@ -5,15 +5,6 @@ See [capture notes and reproduction instructions](README.md) for image provenanc
---
## The optional JUCE workstation
![JUCE workstation playing the bundled test project](juce-workstation.png)
The JUCE frontend plays the bundled test song with an expanded velocity lane;
missing saved MIDI devices appear as a warning while stereo playback continues.
---
## The xtc workspace at a glance
![The xtc workspace at a glance](overview.png)

View file

@ -32,23 +32,6 @@ APIs and GUI history/preparation paths. Original projects, media and user
configuration are not overwritten. Black margins are the virtual desktop;
floating views show the actual detached windows together.
## Optional JUCE workstation (Phase 113)
![JUCE workstation playing the bundled test project](juce-workstation.png)
Rendered directly from the actual JUCE component hierarchy, with the first
track expanded and playback running. The test deliberately configures missing
MIDI devices and captures finite stereo output through private JACK probe ports.
It preserves user preferences and physical routes. A running JACK-compatible
server is required. After building the optional frontend and tests:
```sh
XTC_JUCE_SCREENSHOT="$PWD/screenshots/juce-workstation.png" \
xvfb-run -a -s '-screen 0 1440x960x24' ./build/juce-gui/test_juce_playback
```
See the [JUCE frontend guide](../src/gui/JUCE/README.md) for SDK and build options.
## Continuous audio velocity editing (Phase 110)
![Audio velocity envelope beneath a waveform track](audio-velocity-expanded.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View file

@ -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).

View file

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

View file

@ -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);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
}

View file

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

View file

@ -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,&note) == 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,&note);
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;
}

View file

@ -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 &note) 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

View file

@ -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;
}

View file

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

View file

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

View file

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

View file

@ -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;}

View file

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

View file

@ -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_,&notes);
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,&note);
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,&note);
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;
}

View file

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

View file

@ -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(),&copy);
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);
}

View file

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

View file

@ -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,&current) == 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;
}
}

View file

@ -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;
}

View file

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

View file

@ -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);
}

View file

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

View file

@ -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 &note, 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_,&notes);
for(size_t n=0;!is_audio && n<notes;++n) {
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,&note); 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_,&notes);
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,&note); 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;
}

View file

@ -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 &note, 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

View file

@ -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));
}
}

View file

@ -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.
}

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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,&note);
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,&note);
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

View file

@ -254,29 +254,3 @@ It requires an existing JACK/PipeWire server, changes no user preferences and
connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include
`playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent
input/output/both, returning devices and genuine backend failures.
The optional JUCE workstation builds the shared GUI regression source against
`src/gui/JUCE` as `test_juce_gui`; `juce-gui-*` CTests cover the same retained
commands, fault injection, dialogs, hierarchy and gestures without FLTK. Build
all test fixtures before running them. The real-backend `test_juce_playback`
delivers JUCE MouseEvents to native transport buttons, resumes with a JUCE
KeyPress, and checks finite nonzero stereo samples through a private JACK probe
with missing saved MIDI devices. It writes no user preferences and connects no
physical ports. Run it from the repository root under Xvfb after building:
```sh
cmake --build build/juce-gui --parallel 4
ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1
xvfb-run -a ./build/juce-gui/test_juce_playback
```
`XTC_ENABLE_LIVE_TESTS` registers the latter as `juce-playback`. An optional
`XTC_JUCE_SCREENSHOT` absolute output path captures the actual workstation
component during that live regression.
Linux qualification used JUCE 8.0.15: all 12 JUCE GUI workflows passed (with
focused reruns after native focus/blur fixes), alongside all 57 existing
FLTK/core tests. Native Play/Stop/Space, expanded-track rendering and finite
stereo output passed with deliberately unavailable saved MIDI devices. Bare
Xvfb can emit JUCE X11 BadAtom diagnostics without a window manager; these do
not indicate a failed assertion in the workflow checks.

View file

@ -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;
}
}

View file

@ -11,13 +11,12 @@ parser = argparse.ArgumentParser(description=__doc__)
arguments(parser)
parser.add_argument('--build-dir', type=Path, required=True)
parser.add_argument('--gui', action='store_true')
parser.add_argument('--juce-gui', action='store_true')
args = parser.parse_args()
source = Path(__file__).resolve().parent.parent
entries = []
for destination, original in inventory(source, layout(args)):
name = destination.name
if (name == 'xtc-juce' and not args.juce_gui) or name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')):
if name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')):
continue
if original is None:
original = args.build_dir / name
@ -33,7 +32,7 @@ for destination, original in entries:
try:
with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file:
shutil.copyfileobj(input_file, output)
os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check', 'xtc-juce') else 0o644)
os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check') else 0o644)
os.replace(pending, destination)
finally:
Path(pending).unlink(missing_ok=True)

View file

@ -30,7 +30,6 @@ def directory(value):
def inventory(source, paths):
entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None),
('bindir', 'xtc-check', None), ('bindir', 'xtc', None),
('bindir', 'xtc-juce', None),
('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'),
('licensedir', 'LICENSE', source / 'LICENSE'),
('licensedir', 'CLAP-LICENSE', source / 'third_party/clap/LICENSE'),