Connect FLTK shell to JACK engine controls and status
This commit is contained in:
parent
577d80f018
commit
2ecc03b482
14 changed files with 691 additions and 45 deletions
28
Makefile
28
Makefile
|
|
@ -34,7 +34,7 @@ FLTK_LIBS := $(shell $(FLTK_CONFIG) --ldflags)
|
||||||
GUI_AVAILABLE := yes
|
GUI_AVAILABLE := yes
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all clean test test-jack debug gui install
|
.PHONY: all clean test test-jack test-gui test-gui-jack debug gui install
|
||||||
all: $(BUILD_DIR)/libxtc.a $(BUILD_DIR)/xtc-check
|
all: $(BUILD_DIR)/libxtc.a $(BUILD_DIR)/xtc-check
|
||||||
|
|
||||||
$(BUILD_DIR)/xtc-check: $(BUILD_DIR)/src/main.o $(BUILD_DIR)/libxtc.a
|
$(BUILD_DIR)/xtc-check: $(BUILD_DIR)/src/main.o $(BUILD_DIR)/libxtc.a
|
||||||
|
|
@ -94,9 +94,31 @@ endif
|
||||||
ifeq ($(GUI_AVAILABLE),yes)
|
ifeq ($(GUI_AVAILABLE),yes)
|
||||||
gui: $(BUILD_DIR)/xtc
|
gui: $(BUILD_DIR)/xtc
|
||||||
|
|
||||||
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/libxtc.a
|
$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/libxtc.a
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm
|
||||||
|
|
||||||
|
test-gui: $(BUILD_DIR)/test_gui
|
||||||
|
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||||
|
|
||||||
|
ifeq ($(JACK_AVAILABLE),yes)
|
||||||
|
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/libxtc.a
|
||||||
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
|
||||||
|
|
||||||
|
test-gui-jack: $(BUILD_DIR)/test_gui_jack
|
||||||
|
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui_jack
|
||||||
else
|
else
|
||||||
|
test-gui-jack:
|
||||||
|
@echo "JACK development files are required for the live GUI test."
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/libxtc.a
|
||||||
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
|
||||||
|
else
|
||||||
|
test-gui-jack test-gui:
|
||||||
|
@echo "FLTK development files are required for GUI tests."
|
||||||
|
@exit 1
|
||||||
|
|
||||||
gui:
|
gui:
|
||||||
@echo "FLTK was not found by pkg-config or fltk-config. GUI requires FLTK development files."
|
@echo "FLTK was not found by pkg-config or fltk-config. GUI requires FLTK development files."
|
||||||
@exit 1
|
@exit 1
|
||||||
|
|
@ -111,4 +133,4 @@ clean:
|
||||||
rm -rf "$(BUILD_DIR)"
|
rm -rf "$(BUILD_DIR)"
|
||||||
|
|
||||||
-include $(CORE_OBJECTS:.o=.d) $(TEST_OBJECT:.o=.d) $(BUILD_DIR)/src/main.d $(BUILD_DIR)/tests/unit/test_offline.d $(BUILD_DIR)/tests/unit/test_realtime.d $(BUILD_DIR)/tests/unit/test_backend.d \
|
-include $(CORE_OBJECTS:.o=.d) $(TEST_OBJECT:.o=.d) $(BUILD_DIR)/src/main.d $(BUILD_DIR)/tests/unit/test_offline.d $(BUILD_DIR)/tests/unit/test_realtime.d $(BUILD_DIR)/tests/unit/test_backend.d \
|
||||||
$(BUILD_DIR)/src/gui/main.d $(BUILD_DIR)/src/gui/main_window.d $(BUILD_DIR)/tests/integration/test_jack.d
|
$(BUILD_DIR)/src/gui/main.d $(BUILD_DIR)/src/gui/main_window.d $(BUILD_DIR)/tests/integration/test_jack.d $(BUILD_DIR)/tests/integration/test_gui.d $(BUILD_DIR)/tests/integration/test_gui_jack.d
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This repository is a starter scaffold, not a production DAW. It currently provid
|
||||||
- C tests for deterministic rendering, routing, validation and lifecycle;
|
- C tests for deterministic rendering, routing, validation and lifecycle;
|
||||||
- an optional JACK-compatible mono backend with start/stop, routing and status;
|
- an optional JACK-compatible mono backend with start/stop, routing and status;
|
||||||
- safe control/audio plan handoff and concurrent replacement tests;
|
- safe control/audio plan handoff and concurrent replacement tests;
|
||||||
- an optional FLTK GUI shell;
|
- an optional FLTK shell with engine controls, view switching and backend status;
|
||||||
- architecture, format, GUI, roadmap, and decision documents.
|
- architecture, format, GUI, roadmap, and decision documents.
|
||||||
|
|
||||||
The JACK backend has passed software-loopback testing; physical-device playback
|
The JACK backend has passed software-loopback testing; physical-device playback
|
||||||
|
|
@ -51,7 +51,11 @@ Run the GUI:
|
||||||
./build/xtc
|
./build/xtc
|
||||||
```
|
```
|
||||||
|
|
||||||
The current GUI is only a shell and is not connected to the backend yet.
|
The GUI opens JACK and offers Play/Stop/Reconnect, backend status and view
|
||||||
|
switching. Play starts the engine in silence; playback content and editors are
|
||||||
|
not implemented. F5 starts, F8 stops, Space toggles, F6 reconnects, and Ctrl+1/2/3
|
||||||
|
switches views. Ctrl+Q closes the client and window. No physical ports are
|
||||||
|
connected automatically. See [GUI documentation](docs/GUI.md).
|
||||||
With JACK development files and an existing JACK-compatible server, run the
|
With JACK development files and an existing JACK-compatible server, run the
|
||||||
isolated software-loopback test with `make test-jack`. It connects only test
|
isolated software-loopback test with `make test-jack`. It connects only test
|
||||||
clients. See [engine documentation](docs/AUDIO_ENGINE.md) for configuration,
|
clients. See [engine documentation](docs/AUDIO_ENGINE.md) for configuration,
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,12 @@ the optional JACK implementation drives that same realtime engine.
|
||||||
Only acknowledged retired plans are reclaimed during processing. Backend close
|
Only acknowledged retired plans are reclaimed during processing. Backend close
|
||||||
synchronizes callbacks before reclaiming remaining plans. See decision 0004
|
synchronizes callbacks before reclaiming remaining plans. See decision 0004
|
||||||
for the release/acquire protocol, bounded resource retention and recovery.
|
for the release/acquire protocol, bounded resource retention and recovery.
|
||||||
|
|
||||||
|
## GUI backend ownership
|
||||||
|
|
||||||
|
The Phase 3 window owns one C backend handle and serializes all control calls
|
||||||
|
on the GUI thread. A GUI timer reads snapshots and collects retired resources;
|
||||||
|
no audio callback calls FLTK. Successful window close first closes the JACK
|
||||||
|
client (synchronizing callbacks), then removes the GUI timer and hides the
|
||||||
|
window. Failed close leaves both handle and window available for retry. View
|
||||||
|
switching is GUI state only; editors have no domain mutations yet. See GUI.md.
|
||||||
|
|
|
||||||
|
|
@ -131,5 +131,6 @@ internal realtime guarantees depend on the installed implementation/server.
|
||||||
Validation commands and results are recorded in BUILD.md and the integration
|
Validation commands and results are recorded in BUILD.md and the integration
|
||||||
test README. Current limitations: mono input/output, no parameter ramping or
|
test README. Current limitations: mono input/output, no parameter ramping or
|
||||||
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
|
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
|
||||||
no physical-device or long-duration endurance qualification. GUI controls and
|
no physical-device or long-duration endurance qualification. Phase 3 connects
|
||||||
status display are Phase 3; the GUI remains a placeholder shell.
|
the FLTK shell to backend controls and status; see GUI.md. Its editors remain
|
||||||
|
placeholders and the GUI submits no playback graph yet.
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ make gui
|
||||||
`all` builds the static C library and startup check. `test` runs the existing
|
`all` builds the static C library and startup check. `test` runs the existing
|
||||||
core/offline tests, concurrent plan-handoff stress, JACK fault injection
|
core/offline tests, concurrent plan-handoff stress, JACK fault injection
|
||||||
(when JACK headers are available), and the startup check. `debug` builds and tests separately in
|
(when JACK headers are available), and the startup check. `debug` builds and tests separately in
|
||||||
`build/debug`. `gui` builds the existing placeholder shell; it does not produce
|
`build/debug`. `gui` builds the FLTK shell with backend controls and status.
|
||||||
audio. Generated files remain under `build` and header changes trigger rebuilds.
|
The shell starts a silent engine and never connects physical ports. Generated files remain under `build` and header changes trigger rebuilds.
|
||||||
Compiler, flags, `BUILD_DIR`, `PKG_CONFIG`, `FLTK_CONFIG`, and installation
|
Compiler, flags, `BUILD_DIR`, `PKG_CONFIG`, `FLTK_CONFIG`, and installation
|
||||||
`PREFIX` can be overridden through Make variables. C uses C99; GUI uses C++17.
|
`PREFIX` can be overridden through Make variables. C uses C99; GUI uses C++17.
|
||||||
|
|
||||||
|
|
@ -76,8 +76,24 @@ fault-injection tests cover those notification paths. No packages installed
|
||||||
or system/device configuration changed. Sanitizers are not enabled in the live
|
or system/device configuration changed. Sanitizers are not enabled in the live
|
||||||
callback qualification run.
|
callback qualification run.
|
||||||
|
|
||||||
|
## Phase 3 verification
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make clean all test gui debug test-gui test-gui-jack
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-gui` needs Xvfb and `xvfb-run`, uses a fake C backend and requires no JACK
|
||||||
|
server. `test-gui-jack` needs a running server; it creates no physical routes.
|
||||||
|
Both passed, including repeated active-client close and reconnect. The GUI also
|
||||||
|
builds without JACK metadata and reports unsupported backend support at startup.
|
||||||
|
A live keyboard/visual smoke run passed under Xvfb. Clang ASan/UBSan GUI tests
|
||||||
|
passed with a suppression limited to the installed fontconfig library's
|
||||||
|
exit-time font allocations; see tests/integration/README.md for the distinction
|
||||||
|
and command. No compiler warnings remain with GCC or Clang. Core/backend tests
|
||||||
|
pass with unsuppressed ASan/UBSan. No packages or system configuration changed.
|
||||||
|
|
||||||
## Next milestone
|
## Next milestone
|
||||||
|
|
||||||
Phase 3 connects the FLTK shell to the backend C API for engine controls,
|
Phase 4 implements the modular graph editor and submits validated graph edits
|
||||||
status/error reporting and clean shutdown. Physical playback and endurance,
|
through the C API. Physical playback/endurance, project persistence and
|
||||||
project persistence and sequencing remain unverified/unimplemented as applicable.
|
sequencing remain unverified/unimplemented as applicable.
|
||||||
|
|
|
||||||
46
docs/GUI.md
46
docs/GUI.md
|
|
@ -16,3 +16,49 @@ from project mutation. Never call FLTK from the audio thread.
|
||||||
Visual direction: compact, high-contrast, restrained dark interface. Avoid
|
Visual direction: compact, high-contrast, restrained dark interface. Avoid
|
||||||
excessive animation and modal configuration. Recreate broad usability ideas,
|
excessive animation and modal configuration. Recreate broad usability ideas,
|
||||||
not energyXT's exact artwork or proprietary resources.
|
not energyXT's exact artwork or proprietary resources.
|
||||||
|
|
||||||
|
## Phase 3 shell
|
||||||
|
|
||||||
|
The shell opens JACK on startup through `xtc_backend.h`. It never accesses
|
||||||
|
backend structures, processes samples, creates domain objects, or calls JACK
|
||||||
|
itself. All backend control calls run on the GUI thread; FLTK is never called
|
||||||
|
from an audio callback. No server is started and no audio ports are connected
|
||||||
|
by the GUI. With no graph submitted, the running backend produces silence.
|
||||||
|
|
||||||
|
- **Play / F5:** start the engine. **Stop / F8:** deactivate it. **Space:** toggle.
|
||||||
|
These are engine controls, not timeline playback or transport positioning.
|
||||||
|
- **Reconnect / F6:** close the old client and open a fresh stopped client.
|
||||||
|
Use this after server loss or sample-rate changes. A failed close preserves
|
||||||
|
the handle and reports the failure instead of opening another client.
|
||||||
|
- **View menu / Ctrl+1, Ctrl+2, Ctrl+3:** switch between sequencer, mixer and
|
||||||
|
modular graph. Tabs provide the same view switching. Editors remain clearly
|
||||||
|
labeled placeholders; no project editing is implied.
|
||||||
|
- **File / Quit / Ctrl+Q** or the window close button: close the backend and
|
||||||
|
synchronize audio callbacks before hiding/destroying widgets. A failed close
|
||||||
|
leaves the window open for a retry. Unsupported New/Open/Save actions are
|
||||||
|
visibly disabled. About identifies the development milestone and version.
|
||||||
|
|
||||||
|
The indicator shows disconnected, ready, running, server lost, format changed
|
||||||
|
or process-error states. The status line shows rate, actual block size, mono
|
||||||
|
layout, processed block count, xruns and process result. Command errors remain
|
||||||
|
visible until a successful control action; periodic refresh does not erase them.
|
||||||
|
A GUI-owned 100 ms timer reads atomic snapshots and collects retired plans.
|
||||||
|
Label text changes only when needed. The timer is removed before successful
|
||||||
|
close and again by the window destructor. No GUI locks are held across control
|
||||||
|
calls. Operations such as opening JACK can briefly block the GUI; there is no
|
||||||
|
worker/control-thread framework in this initial shell.
|
||||||
|
|
||||||
|
MainWindow owns the backend handle and FLTK owns the window's children. The
|
||||||
|
destructor also attempts cleanup on exceptional event-loop exit; if JACK close
|
||||||
|
itself fails then, it reports to stderr and retains the callback context until
|
||||||
|
process exit. Normal close/reconnect paths permit retry and release ownership
|
||||||
|
only after successful close. The entry point catches C++ exceptions before
|
||||||
|
process exit. Backend operations return C error codes throughout.
|
||||||
|
|
||||||
|
Tests: `make test-gui` runs real widgets/callbacks/timers against a fake C backend
|
||||||
|
under Xvfb, including control errors, startup without JACK, partially initialized
|
||||||
|
handles, server/format notifications, view switching, retrying close, and timer
|
||||||
|
removal. `make test-gui-jack` additionally requires a running JACK-compatible
|
||||||
|
server, tests repeated start/stop, reconnect and active-client close, and creates
|
||||||
|
no routes. Physical playback is outside this shell. See BUILD.md and the
|
||||||
|
integration test README for sanitizer limitations and keyboard smoke results.
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,18 @@ format/xrun/loss status, start/stop/reopen, and bounded pending/retired plan
|
||||||
handoff using lock-free atomics. Hardware-independent failure/concurrency tests
|
handoff using lock-free atomics. Hardware-independent failure/concurrency tests
|
||||||
and a 30-second software-loopback run pass. See decision 0004 and AUDIO_ENGINE.md.
|
and a 30-second software-loopback run pass. See decision 0004 and AUDIO_ENGINE.md.
|
||||||
Physical hardware playback/endurance and live server-loss recovery remain
|
Physical hardware playback/endurance and live server-loss recovery remain
|
||||||
unverified; no GUI engine integration until Phase 3.
|
unverified; GUI engine controls are implemented in Phase 3.
|
||||||
|
|
||||||
## Phase 3 — FLTK shell
|
## Phase 3 — FLTK shell
|
||||||
Main window, transport controls, view switching, backend status, clean startup
|
Main window, transport controls, view switching, backend status, clean startup
|
||||||
and shutdown through the C API.
|
and shutdown through the C API.
|
||||||
|
|
||||||
|
Implemented: dark FLTK shell, Play/Stop/Reconnect, view/menu shortcuts, backend
|
||||||
|
state/format/xrun/process status, persistent command errors, timer-based refresh
|
||||||
|
and retirement collection, and callback-safe close/retry. Mock-backend GUI tests
|
||||||
|
and live GUI/JACK start/stop/reconnect/close tests pass under Xvfb. Editors and
|
||||||
|
project-file actions remain placeholders; engine runs in silence. See GUI.md.
|
||||||
|
|
||||||
## Phase 4 — Modular editor
|
## Phase 4 — Modular editor
|
||||||
Draw nodes/ports/edges; create, move and connect nodes; reject invalid edges;
|
Draw nodes/ports/edges; create, move and connect nodes; reject invalid edges;
|
||||||
safely submit edits to the C core.
|
safely submit edits to the C core.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <exception>
|
||||||
#include "main_window.hpp"
|
#include "main_window.hpp"
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
MainWindow window;
|
try {
|
||||||
window.show(argc, argv);
|
Fl::scheme("flat");
|
||||||
return Fl::run();
|
Fl::background(34, 36, 40);
|
||||||
|
Fl::background2(24, 26, 30);
|
||||||
|
Fl::foreground(230, 232, 236);
|
||||||
|
MainWindow window;
|
||||||
|
window.show(argc, argv);
|
||||||
|
return Fl::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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,240 @@
|
||||||
#include "main_window.hpp"
|
#include "main_window.hpp"
|
||||||
|
#include "xtc/xtc_version.h"
|
||||||
|
|
||||||
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Group.H>
|
||||||
#include <FL/Fl_Menu_Bar.H>
|
#include <FL/Fl_Menu_Bar.H>
|
||||||
#include <FL/Fl_Tabs.H>
|
#include <FL/Fl_Tabs.H>
|
||||||
#include <FL/Fl_Group.H>
|
#include <FL/fl_ask.H>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
extern "C" {
|
namespace {
|
||||||
#include "xtc/xtc.h"
|
void set_label(Fl_Box *box, const char *text)
|
||||||
|
{
|
||||||
|
if (box->label() == nullptr || std::strcmp(box->label(), text) != 0)
|
||||||
|
box->copy_label(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::MainWindow()
|
MainWindow::MainWindow()
|
||||||
: Fl_Double_Window(1000, 680, "xtc — modular audio workstation")
|
: Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation")
|
||||||
{
|
{
|
||||||
begin();
|
begin();
|
||||||
|
auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
|
||||||
|
menu->add("File/New (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
|
||||||
|
menu->add("File/Open (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
|
||||||
|
menu->add("File/Save (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
|
||||||
|
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
|
||||||
|
menu->add("Audio/Start engine", FL_F + 5, play_callback, this);
|
||||||
|
menu->add("Audio/Stop engine", FL_F + 8, stop_callback, this);
|
||||||
|
menu->add("Audio/Reconnect", FL_F + 6, reconnect_callback, this);
|
||||||
|
|
||||||
Fl_Menu_Bar *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
|
play_ = new Fl_Button(12, 40, 80, 30, "Play");
|
||||||
menu->add("File/New", 0, nullptr);
|
play_->callback(play_callback, this);
|
||||||
menu->add("File/Open...", 0, nullptr);
|
play_->tooltip("Start the audio engine (F5 or Space). No sequencer content yet.");
|
||||||
menu->add("File/Save", 0, nullptr);
|
stop_ = new Fl_Button(100, 40, 80, 30, "Stop");
|
||||||
menu->add("Help/About", 0, nullptr);
|
stop_->callback(stop_callback, this);
|
||||||
|
stop_->tooltip("Stop the audio engine (F8 or Space).");
|
||||||
|
auto *reconnect = new Fl_Button(188, 40, 110, 30, "Reconnect");
|
||||||
|
reconnect->callback(reconnect_callback, this);
|
||||||
|
reconnect->tooltip("Close and reopen JACK, leaving the engine stopped (F6).");
|
||||||
|
indicator_ = new Fl_Box(316, 40, 672, 30, "Audio: disconnected");
|
||||||
|
indicator_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||||
|
indicator_->labelfont(FL_BOLD);
|
||||||
|
auto *hint = new Fl_Box(12, 78, 976, 24,
|
||||||
|
"Engine controls only. Playback content and recording are not implemented; no ports are connected automatically.");
|
||||||
|
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||||
|
hint->labelsize(12);
|
||||||
|
|
||||||
Fl_Tabs *tabs = new Fl_Tabs(8, 36, 984, 600);
|
tabs_ = new Fl_Tabs(8, 114, 984, 470);
|
||||||
tabs->begin();
|
tabs_->begin();
|
||||||
|
auto *sequencer = new Fl_Group(8, 140, 984, 444, "Sequencer");
|
||||||
Fl_Group *sequencer = new Fl_Group(8, 60, 984, 576, "Sequencer");
|
|
||||||
sequencer->begin();
|
sequencer->begin();
|
||||||
new Fl_Box(24, 84, 940, 40,
|
new Fl_Box(24, 164, 940, 40, "Sequencer placeholder — timeline and clips are not implemented yet.");
|
||||||
"Sequencer placeholder — timeline and clips are not implemented yet.");
|
|
||||||
sequencer->end();
|
sequencer->end();
|
||||||
|
auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer");
|
||||||
Fl_Group *mixer = new Fl_Group(8, 60, 984, 576, "Mixer");
|
|
||||||
mixer->begin();
|
mixer->begin();
|
||||||
new Fl_Box(24, 84, 940, 40,
|
new Fl_Box(24, 164, 940, 40, "Mixer placeholder — channels and meters are not implemented yet.");
|
||||||
"Mixer placeholder — channels and meters are not implemented yet.");
|
|
||||||
mixer->end();
|
mixer->end();
|
||||||
|
auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph");
|
||||||
Fl_Group *graph = new Fl_Group(8, 60, 984, 576, "Modular Graph");
|
|
||||||
graph->begin();
|
graph->begin();
|
||||||
new Fl_Box(24, 84, 940, 40,
|
new Fl_Box(24, 164, 940, 40, "Modular graph placeholder — visual graph editing is not implemented yet.");
|
||||||
"Modular graph placeholder — the C graph API is the current prototype.");
|
|
||||||
graph->end();
|
graph->end();
|
||||||
|
tabs_->end();
|
||||||
|
tabs_->value(sequencer);
|
||||||
|
menu->add("View/Sequencer", FL_CTRL + '1', view_callback, sequencer);
|
||||||
|
menu->add("View/Mixer", FL_CTRL + '2', view_callback, mixer);
|
||||||
|
menu->add("View/Modular Graph", FL_CTRL + '3', view_callback, graph);
|
||||||
|
menu->add("Help/About", 0, about_callback);
|
||||||
|
|
||||||
tabs->end();
|
status_ = new Fl_Box(12, 594, 976, 26, "JACK: not connected");
|
||||||
|
status_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||||
|
status_->labelsize(12);
|
||||||
|
error_ = new Fl_Box(12, 624, 976, 44, "");
|
||||||
|
error_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
|
||||||
|
error_->labelcolor(fl_rgb_color(255, 155, 120));
|
||||||
|
error_->labelsize(12);
|
||||||
end();
|
end();
|
||||||
resizable(tabs);
|
resizable(tabs_);
|
||||||
|
size_range(800, 540);
|
||||||
|
callback(close_callback, this);
|
||||||
|
open_backend();
|
||||||
|
Fl::add_timeout(0.1, timer_callback, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::show(int argc, char **argv)
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
Fl_Double_Window::show(argc, argv);
|
Fl::remove_timeout(timer_callback, this);
|
||||||
|
// Normal window/menu close already synchronized callbacks. This fallback
|
||||||
|
// covers exceptional event-loop exit. A failed close must retain its C
|
||||||
|
// callback context until process exit, rather than free active audio state.
|
||||||
|
if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK)
|
||||||
|
std::fputs("Failed to close JACK during GUI destruction; backend retained until process exit.\n", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::report_error(const char *operation, xtc_result result)
|
||||||
|
{
|
||||||
|
char text[384];
|
||||||
|
const char *hint = result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support." :
|
||||||
|
"Check the JACK-compatible server and use Reconnect to retry.";
|
||||||
|
std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint);
|
||||||
|
set_label(error_, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::open_backend()
|
||||||
|
{
|
||||||
|
xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 1U};
|
||||||
|
xtc_result result = xtc_backend_open(&config, &backend_);
|
||||||
|
ready_ = result == XTC_OK;
|
||||||
|
running_ = false;
|
||||||
|
if (result != XTC_OK) report_error("Open JACK failed", result);
|
||||||
|
else set_label(error_, "");
|
||||||
|
refresh_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::start_engine()
|
||||||
|
{
|
||||||
|
if (!ready_) {
|
||||||
|
report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (running_) return;
|
||||||
|
xtc_result result = xtc_backend_start(backend_);
|
||||||
|
if (result != XTC_OK) report_error("Start engine failed", result);
|
||||||
|
else set_label(error_, "");
|
||||||
|
refresh_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::stop_engine()
|
||||||
|
{
|
||||||
|
if (!ready_) return;
|
||||||
|
xtc_result result = xtc_backend_stop(backend_);
|
||||||
|
if (result != XTC_OK) report_error("Stop engine failed", result);
|
||||||
|
else set_label(error_, "");
|
||||||
|
refresh_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::reconnect_engine()
|
||||||
|
{
|
||||||
|
xtc_result result = xtc_backend_close(backend_);
|
||||||
|
if (result != XTC_OK) {
|
||||||
|
report_error("Reconnect could not close JACK", result);
|
||||||
|
return; // Preserve the existing handle for a later retry.
|
||||||
|
}
|
||||||
|
backend_ = nullptr;
|
||||||
|
ready_ = false;
|
||||||
|
running_ = false;
|
||||||
|
open_backend();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::refresh_status()
|
||||||
|
{
|
||||||
|
bool can_play = false;
|
||||||
|
if (!ready_) {
|
||||||
|
set_label(indicator_, "Audio: disconnected");
|
||||||
|
set_label(status_, "JACK: not connected — use Reconnect after starting the server");
|
||||||
|
indicator_->labelcolor(fl_rgb_color(255, 155, 120));
|
||||||
|
} else {
|
||||||
|
xtc_backend_status status = {};
|
||||||
|
xtc_result result = xtc_backend_get_status(backend_, &status);
|
||||||
|
if (result != XTC_OK) {
|
||||||
|
report_error("Read engine status failed", result);
|
||||||
|
set_label(indicator_, "Audio: status unavailable");
|
||||||
|
set_label(status_, "Unable to read JACK status — use Reconnect");
|
||||||
|
indicator_->labelcolor(fl_rgb_color(255, 155, 120));
|
||||||
|
} else {
|
||||||
|
running_ = status.running != 0;
|
||||||
|
const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK;
|
||||||
|
can_play = !running_ && !fault;
|
||||||
|
const char *state = status.server_lost ? "Audio: server lost — Reconnect required" :
|
||||||
|
status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" :
|
||||||
|
status.process_result != XTC_OK ? "Audio: processing error" :
|
||||||
|
running_ ? "Audio: running (silence — no playback content)" : "Audio: ready / stopped";
|
||||||
|
set_label(indicator_, state);
|
||||||
|
indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) :
|
||||||
|
running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215));
|
||||||
|
char text[256];
|
||||||
|
std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | mono | %u blocks | %u xruns | %s",
|
||||||
|
status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns,
|
||||||
|
xtc_result_string(status.process_result));
|
||||||
|
set_label(status_, text);
|
||||||
|
}
|
||||||
|
xtc_backend_collect(backend_);
|
||||||
|
}
|
||||||
|
if (can_play) play_->activate(); else play_->deactivate();
|
||||||
|
if (ready_ && running_) stop_->activate(); else stop_->deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainWindow::close_window()
|
||||||
|
{
|
||||||
|
xtc_result result = xtc_backend_close(backend_);
|
||||||
|
if (result != XTC_OK) {
|
||||||
|
report_error("Close JACK failed; window kept open for retry", result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
backend_ = nullptr;
|
||||||
|
ready_ = false;
|
||||||
|
running_ = false;
|
||||||
|
closing_ = true;
|
||||||
|
Fl::remove_timeout(timer_callback, this);
|
||||||
|
hide();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MainWindow::handle(int event)
|
||||||
|
{
|
||||||
|
if (event == FL_SHORTCUT && Fl::event_key() == ' ' &&
|
||||||
|
(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) {
|
||||||
|
if (running_) stop_engine(); else start_engine();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Fl_Double_Window::handle(event);
|
||||||
|
}
|
||||||
|
void MainWindow::play_callback(Fl_Widget *, void *context)
|
||||||
|
{ static_cast<MainWindow *>(context)->start_engine(); }
|
||||||
|
void MainWindow::stop_callback(Fl_Widget *, void *context)
|
||||||
|
{ static_cast<MainWindow *>(context)->stop_engine(); }
|
||||||
|
void MainWindow::reconnect_callback(Fl_Widget *, void *context)
|
||||||
|
{ static_cast<MainWindow *>(context)->reconnect_engine(); }
|
||||||
|
void MainWindow::close_callback(Fl_Widget *, void *context)
|
||||||
|
{ (void)static_cast<MainWindow *>(context)->close_window(); }
|
||||||
|
void MainWindow::view_callback(Fl_Widget *widget, void *view)
|
||||||
|
{
|
||||||
|
auto *window = static_cast<MainWindow *>(widget->window());
|
||||||
|
window->tabs_->value(static_cast<Fl_Widget *>(view));
|
||||||
|
window->tabs_->redraw();
|
||||||
|
}
|
||||||
|
void MainWindow::about_callback(Fl_Widget *, void *)
|
||||||
|
{
|
||||||
|
fl_message("%s %s\nModular audio workstation\nPhase 3: JACK engine controls; editors remain placeholders.",
|
||||||
|
XTC_PROJECT_NAME, XTC_VERSION_STRING);
|
||||||
|
}
|
||||||
|
void MainWindow::timer_callback(void *context)
|
||||||
|
{
|
||||||
|
auto *window = static_cast<MainWindow *>(context);
|
||||||
|
window->refresh_status();
|
||||||
|
if (!window->closing_) Fl::repeat_timeout(0.1, timer_callback, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,47 @@
|
||||||
#define XTC_MAIN_WINDOW_HPP
|
#define XTC_MAIN_WINDOW_HPP
|
||||||
|
|
||||||
#include <FL/Fl_Double_Window.H>
|
#include <FL/Fl_Double_Window.H>
|
||||||
|
#include "xtc/xtc_backend.h"
|
||||||
|
|
||||||
|
class Fl_Box;
|
||||||
|
class Fl_Button;
|
||||||
|
class Fl_Tabs;
|
||||||
|
|
||||||
class MainWindow : public Fl_Double_Window {
|
class MainWindow : public Fl_Double_Window {
|
||||||
public:
|
public:
|
||||||
MainWindow();
|
MainWindow();
|
||||||
void show(int argc, char **argv);
|
~MainWindow() override;
|
||||||
|
MainWindow(const MainWindow &) = delete;
|
||||||
|
MainWindow &operator=(const MainWindow &) = delete;
|
||||||
|
using Fl_Double_Window::show;
|
||||||
|
int handle(int event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
xtc_backend *backend_ = nullptr;
|
||||||
|
bool ready_ = false;
|
||||||
|
bool running_ = false;
|
||||||
|
bool closing_ = false;
|
||||||
|
Fl_Button *play_ = nullptr;
|
||||||
|
Fl_Button *stop_ = nullptr;
|
||||||
|
Fl_Box *indicator_ = nullptr;
|
||||||
|
Fl_Box *status_ = nullptr;
|
||||||
|
Fl_Box *error_ = nullptr;
|
||||||
|
Fl_Tabs *tabs_ = nullptr;
|
||||||
|
|
||||||
|
void open_backend();
|
||||||
|
void start_engine();
|
||||||
|
void stop_engine();
|
||||||
|
void reconnect_engine();
|
||||||
|
void refresh_status();
|
||||||
|
bool close_window();
|
||||||
|
void report_error(const char *operation, xtc_result result);
|
||||||
|
static void play_callback(Fl_Widget *, void *context);
|
||||||
|
static void stop_callback(Fl_Widget *, void *context);
|
||||||
|
static void reconnect_callback(Fl_Widget *, void *context);
|
||||||
|
static void close_callback(Fl_Widget *, void *context);
|
||||||
|
static void view_callback(Fl_Widget *widget, void *view);
|
||||||
|
static void about_callback(Fl_Widget *, void *);
|
||||||
|
static void timer_callback(void *context);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,40 @@ format change is performed on the shared server. Those paths, missing server,
|
||||||
port/callback setup failure, activation/deactivation/close failure, null buffers,
|
port/callback setup failure, activation/deactivation/close failure, null buffers,
|
||||||
invalid configuration, oversize buffers, and reopen are tested with injected
|
invalid configuration, oversize buffers, and reopen are tested with injected
|
||||||
JACK callbacks/results in `tests/unit/test_backend.c`.
|
JACK callbacks/results in `tests/unit/test_backend.c`.
|
||||||
|
|
||||||
|
## FLTK shell tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make gui test-gui
|
||||||
|
# Optional, requires a running JACK-compatible server:
|
||||||
|
make test-gui-jack
|
||||||
|
```
|
||||||
|
|
||||||
|
These targets require FLTK development files, `xvfb-run` and Xvfb. No additional
|
||||||
|
GUI test library is needed. `test_gui.cpp` uses real widgets, menus and timers
|
||||||
|
with a fake public C backend. It covers Play/Stop/Reconnect, view switching,
|
||||||
|
persistent startup/control errors, server loss/rate changes, partial handles,
|
||||||
|
close-failure retry and timer removal. All 18 opened handles are closed.
|
||||||
|
`test_gui_jack.cpp` uses the real backend, repeats start/stop nine times across
|
||||||
|
three windows, reconnects, and closes each window while running. No audio ports
|
||||||
|
are connected. Both tests passed on the inspected host.
|
||||||
|
|
||||||
|
A separate xdotool smoke run against `build/xtc` under a 1280x800 Xvfb display
|
||||||
|
exercised F5/F8/F6, Space, Ctrl+1/2/3 and Ctrl+Q. The app exited successfully;
|
||||||
|
visual inspection confirmed running status and mixer view switching.
|
||||||
|
|
||||||
|
Clang ASan/UBSan found no application errors in the GUI tests. The unsuppressed
|
||||||
|
run reports exit-time allocations originating in the installed fontconfig
|
||||||
|
library used by FLTK/Pango. `lsan.supp` narrowly suppresses that library; leak
|
||||||
|
checking stays enabled for application allocations. This is not a claim that
|
||||||
|
the third-party font stack is leak-free. Apply only to GUI sanitizer tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LSAN_OPTIONS=suppressions=tests/integration/lsan.supp xvfb-run -a -s '-screen 0 1280x800x24' ./build/gui-sanitize/test_gui
|
||||||
|
```
|
||||||
|
|
||||||
|
Build that binary with Clang/Clang++, `-fsanitize=address,undefined` in both
|
||||||
|
CFLAGS/CXXFLAGS and LDFLAGS, using the baseline warning/include/standard flags
|
||||||
|
from BUILD.md. Keep FLTK's `-I` and `-D` flags from `fltk-config --cxxflags`,
|
||||||
|
but exclude the distribution's GCC-specific LTO/object-code flags for Clang.
|
||||||
|
No suppressions are used for core/backend/realtime sanitizer tests.
|
||||||
|
|
|
||||||
4
tests/integration/lsan.supp
Normal file
4
tests/integration/lsan.supp
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Installed FLTK/Pango/fontconfig retain font caches at process exit.
|
||||||
|
# Keep leak detection active for application allocations and backend handles.
|
||||||
|
# The unsuppressed GUI run reports allocation stacks in libfontconfig.so.1.
|
||||||
|
leak:libfontconfig.so
|
||||||
204
tests/integration/test_gui.cpp
Normal file
204
tests/integration/test_gui.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include "../../src/gui/main_window.hpp"
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Group.H>
|
||||||
|
#include <FL/Fl_Menu_Bar.H>
|
||||||
|
#include <FL/Fl_Tabs.H>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
|
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
|
||||||
|
* timers run under Xvfb; no server/device is required or touched. */
|
||||||
|
struct xtc_backend { int running = 0; };
|
||||||
|
namespace {
|
||||||
|
xtc_backend handle;
|
||||||
|
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
|
||||||
|
xtc_result status_result = XTC_OK;
|
||||||
|
xtc_backend_status engine_status = {};
|
||||||
|
bool partial_open = false;
|
||||||
|
unsigned int opens = 0, starts = 0, stops = 0, closes = 0, collects = 0, alive = 0;
|
||||||
|
int failures = 0;
|
||||||
|
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); ++failures; } } while (0)
|
||||||
|
void pump()
|
||||||
|
{
|
||||||
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(180);
|
||||||
|
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
||||||
|
}
|
||||||
|
Fl_Widget *find(Fl_Group *group, const char *label)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < group->children(); ++i) {
|
||||||
|
Fl_Widget *child = group->child(i);
|
||||||
|
if (child->label() != nullptr && std::strcmp(child->label(), label) == 0) return child;
|
||||||
|
auto *nested = dynamic_cast<Fl_Group *>(child);
|
||||||
|
if (nested != nullptr) if (auto *match = find(nested, label)) return match;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
bool contains(Fl_Group *group, const char *text)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < group->children(); ++i) {
|
||||||
|
Fl_Widget *child = group->child(i);
|
||||||
|
if (child->label() != nullptr && std::strstr(child->label(), text) != nullptr) return true;
|
||||||
|
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (contains(nested, text)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void press(Fl_Widget *widget)
|
||||||
|
{
|
||||||
|
CHECK(widget != nullptr);
|
||||||
|
if (widget != nullptr) widget->do_callback();
|
||||||
|
}
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
CHECK(alive == 0U);
|
||||||
|
open_result = start_result = stop_result = close_result = status_result = XTC_OK;
|
||||||
|
engine_status = {};
|
||||||
|
engine_status.sample_rate = 48000U;
|
||||||
|
engine_status.buffer_frames = 1024U;
|
||||||
|
partial_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extern "C" {
|
||||||
|
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
|
||||||
|
{
|
||||||
|
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 1U);
|
||||||
|
++opens;
|
||||||
|
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
|
||||||
|
if (*out != nullptr) { ++alive; handle.running = 0; }
|
||||||
|
return open_result;
|
||||||
|
}
|
||||||
|
xtc_result xtc_backend_close(xtc_backend *backend)
|
||||||
|
{
|
||||||
|
if (backend == nullptr) return XTC_OK;
|
||||||
|
++closes;
|
||||||
|
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; }
|
||||||
|
return close_result;
|
||||||
|
}
|
||||||
|
xtc_result xtc_backend_start(xtc_backend *backend)
|
||||||
|
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) handle.running = 1; return start_result; }
|
||||||
|
xtc_result xtc_backend_stop(xtc_backend *backend)
|
||||||
|
{ CHECK(backend == &handle); ++stops; if (stop_result == XTC_OK) handle.running = 0; return stop_result; }
|
||||||
|
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *status)
|
||||||
|
{
|
||||||
|
CHECK(backend == &handle && alive == 1U);
|
||||||
|
*status = engine_status;
|
||||||
|
status->running = handle.running && !status->server_lost;
|
||||||
|
return status_result;
|
||||||
|
}
|
||||||
|
void xtc_backend_collect(xtc_backend *backend)
|
||||||
|
{ CHECK(backend == &handle && alive == 1U); ++collects; }
|
||||||
|
}
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
{
|
||||||
|
MainWindow window;
|
||||||
|
window.show(argc, argv);
|
||||||
|
auto *play = find(&window, "Play");
|
||||||
|
auto *stop = find(&window, "Stop");
|
||||||
|
auto *reconnect = find(&window, "Reconnect");
|
||||||
|
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
||||||
|
if (play == nullptr || stop == nullptr || reconnect == nullptr) return 1;
|
||||||
|
CHECK(play->active() && !stop->active());
|
||||||
|
CHECK(contains(&window, "ready / stopped"));
|
||||||
|
press(play);
|
||||||
|
CHECK(starts == 1U && !play->active() && stop->active());
|
||||||
|
CHECK(contains(&window, "running (silence"));
|
||||||
|
press(stop);
|
||||||
|
CHECK(stops == 1U && play->active() && !stop->active());
|
||||||
|
start_result = XTC_ERR_IO;
|
||||||
|
press(play);
|
||||||
|
CHECK(contains(&window, "Start engine failed: I/O error"));
|
||||||
|
pump(); // A refresh must not erase a command error.
|
||||||
|
CHECK(contains(&window, "Start engine failed: I/O error"));
|
||||||
|
start_result = XTC_OK;
|
||||||
|
press(play);
|
||||||
|
CHECK(!contains(&window, "Start engine failed"));
|
||||||
|
stop_result = XTC_ERR_IO;
|
||||||
|
press(stop);
|
||||||
|
CHECK(contains(&window, "Stop engine failed") && stop->active());
|
||||||
|
stop_result = XTC_OK;
|
||||||
|
press(stop);
|
||||||
|
Fl_Menu_Bar *menu = nullptr;
|
||||||
|
Fl_Tabs *tabs = nullptr;
|
||||||
|
for (int i = 0; i < window.children(); ++i) {
|
||||||
|
if (auto *found = dynamic_cast<Fl_Menu_Bar *>(window.child(i))) menu = found;
|
||||||
|
if (auto *found = dynamic_cast<Fl_Tabs *>(window.child(i))) tabs = found;
|
||||||
|
}
|
||||||
|
CHECK(menu != nullptr && tabs != nullptr);
|
||||||
|
if (menu == nullptr || tabs == nullptr) return 1;
|
||||||
|
for (const char *view : {"View/Mixer", "View/Modular Graph", "View/Sequencer"}) {
|
||||||
|
const Fl_Menu_Item *item = menu->find_item(view);
|
||||||
|
CHECK(item != nullptr);
|
||||||
|
if (item != nullptr) {
|
||||||
|
menu->picked(item);
|
||||||
|
CHECK(std::strcmp(tabs->value()->label(), std::strchr(view, '/') + 1) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engine_status.xruns = 3U;
|
||||||
|
engine_status.processed_blocks = 123U;
|
||||||
|
pump();
|
||||||
|
CHECK(contains(&window, "123 blocks | 3 xruns"));
|
||||||
|
engine_status.sample_rate_changed = 1;
|
||||||
|
pump();
|
||||||
|
CHECK(contains(&window, "sample rate changed") && !play->active());
|
||||||
|
engine_status.sample_rate_changed = 0;
|
||||||
|
engine_status.server_lost = 1;
|
||||||
|
pump();
|
||||||
|
CHECK(contains(&window, "server lost") && !play->active());
|
||||||
|
engine_status.server_lost = 0;
|
||||||
|
press(reconnect);
|
||||||
|
CHECK(contains(&window, "ready / stopped") && play->active());
|
||||||
|
status_result = XTC_ERR_IO;
|
||||||
|
pump();
|
||||||
|
CHECK(contains(&window, "status unavailable") && !play->active());
|
||||||
|
status_result = XTC_OK;
|
||||||
|
press(reconnect);
|
||||||
|
close_result = XTC_ERR_IO;
|
||||||
|
press(reconnect);
|
||||||
|
CHECK(contains(&window, "Reconnect could not close JACK") && alive == 1U);
|
||||||
|
window.do_callback();
|
||||||
|
CHECK(window.shown() && alive == 1U && contains(&window, "window kept open for retry"));
|
||||||
|
close_result = XTC_OK;
|
||||||
|
menu->picked(menu->find_item("File/Quit"));
|
||||||
|
CHECK(!window.shown() && alive == 0U);
|
||||||
|
unsigned int before = collects;
|
||||||
|
pump();
|
||||||
|
CHECK(collects == before); // Removed timer cannot call a closed backend.
|
||||||
|
}
|
||||||
|
for (xtc_result failure : {XTC_ERR_IO, XTC_ERR_UNSUPPORTED}) {
|
||||||
|
reset();
|
||||||
|
open_result = failure;
|
||||||
|
MainWindow window;
|
||||||
|
window.show(argc, argv);
|
||||||
|
CHECK(contains(&window, "Open JACK failed") && !find(&window, "Play")->active());
|
||||||
|
open_result = XTC_OK;
|
||||||
|
press(find(&window, "Reconnect"));
|
||||||
|
CHECK(find(&window, "Play")->active());
|
||||||
|
press(find(&window, "Play"));
|
||||||
|
window.do_callback(); // Close while running, without a preceding stop.
|
||||||
|
CHECK(!window.shown() && alive == 0U);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
partial_open = true;
|
||||||
|
open_result = XTC_ERR_IO;
|
||||||
|
{
|
||||||
|
MainWindow window;
|
||||||
|
CHECK(alive == 1U && !find(&window, "Play")->active());
|
||||||
|
window.do_callback(); // Partially initialized handles are closed only.
|
||||||
|
CHECK(alive == 0U);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
MainWindow window;
|
||||||
|
press(find(&window, "Play"));
|
||||||
|
window.do_callback();
|
||||||
|
}
|
||||||
|
CHECK(alive == 0U);
|
||||||
|
if (failures != 0) return 1;
|
||||||
|
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
60
tests/integration/test_gui_jack.cpp
Normal file
60
tests/integration/test_gui_jack.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#include "../../src/gui/main_window.hpp"
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Group.H>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Fl_Widget *find(Fl_Group &group, const char *label)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < group.children(); ++i) {
|
||||||
|
Fl_Widget *child = group.child(i);
|
||||||
|
if (child->label() != nullptr && std::strstr(child->label(), label) != nullptr) return child;
|
||||||
|
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (auto *match = find(*nested, label)) return match;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
void pump()
|
||||||
|
{
|
||||||
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);
|
||||||
|
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
// Real JACK backend and real FLTK event loop. No routes, no playback data.
|
||||||
|
for (int iteration = 0; iteration < 3; ++iteration) {
|
||||||
|
MainWindow window;
|
||||||
|
window.show(argc, argv);
|
||||||
|
CHECK(find(window, "ready / stopped") != nullptr);
|
||||||
|
auto *play = find(window, "Play");
|
||||||
|
auto *stop = find(window, "Stop");
|
||||||
|
auto *reconnect = find(window, "Reconnect");
|
||||||
|
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
||||||
|
for (int restart = 0; restart < 3; ++restart) {
|
||||||
|
CHECK(play->active());
|
||||||
|
play->do_callback();
|
||||||
|
pump();
|
||||||
|
CHECK(find(window, "running (silence") != nullptr);
|
||||||
|
auto *status = find(window, "JACK |");
|
||||||
|
CHECK(status != nullptr);
|
||||||
|
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
||||||
|
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | mono | %u blocks", &rate, &frames, &blocks) == 3);
|
||||||
|
CHECK(rate > 0U && frames > 0U && blocks > 0U);
|
||||||
|
stop->do_callback();
|
||||||
|
CHECK(find(window, "ready / stopped") != nullptr);
|
||||||
|
}
|
||||||
|
reconnect->do_callback();
|
||||||
|
CHECK(find(window, "ready / stopped") != nullptr);
|
||||||
|
play->do_callback();
|
||||||
|
pump();
|
||||||
|
CHECK(find(window, "running (silence") != nullptr);
|
||||||
|
window.do_callback(); // Close with an active client; synchronizes callbacks.
|
||||||
|
CHECK(!window.shown());
|
||||||
|
pump(); // No timer may access the closed client.
|
||||||
|
}
|
||||||
|
std::puts("Live GUI/JACK start/stop/reconnect/active-close tests passed.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue