feat(midi): qualify routed startup and refresh workflow screenshots (phase 105)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:35:48 +01:00
commit 25d4ad5d3b
73 changed files with 208 additions and 37 deletions

View file

@ -608,3 +608,7 @@ test-midi-file: $(BUILD_DIR)/test_midi_file
./$(BUILD_DIR)/test_midi_file
test: test-midi-file
-include $(BUILD_DIR)/tests/unit/test_midi_file.d
.PHONY: test-configuration-startup
test-configuration-startup: gui
xvfb-run -a -s "-screen 0 1440x960x24" $(PYTHON) tests/integration/test_configuration_startup.py ./$(BUILD_DIR)/xtc

View file

@ -184,3 +184,10 @@ Phases 90–99 add direct selected-track inserts, effect duplication, chain copy
paste, A/B snapshots, three internal effect templates, bulk rack actions, insert
peak meters, tap tempo, audio crossfades and MIDI chord strumming. See
[creative workflows and limits](docs/CREATIVE_WORKFLOWS.md).
MIDI devices and defaults are configurable in **Preferences → midi**, including
JACK/PipeWire input/output choices, channel filtering and optional thru. Use
**File → Import MIDI** or **Export selected clip as MIDI** to exchange note clips.
Application preferences load from **`~/.xtc.conf`** at startup and save on Apply
and clean exit. See [configuration and MIDI workflows](docs/MIDI_WORKFLOWS.md)
for file support, routing and persistence details.

View file

@ -82,3 +82,8 @@ Unterminated notes, zero source duration and malformed events reject rather than
silently dropping notes. Unmatched note-offs are harmless and ignored. Overlapping
same-pitch notes pair FIFO within each file track/channel. Project save/reload
uses the existing format; no format migration or extra dependency is required.
Configured startup holds the transport clock and emits silence until selected
MIDI ports are connected, so notes at the song start are not consumed before
routing. MIDI routing retries publication for at most 100 ms on the control
thread; the callback never sleeps or calls the JACK graph connection APIs.

View file

@ -3,11 +3,12 @@
Open **Edit / Preferences** (Ctrl+,). Each tab edits application settings.
**Apply** validates every tab, saves `~/.xtc.conf`, and applies the values.
**Close** discards edits made since the last Apply. Apply stops playback; stop
recording before opening Preferences. Missing configuration uses defaults.
recording before opening Preferences. Clean exit also saves collected application settings.
Startup reads `~/.xtc.conf` before project/backend creation; missing keys use defaults.
Malformed configuration reports an error and uses defaults without overwriting
it. Invalid input or failed writes preserve the previous saved configuration.
The file is INI with `[audio]`, `[plugins]`, `[interface]`, `[editing]`,
The file is INI with `[audio]`, `[plugins]`, `[midi]`, `[interface]`, `[editing]`,
`[project_defaults]` and `[transport]` sections. Save uses a mode-0600 temporary
file, flushes and fsyncs it, then atomically renames it. Unknown keys/sections,
duplicate keys, invalid ranges, embedded NULs and files larger than 64 KiB are
@ -18,7 +19,8 @@ may be quoted, with `\\` and `\"` escapes. Apply rewrites comments and formattin
| --- | --- |
| audio | `client_name="xtc"`, `max_frames=65536`, `channels=2`, `transition_ms=5`, `offline_rate=48000`, `recording_seconds=2` |
| plugins | `clap_paths`, `vst2_paths`, `vst3_paths`, `ladspa_paths`: colon-separated `~/.clap`, `~/.vst`, `~/.vst3`, `~/.ladspa`, followed by corresponding `/usr/local/lib/` and `/usr/lib/` directories |
| interface | `width=1280`, `height=800`, `piano_width=900`, `piano_height=620`, `refresh_ms=100` |
| midi | `midi_input_port=""`, `midi_output_port=""`, `midi_input_channel=-1`, `midi_output_channel=-1`, `midi_thru=0`; ports connect on Play/Record |
| interface | `main_x=0`, `main_y=0`, `inspector_visible=0`, `width=1280`, `height=800`, `piano_width=900`, `piano_height=620`, `refresh_ms=100` |
| editing | `sequencer_snap=240`, `piano_snap=240`, `sequencer_zoom=48`, `clip_ticks=3840`, `note_ticks=960`, `note_velocity=100`, `note_channel=0`, `filter_channel=-1`, `filter_low=0`, `filter_high=127` |
| project_defaults | `tempo=120000` (milli-BPM), `volume_cc=7`, `pan_cc=10`, `sustain_cc=64` (128 disables a mapping) |
| transport | `loop_start=0`, `loop_end=3840`, `loop_enabled=0`, `follow_playhead=0` |
@ -34,7 +36,7 @@ safety limits and plugin ABI requirements are not runtime preferences.
Snap accepts 1 (Off), 120, 160, 240, 320, 480, 960, 1920 or 3840 ticks.
Zoom accepts 24, 48, 96, 192 or 384 pixels per beat. MIDI channels are zero-based;
filter channel -1 shows all channels. Window sizes, grids and filters currently
in use populate Preferences when it opens; press Apply to persist them.
in use populate Preferences when it opens; Apply or a clean application exit persists them.
Plugin selection now opens a library of `.clap` and `.so` files from the search
paths. `~/` expands to the home directory; empty/nonabsolute roots are skipped.
@ -49,7 +51,7 @@ other locations. It is not a plugin certification or isolation service.
**Shift-drag the sequencer ruler** to select a region using the selected Snap.
Dragging in either direction works. A band shows the region; **Loop** enables
or disables it. **Loop range...** also accepts start and end in ticks. Selection
and toggling take effect immediately; Apply in Preferences persists the region.
and toggling take effect immediately; Apply or clean exit persists the region.
The region is global application configuration and is not stored in `.xtc`
projects. Changing a region during playback stops, reconfigures and restarts the
backend. Selecting a region does not itself enable looping.
@ -73,12 +75,19 @@ playback stopping uses the GUI timer and is disabled during looping/recording.
See BUILTIN_DEVICES.md for endpoint, cancellation and reset semantics.
Phase 45 adds `[interface] transport_detached`, `mixer_detached`, and each
panel's `_x`, `_y`, `_width`, `_height` rectangle. Use Preferences / Apply after
panel's `_x`, `_y`, `_width`, `_height` rectangle. Use Preferences / Apply or close the application cleanly after
moving panels to save the captured layout. Restoration clamps rectangles to the
current display; the floating transport wraps at narrow widths.
Phase 46 extends `[interface]` with `sequencer_detached`, `graph_detached`,
`piano_detached`, editor window geometry and `track_header_width` (180..360).
Piano retains `piano_width`/`piano_height` and adds `piano_x`/`piano_y`.
Reset workspace layout changes the session; Preferences / Apply persists it.
Reset workspace layout changes the session; Preferences / Apply or clean exit persists it.
Only original-tab docking and floating windows are supported.
Phases 100–105 extend configuration with complete workspace collection and MIDI
ports/channels/thru. Invalid startup files are preserved on exit; explicit Apply
can replace them after validation. If saving fails, the window stays open for
retry. See [MIDI_WORKFLOWS.md](MIDI_WORKFLOWS.md) for device setup and notes-only
Standard MIDI File import/export. JACK server hardware settings and musical
project contents remain owned by the server and project respectively.

View file

@ -1006,7 +1006,8 @@ Persist application preferences in `~/.xtc.conf` and provide selectable sequence
### Implementation status
Implemented without dependencies or a project format change. Application settings
are saved only with Apply; audio connection options take effect on Reconnect and
were initially saved only with Apply; Phase 100 also saves on clean exit.
Audio connection options take effect on Reconnect and
project defaults on New. Existing projects retain their musical data. Loop ranges
are global preferences, not project data. Enabled loops require at least 64 samples.
See [PREFERENCES.md](PREFERENCES.md) for configuration, workflow and limitations.
@ -2301,15 +2302,38 @@ Implemented: bounded C99 type 0/1 PPQN notes import with running status, channel
Capture current MIDI preferences, file workflows and imported notes, refresh the gallery, and qualify configuration, MIDI I/O and file workflows together.
### Tasks and acceptance criteria
- [ ] Implement and document the workflow with bounded validation and safe ownership.
- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo.
- [ ] Commit this phase separately after validation.
- [x] Implement and document the workflow with bounded validation and safe ownership.
- [x] Pass meaningful core and GUI checks, including errors and persistence or undo.
- [x] Commit this phase separately after validation.
### Implementation status
Planned.
Implemented: all 66 gallery PNGs were refreshed, including five new MIDI preferences/help/file-menu/import captures through real C/GUI workflows. A real-process isolated-home test proves ~/.xtc.conf creation, startup reload and malformed-file preservation. Final routing qualification adds bounded PipeWire publication retry and holds transport until chosen routes exist, preserving notes at position zero; deterministic callback and real receiver checks pass. Full core/reliability, GUI/plugin/JACK, optional-backend builds, ASan/UBSan, static analysis and staged installation checks passed.
---
## Integrated Validation for Phases 100–105
Completed on 2026-11-06. Normal builds use `BUILD_DIR=build/audio-editor`:
`all gui test-reliability`, `test-gui-midi-workflows`, `test-gui-creative`,
`test-gui-track-fx`, full `test-gui`, modern-plugin/instrument/LADSPA GUI fixtures,
`test-gui-jack`, `test-jack-midi`, and `test-configuration-startup` passed.
The final routed-start change was rechecked with core/reliability, full/focused
GUI, live MIDI first-note receipt and real-process configuration startup tests.
`BUILD_DIR=build/midi-minimal PKG_CONFIG=false FLTK_CONFIG=false all test` passed.
Clang ASan/UBSan full core/reliability and focused/full GUI checks passed using
only the existing Fontconfig leak suppression. The existing Clang/FLTK redundant
instrumentation warning remains; normal warning-enabled builds are clean.
Clang static analysis (including a final backend recheck), default/custom staged
install layouts and C/C++ pkg-config consumers passed.
The full gallery and host VST3/creative/MIDI capture modes regenerated 66 PNGs.
File import is notes-only type 0/1 PPQN and retains project tempo. MIDI devices
are JACK/PipeWire ports; physical hardware/bridge compatibility is not claimed
from software-port tests. No dependencies or project-format change were added.
See [MIDI_WORKFLOWS.md](MIDI_WORKFLOWS.md), [PREFERENCES.md](PREFERENCES.md) and
[the gallery](../screenshots/README.md) for reproduction and exact limits.
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification.

View file

@ -114,7 +114,8 @@ Duplicate, Rename and Delete. Invalid/capacity edits retain the existing model.
View/Snap selects 1, 120, 240 (default), 480 or 960 ticks. It affects ruler seeks,
clip moves and trims; 1 tick is the finest grid. Existing +/- zoom and scrollbars
retain their behavior. Piano roll has its own configurable grid (see MIDI.md); mixer lanes retain their 240-tick
grid. Grid/zoom/selection are session UI preferences, not undoable music edits.
grid. Grid and zoom are application preferences saved on Apply/clean exit;
selection is session state. These controls are not undoable music edits.
Escape cancels a drag; Delete affects the focused editor; text fields keep their
own editing keys. Commands are named in menus/tooltips and timeline focus accepts
keyboard editing; screen-reader and broad accessibility qualification are pending.

View file

@ -164,7 +164,7 @@ and meters; the main Mixer tab offers Show and Dock actions while it is floating
The transport owns the playback buttons, position, **BPM**, **Loop** toggle and
**Loop range...** settings in both layouts. Shift-drag on the sequencer ruler
still updates the same loop region. BPM remains an undoable project edit;
loop preferences retain their existing Preferences / Apply persistence behavior.
loop preferences persist through Preferences / Apply or clean application exit.
Space and menu playback shortcuts work from either floating panel, with normal
numeric/text editing protection. Closing the main window closes all panels only
after the existing unsaved/recording/backend shutdown checks succeed.
@ -182,8 +182,8 @@ playback shortcuts, failed shutdown and cleanup.
## Phase 45 saved panel layouts
Preferences / Apply now saves transport/mixer dock state and window rectangles.
Open Preferences after arranging the windows to capture their current positions.
Closing the application alone does not save preferences. Startup restores the
Preferences / Apply captures their current positions; Phase 100 also saves on
clean application exit. Startup restores the
saved layout; off-screen rectangles are clamped to an available display's work
area. Invalid dimensions are rejected transactionally by the settings reader.

View file

@ -81,6 +81,10 @@ xtc_result xtc_backend_close(xtc_backend *backend);
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_backend_start(xtc_backend *backend);
/* Activate with transport held until chosen MIDI routes are established.
* NULL/empty routes mean none. Control-thread only; failed routes stop playback.
* Avoids losing notes at position zero while JACK publishes/connects ports. */
xtc_result xtc_backend_start_routed(xtc_backend *,const char *input_source,const char *output_destination);
/**
* @brief Deactivates the backend and establishes callback quiescence.
*

View file

@ -1,7 +1,8 @@
# xtc screenshots
The complete gallery was refreshed after Phase 89 on 2026-11-06, including
the sample editor, compact workspace and real installed VST3 insert chains.
The complete gallery was refreshed after Phases 100–105 on 2026-11-06, including
MIDI preferences/import, the sample editor, creative workflows and real installed
VST3 insert chains.
These are actual rendered application windows and menus on a 1440 × 960 virtual
X11 screen, without mockups, compositing or image edits.
@ -14,10 +15,27 @@ 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.
## MIDI devices, defaults and file exchange (Phases 100–105)
- [MIDI source/destination preferences and channel defaults](preferences-midi.png)
- [Device setup and MIDI defaults help](preferences-midi-help.png)
- [File menu with MIDI import and selected-clip export](midi-file-menu.png)
- [An exported clip reimported into a new arrangement](midi-import-arrangement.png)
- [The imported notes and velocities in the piano roll](midi-import-piano-roll.png)
These captures use real file export/import and the real preferences schema. No
hardware device names or connected devices are fabricated; blank port defaults
mean no automatic connection. Actual device choices depend on host JACK/PipeWire
exposure. Configuration paths are disabled in captures to preserve user settings.
See [MIDI workflows and limits](../docs/MIDI_WORKFLOWS.md).
Reproduce these five captures with
`XTC_CAPTURE_MIDI_ONLY=1 make BUILD_DIR=build/audio-editor screenshots`.
## Creative workflows (Phases 90–99)
Four additional real captures show the current transport Tap button, insert
meters and new creative controls. Earlier gallery images retain Phase 89 UI.
Four real captures show the current transport Tap button, insert meters and
creative controls; the rest of the gallery also reflects the current UI.
- [Dub Echo template in the insert graph](creative-dub-rack.png)
- [Rack actions: bulk edits, comparison and chain clipboard](creative-rack-actions.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

View file

@ -21,7 +21,7 @@ struct xtc_backend {
xtc_audio_plan *audio;
xtc_midi_plan *midi;
xtc_midi_output_plan *midi_out;
unsigned int output_seen,output_muted;
unsigned int output_seen,output_muted,route_wait;
int input_channel,output_channel,midi_thru;
unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right;
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
@ -167,7 +167,7 @@ static int process(jack_nframes_t frames, void *context)
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
return 0;
}
if (__atomic_load_n(&backend->stop_done, __ATOMIC_ACQUIRE)) {
if (__atomic_load_n(&backend->route_wait,__ATOMIC_ACQUIRE) || __atomic_load_n(&backend->stop_done, __ATOMIC_ACQUIRE)) {
memset(output, 0, (size_t)frames * sizeof(*output));
if (right) memset(right, 0, (size_t)frames * sizeof(*right));
__atomic_store_n(&backend->peak_left, 0U, __ATOMIC_RELAXED);
@ -936,10 +936,28 @@ xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char
r=xtc_backend_midi_name(b,d,&own);if(r!=XTC_OK)return r;
if(!b->running)return XTC_ERR_INVALID_STATE;
if(!strcmp(remote,jack_port_name(b->midi_input)) || !strcmp(remote,jack_port_name(b->midi_output)))return XTC_ERR_INVALID_ARGUMENT;
code=disconnect ? jack_disconnect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own) :
jack_connect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own);
if(disconnect)code=jack_disconnect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own);
else{unsigned int attempt;
/* PipeWire publishes newly activated ports asynchronously. Control only. */
for(attempt=0;;++attempt){
code=jack_connect(b->client,d==XTC_PORT_OUTPUT ? own : remote,d==XTC_PORT_OUTPUT ? remote : own);
if(code==0 || code==EEXIST || attempt==10)break;
{struct timespec pause={0,10000000};nanosleep(&pause,NULL);}
}
}
return code==0 || (!disconnect && code==EEXIST) ? XTC_OK : XTC_ERR_IO;
}
xtc_result xtc_backend_start_routed(xtc_backend *b,const char *input,const char *output)
{
xtc_result r;if(!b)return XTC_ERR_INVALID_ARGUMENT;
if(!b->ready || b->running)return XTC_ERR_INVALID_STATE;
__atomic_store_n(&b->route_wait,1U,__ATOMIC_RELEASE);
r=xtc_backend_start(b);
if(r==XTC_OK && input && *input)r=xtc_backend_route_midi(b,XTC_PORT_INPUT,input,0);
if(r==XTC_OK && output && *output)r=xtc_backend_route_midi(b,XTC_PORT_OUTPUT,output,0);
if(r!=XTC_OK && b->running){xtc_result stopped=xtc_backend_stop(b);if(stopped!=XTC_OK)r=stopped;}
__atomic_store_n(&b->route_wait,0U,__ATOMIC_RELEASE);return r;
}
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out)
{
size_t i;
@ -1234,4 +1252,5 @@ xtc_result xtc_backend_set_midi_options(xtc_backend *b,int i,int o,int t){(void)
xtc_result xtc_backend_midi_name(const xtc_backend *b,xtc_port_direction d,const char **o){(void)b;(void)d;(void)o;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *b,xtc_port_direction d,xtc_backend_port_visitor v,void *c){(void)b;(void)d;(void)v;(void)c;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_route_midi(xtc_backend *b,xtc_port_direction d,const char *r,int x){(void)b;(void)d;(void)r;(void)x;return XTC_ERR_UNSUPPORTED;}
xtc_result xtc_backend_start_routed(xtc_backend *b,const char *i,const char *o){(void)b;(void)i;(void)o;return XTC_ERR_UNSUPPORTED;}
#endif

View file

@ -2574,12 +2574,8 @@ std::vector<std::string> MainWindow::midi_devices(bool input)
xtc_result MainWindow::start_configured_backend()
{
xtc_result r=xtc_backend_set_midi_options(backend_,static_cast<int>(settings_.midi_input_channel),static_cast<int>(settings_.midi_output_channel),settings_.midi_thru!=0);
if(r==XTC_OK)r=xtc_backend_start(backend_);
if(r!=XTC_OK)return r;
if(settings_.midi_input_port[0])r=xtc_backend_route_midi(backend_,XTC_PORT_INPUT,settings_.midi_input_port,0);
if(settings_.midi_output_port[0]){auto out=xtc_backend_route_midi(backend_,XTC_PORT_OUTPUT,settings_.midi_output_port,0);if(r==XTC_OK)r=out;}
if(r!=XTC_OK){(void)xtc_backend_stop(backend_);return r;}
return XTC_OK;
if(r==XTC_OK)r=xtc_backend_start_routed(backend_,settings_.midi_input_port,settings_.midi_output_port);
return r;
}
void MainWindow::show_preferences()
{

View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Exercise the real GUI's default ~/.xtc.conf lifecycle in an isolated child HOME."""
import configparser
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
binary = str(Path(sys.argv[1]).resolve())
with tempfile.TemporaryDirectory(prefix="xtc-config-startup-") as directory:
configuration = Path(directory) / ".xtc.conf"
environment = dict(os.environ)
environment["HOME"] = directory # Child only; never reads/writes the user's config.
def run_window(expected_width):
process = subprocess.Popen([binary], env=environment, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
try:
window = None
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
if process.poll() is not None:
raise AssertionError(f"GUI exited early: {process.communicate()}")
found = subprocess.run(["xdotool", "search", "--onlyvisible", "--pid",
str(process.pid), "--name", "xtc"],
capture_output=True, text=True)
if found.returncode == 0:
window = found.stdout.splitlines()[0]
geometry = subprocess.check_output(
["xdotool", "getwindowgeometry", "--shell", window], text=True)
if f"WIDTH={expected_width}\n" in geometry:
break
time.sleep(.05)
else:
raise AssertionError(f"Expected startup width {expected_width}; window={window}")
subprocess.run(["xdotool", "windowfocus", window], check=True)
subprocess.run(["xdotool", "key", "--window", window, "ctrl+q"], check=True)
output, error = process.communicate(timeout=15)
assert process.returncode == 0, (output, error)
finally:
if process.poll() is None:
process.kill()
process.communicate()
run_window(1280)
assert configuration.exists() and configuration.stat().st_mode & 0o777 == 0o600
saved = configparser.ConfigParser()
saved.read(configuration)
assert saved["midi"]["midi_input_channel"] == "-1"
assert saved["editing"]["note_velocity"] == "100"
configuration.write_text("[interface]\nwidth=1360\nheight=860\n"
"[midi]\nmidi_input_channel=5\nmidi_output_channel=7\n"
"[project_defaults]\ntempo=137000\n")
run_window(1360)
saved.read(configuration)
assert saved["midi"]["midi_input_channel"] == "5"
assert saved["midi"]["midi_output_channel"] == "7"
assert saved["project_defaults"]["tempo"] == "137000"
invalid = "[unknown]\nkeep_this_file=1\n"
configuration.write_text(invalid)
run_window(1280)
assert configuration.read_text() == invalid
print("Real startup ~/.xtc.conf creation, defaults, reload and malformed-file preservation passed.")

View file

@ -166,6 +166,8 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
++engine_status.submitted_revision;
return XTC_OK;
}
xtc_result xtc_backend_start_routed(xtc_backend *b,const char *input,const char *output)
{auto r=xtc_backend_start(b);if(r==XTC_OK && input && *input)r=xtc_backend_route_midi(b,XTC_PORT_INPUT,input,0);if(r==XTC_OK && output && *output)r=xtc_backend_route_midi(b,XTC_PORT_OUTPUT,output,0);if(r!=XTC_OK && handle.running)(void)xtc_backend_stop(b);return r;}
xtc_result xtc_backend_set_midi_options(xtc_backend *,int input,int output,int thru)
{CHECK(input>=-1 && input<16 && output>=-1 && output<16 && thru>=0 && thru<2);return handle.running ? XTC_ERR_INVALID_STATE : XTC_OK;}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction d,xtc_backend_port_visitor visit,void *c)

View file

@ -59,7 +59,7 @@ int main(void)
jack_status_t flags = 0;
probe state = {0};
const char *destination;
uint64_t track, clip, note;
uint64_t track, clip, note;unsigned int before_scheduled;
int result = EXIT_FAILURE;
REQUIRE(mkdtemp(directory));
snprintf(song, sizeof(song), "%s/song.xtc", directory);
@ -77,7 +77,7 @@ int main(void)
REQUIRE(state.input && state.right && state.midi && state.receive);
REQUIRE(xtc_backend_set_midi_options(backend,-1,-1,1)==XTC_OK);
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_start_routed(backend,jack_port_name(state.midi),jack_port_name(state.receive)) == XTC_OK);
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
wait_blocks(); /* PipeWire JACK publishes newly activated ports asynchronously. */
{
@ -109,14 +109,18 @@ int main(void)
REQUIRE(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
REQUIRE(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
REQUIRE(xtc_backend_prepare_project(backend, q) == XTC_OK);
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
before_scheduled=__atomic_load_n(&state.midi_on,__ATOMIC_RELAXED);
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start_routed(backend,NULL,jack_port_name(state.receive)) == XTC_OK);
wait_blocks(); /* Publish restarted PipeWire ports before connecting. */
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED) && __atomic_load_n(&state.right_loud, __ATOMIC_RELAXED));
REQUIRE(__atomic_load_n(&state.midi_on,__ATOMIC_RELAXED)>before_scheduled);
{ xtc_backend_status status; REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0); }
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_backend_seek(backend, 7680000) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
wait_blocks();
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }

