feat(midi): qualify routed startup and refresh workflow screenshots (phase 105)
4
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
BIN
screenshots/midi-file-menu.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
BIN
screenshots/midi-import-arrangement.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
screenshots/midi-import-piano-roll.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 51 KiB |
BIN
screenshots/preferences-midi-help.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
screenshots/preferences-midi.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
65
tests/integration/test_configuration_startup.py
Normal 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.")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);});}
|
||||
|
|
|
|||