View file

@ -25,7 +25,7 @@ static unsigned char sent[512][3];static unsigned int sent_time[512],sent_count;
static unsigned int midi_count;
static jack_midi_event_t midi_events[256];
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
static int active, closed, port_calls, null_buffer;
static int active, closed, port_calls, null_buffer, process_during_connect;
static unsigned int server_frames = 32U;
jack_client_t *jack_client_open(const char *name, jack_options_t options, jack_status_t *status, ...)
{
@ -88,7 +88,7 @@ const char **jack_get_ports(jack_client_t *client, const char *pattern, const ch
}
void jack_free(void *pointer) { free(pointer); }
int jack_connect(jack_client_t *client, const char *source, const char *dest)
{ (void)client; return strstr(source, "missing") || strstr(dest, "missing") ? -1 : EEXIST; }
{ (void)client; if(process_during_connect && process_cb)process_cb(32,context); return strstr(source, "missing") || strstr(dest, "missing") ? -1 : EEXIST; }
int jack_disconnect(jack_client_t *client, const char *source, const char *dest)
{ (void)client; (void)source; (void)dest; return 0; }
static int visit(const char *name, void *arg)
@ -413,7 +413,9 @@ int main(void)
CHECK(xtc_project_add_midi_clip(p,t,0,3840,&c)==XTC_OK && xtc_project_add_note(p,c,0,960,60,90,1,&n)==XTC_OK);
CHECK(xtc_backend_open(&config,&backend)==XTC_OK && xtc_backend_prepare_project(backend,p)==XTC_OK);
CHECK(xtc_backend_midi_name(backend,XTC_PORT_OUTPUT,&midi_name)==XTC_OK && !strcmp(midi_name,"test:midi_out"));
CHECK(xtc_backend_set_midi_options(backend,2,4,1)==XTC_OK && xtc_backend_start(backend)==XTC_OK);
CHECK(xtc_backend_set_midi_options(backend,2,4,1)==XTC_OK);
process_during_connect=1;CHECK(xtc_backend_start_routed(backend,"controller:out","device:in")==XTC_OK);process_during_connect=0;
CHECK(xtc_backend_get_status(backend,&status)==XTC_OK && status.sample_position==0 && !sent_count);
CHECK(xtc_backend_route_midi(backend,XTC_PORT_OUTPUT,"device:in",0)==XTC_OK);
CHECK(xtc_backend_set_midi_options(backend,-1,-1,0)==XTC_ERR_INVALID_STATE);
midi_count=1;midi_events[0]=(jack_midi_event_t){8,3,on};CHECK(process_cb(32,context)==0);

View file

@ -63,6 +63,17 @@ int main(int argc,char **argv) {
MainWindow window(false,"");main_window=&window;window.resize(0,0,1400,900);window.show();check(window.open_project(project.c_str()));pump();
auto *s=first<SequencerWidget>(window);if(!s)throw std::runtime_error("Sequencer missing");
auto reset=[&](){window.reset_layout();check(window.open_project(project.c_str()));window.resize(0,20,1400,900);tab("Sequencer");s->select(0,0);s->position(0);check(window.set_loop(3840,7680,true));window.fit_song();pump();};
if(std::getenv("XTC_CAPTURE_MIDI_ONLY")) {
reset();dialog("preferences-midi",[&]{window.show_preferences();},"midi");
dialog("preferences-midi-help",[&]{window.show_preferences();},"MIDI help");
auto *bar=first<Fl_Menu_Bar>(window);auto *file=bar->find_item("File");if(!file)throw std::runtime_error("File menu missing");
Fl::add_timeout(.3,[](void*){capture("midi-file-menu");if(system("xdotool key Escape"))std::abort();});
(file+1)->pulldown(0,bar->y()+bar->h(),0,0,nullptr,bar);
std::string midi=root+"/exported-keys.mid";check(window.export_midi_clip(midi.c_str()));
check(window.new_project());check(window.import_midi_file(midi.c_str()));window.fit_song();capture("midi-import-arrangement");
piano_view();capture("midi-import-piano-roll");
window.do_callback();std::puts("MIDI workflow captures completed.");return 0;
}
if(std::getenv("XTC_CAPTURE_CREATIVE_ONLY")) {
reset();check(window.open_track_inserts(0));auto *inserts=window.track_inserts();
check(inserts->apply_template(1));tab("Track inserts");capture("creative-dub-rack");
@ -118,7 +129,7 @@ int main(int argc,char **argv) {
dialog("audio-clip-properties",[&]{s->audio_properties_dialog();});dialog("audio-normalization",[&]{s->normalize_dialog();});check(s->normalize_audio(.25f));capture("audio-normalization-result");
dialog("project-collection",[&]{menu("File/Collect project media...");});check(window.collect_project((root+"/portable").c_str()));
dialog("loop-range",[&]{button("Loop range...");});dialog("midi-controller-mapping",[&]{menu("Audio/MIDI controller mapping");});dialog("about",[&]{menu("Help/About");});
for(const char *section:{"audio","plugins","interface","editing","project_defaults","transport"}){std::string file="preferences-"+std::string(section);for(char &c:file)if(c==' ')c='_';dialog(file.c_str(),[&]{window.show_preferences();},section);}
for(const char *section:{"audio","plugins","midi","interface","editing","project_defaults","transport"}){std::string file="preferences-"+std::string(section);for(char &c:file)if(c==' ')c='_';dialog(file.c_str(),[&]{window.show_preferences();},section);}
xtc_settings settings=window.settings();std::string fixtures=std::filesystem::canonical(argv[0]).parent_path().string();snprintf(settings.ladspa_paths,sizeof(settings.ladspa_paths),"%s",fixtures.c_str());settings.clap_paths[0]=settings.vst2_paths[0]=settings.vst3_paths[0]=0;
dialog("plugin-library",[&]{std::string path;xtc_choose_plugin_file(settings,path);});
for(auto entry:{std::pair<const char *,const char *>{"plugin-descriptors","plugin-fixture.so"},{"clap-instrument-descriptors","clap-synth-fixture.clap"},{"vst2-instrument-descriptors","vst2-synth-fixture.so"},{"vst3-instrument-descriptors","vst3-synth-fixture.so"}}){unsigned int index=0;dialog(entry.first,[&]{xtc_choose_plugin((fixtures+"/"+entry.second).c_str(),48000,index,strcmp(entry.first,"plugin-descriptors")!=0);});}