Phase 35: add persistent preferences and sequencer loop playback

This commit is contained in:
Johannes Findeisen 2026-10-28 10:37:01 +01:00
commit 47b7d38a66
21 changed files with 852 additions and 108 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -511,3 +511,12 @@ test: test-instruments
.PHONY: test-gui-instruments
test-gui-instruments: $(BUILD_DIR)/test_gui $(BUILD_DIR)/clap-synth-fixture.clap $(BUILD_DIR)/vst2-synth-fixture.so $(BUILD_DIR)/vst3-synth-fixture.so
XTC_TEST_SYNTH_CLAP="$(abspath $(BUILD_DIR))/clap-synth-fixture.clap" XTC_TEST_SYNTH_VST2="$(abspath $(BUILD_DIR))/vst2-synth-fixture.so" XTC_TEST_SYNTH_VST3="$(abspath $(BUILD_DIR))/vst3-synth-fixture.so" xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
$(BUILD_DIR)/test_settings: $(BUILD_DIR)/tests/unit/test_settings.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-settings
test-settings: $(BUILD_DIR)/test_settings
./$(BUILD_DIR)/test_settings
test: test-settings
-include $(BUILD_DIR)/tests/unit/test_settings.d

68
docs/PREFERENCES.md Normal file
View file

@ -0,0 +1,68 @@
# Application preferences and looping
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.
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]`,
`[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
rejected. Blank lines and full-line `;` / `#` comments are supported. Strings
may be quoted, with `\\` and `\"` escapes. Apply rewrites comments and formatting.
| Section | Settings and defaults |
| --- | --- |
| 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` |
| 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` |
Audio client name, block capacity, output channels and fades take effect on
**Reconnect**. JACK chooses the actual device, rate and hardware buffer size;
these remain JACK server settings. Offline rate is used when disconnected.
Recording capacity applies to the next take. Tempo and CC maps apply to **New**
projects. Track mix, routing, instruments, plugin parameters, automation and
musical content remain project data, saved through Save Project. Internal
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.
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.
Traversal does not follow directory symlinks and stops at depth six, 4096 entries
or 512 files. Linux VST3 bundle binaries are found inside bundle directories.
Listing reads filenames only. **Inspect plugin** explicitly loads the selected
trusted binary through the existing descriptor chooser; **Browse files** allows
other locations. It is not a plugin certification or isolation service.
## Loop playback
**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.
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.
Playback wraps at the exclusive end sample, including inside an audio block,
and starts at the region beginning when the transport lies outside the region.
The region follows the current project's tempo. Enabled regions must span at
least 64 samples at the active rate. Scheduled MIDI uses the existing seek/reset
and note-chase behavior at a wrap. Effects retain tails/state. No loop-boundary
crossfade is added, so discontinuous audio can click. Recording requires looping
to be disabled. Export retains its explicit linear range.
Validation covers settings roundtrips, malformed/duplicate/overflow/NUL input,
private permissions, save failure, GUI Apply/Close/reload and plugin listing,
plus JACK callback wrap positions and existing GUI/core regressions. Tests use
temporary configuration paths and do not write the user's configuration.

View file

@ -74,3 +74,8 @@ and trims, MIDI double-click editing, a piano velocity lane/slider and per-track
instrument dropdowns. Snap preferences are UI state; velocity and instrument
bindings use the existing v12 format. No new dependency or engine feature is
required. See WORKSPACE.md for supported divisions, controller semantics and limits.
Phase 35 adds Edit / Preferences with an Apply button and `~/.xtc.conf` INI
persistence, configurable plugin search paths and a file library, plus sequencer
ruler loop selection and sample-boundary playback wrapping. See PREFERENCES.md
for settings, reconnect behavior and looping limits.

View file

@ -989,6 +989,30 @@ patches control its audible response. See WORKSPACE.md and MIDI.md.
---
## Phase 35 — Application Preferences and Sequencer Looping
### Goal
Persist application preferences in `~/.xtc.conf` and provide selectable sequencer loops.
### Tasks and acceptance criteria
- [x] Add a flat, tabbed Edit / Preferences dialog with Apply and Close.
- [x] Validate and atomically save all preference tabs as a private INI file; load at startup.
- [x] Expose audio, interface, editing, new-project, plugin-path and transport settings.
- [x] List plugin files from configurable CLAP, VST2, VST3 and LADSPA search paths without executing modules.
- [x] Select a snapped loop range by Shift-dragging the ruler or entering tick bounds.
- [x] Toggle looping and split audio processing at exact sample boundaries.
- [x] Preserve real-time constraints and reject recording with looping enabled.
- [x] Verify persistence, invalid input, GUI Apply/reload, backend wraps and existing regressions.
### 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
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.
---
## 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

@ -106,6 +106,10 @@ xtc_result xtc_backend_request_stop(xtc_backend *backend);
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample);
/* Stopped-only loop [start,end) in samples. Enabled loops require >=64 samples
* and cannot be combined with recording. Callback splits at exact boundaries;
* live MIDI offsets remain relative to the JACK block. */
xtc_result xtc_backend_set_loop(xtc_backend *backend, uint64_t start, uint64_t end, int enabled);
/* Copies graph. While running, capacity means pending slot busy.
* While stopped, transactionally replaces all retained plans (counters/revisions
* restart); failed compilation leaves the previous engine intact. */

View file

@ -0,0 +1,37 @@
#ifndef XTC_SETTINGS_H
#define XTC_SETTINGS_H
#include "xtc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Control-thread application preferences; not project data or audio state. */
typedef struct {
char client_name[64], clap_paths[2048], vst2_paths[2048], vst3_paths[2048], ladspa_paths[2048];
int64_t max_frames, channels, transition_ms, offline_rate, recording_seconds;
int64_t width, height, piano_width, piano_height, refresh_ms;
int64_t sequencer_snap, piano_snap, sequencer_zoom;
int64_t clip_ticks, note_ticks, note_velocity, note_channel, filter_channel, filter_low, filter_high;
int64_t tempo, volume_cc, pan_cc, sustain_cc;
int64_t loop_start, loop_end, loop_enabled;
} xtc_settings;
typedef struct {
const char *section, *key, *label;
size_t offset, capacity; /* capacity > 0 means string; otherwise int64_t. */
int64_t minimum, maximum;
} xtc_setting_info;
const xtc_setting_info *xtc_settings_schema(size_t *count);
void xtc_settings_defaults(xtc_settings *settings);
xtc_result xtc_settings_validate(const xtc_settings *settings);
xtc_result xtc_settings_get(const xtc_settings *settings, size_t index, char *out, size_t size);
xtc_result xtc_settings_set(xtc_settings *settings, size_t index, const char *value);
xtc_result xtc_settings_path(char *out, size_t size);
/* Strict bounded INI. Missing file returns NOT_FOUND; failure retains destination.
* Save validates then writes a 0600 temporary, fsyncs and atomically renames it. */
xtc_result xtc_settings_load(xtc_settings *settings, const char *path);
xtc_result xtc_settings_save(const xtc_settings *settings, const char *path);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -29,6 +29,8 @@ struct xtc_backend {
unsigned int stop_requested, stop_done;
int stop_seen; /* Audio owner. */
uint64_t sample_position;
uint64_t loop_start, loop_end;
int loop_enabled;
unsigned int expected_rate;
unsigned int rate, frames, lost, rate_changed, xruns;
unsigned int process_result;
@ -45,22 +47,22 @@ static void publish_peak(unsigned int *slot, float peak)
}
/**
* @brief Decodes queued JACK MIDI and renders the instrument for the current audio block.
* @brief Decodes queued JACK MIDI once for the current audio block.
*
* @param backend Backend handle.
* @param position Absolute sample position at the start of the block.
* @param frames Number of audio frames.
* @return XTC_OK on success, or an error code describing the failure.
* @param events Caller-owned bounded event storage.
* @return Number of decoded events; malformed input requests a MIDI panic.
*/
static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nframes_t frames)
static size_t decode_midi(xtc_backend *backend, uint64_t position, jack_nframes_t frames, xtc_midi_event events[XTC_MIDI_MAX_EVENTS])
{
xtc_midi_event events[XTC_MIDI_MAX_EVENTS];
size_t count = 0;
uint32_t i, total;
unsigned int requested = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
void *buffer;
if (!backend->midi)
return XTC_OK;
return 0;
if (requested != backend->panic_seen) {
xtc_midi_plan_panic(backend->midi, position);
backend->panic_seen = requested;
@ -91,9 +93,7 @@ static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nfra
}
++count;
}
return xtc_midi_plan_render_stereo(backend->midi, position, events, count, backend->instrument,
backend->output_right ? backend->instrument + backend->max_frames : NULL,
(size_t)frames);
return count;
}
/**
@ -137,27 +137,50 @@ static int process(jack_nframes_t frames, void *context)
} else {
const float *input = jack_port_get_buffer(backend->input, frames);
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
(void)xtc_realtime_position(backend->engine,position);
if (backend->recorder)
xtc_recorder_push(backend->recorder, input, (size_t)frames);
{
size_t channel;
xtc_midi_event events[XTC_MIDI_MAX_EVENTS], segment_events[XTC_MIDI_MAX_EVENTS];
size_t count = decode_midi(backend,position,frames,events), offset = 0;
if (backend->recorder) xtc_recorder_push(backend->recorder,input,(size_t)frames);
while (offset < frames && result == XTC_OK) {
size_t n = (size_t)frames-offset, i, queued = 0, channel;
if (backend->loop_enabled) {
if (position < backend->loop_start || position >= backend->loop_end) position = backend->loop_start;
if (backend->loop_end-position < n) n = (size_t)(backend->loop_end-position);
}
if (position > UINT64_MAX-n) { result = XTC_ERR_CAPACITY; break; }
for (channel = 0; channel < backend->meter_count*2; ++channel)
memset(backend->monitor + channel*backend->max_frames, 0, (size_t)frames*sizeof(float));
memset(backend->monitor+channel*backend->max_frames,0,n*sizeof(float));
(void)xtc_realtime_position(backend->engine,position);
if (backend->audio)
result = xtc_audio_plan_render_stereo(backend->audio,position,backend->timeline,
right ? backend->timeline+backend->max_frames : NULL,n);
for (i = 0; i < count; ++i) if (events[i].offset >= offset && events[i].offset < offset+n) {
segment_events[queued] = events[i]; segment_events[queued++].offset -= (unsigned int)offset;
}
if (result == XTC_OK && backend->midi)
result = xtc_midi_plan_render_stereo(backend->midi,position,segment_events,queued,backend->instrument,
right ? backend->instrument+backend->max_frames : NULL,n);
if (result == XTC_OK)
result = xtc_realtime_process_stereo(backend->engine,input ? input+offset : NULL,
backend->audio ? backend->timeline : NULL,
backend->audio && right ? backend->timeline+backend->max_frames : NULL,
backend->midi ? backend->instrument : NULL,
backend->midi && right ? backend->instrument+backend->max_frames : NULL,
output+offset,right ? right+offset : NULL,n);
if (result == XTC_OK) {
size_t track, side, frame;
for (track = 0; track < backend->meter_count; ++track) for (side = 0; side < 2; ++side) {
float peak = 0;
for (frame = 0; frame < n; ++frame) {
float value = fabsf(backend->monitor[(track*2+side)*backend->max_frames+frame]);
if (isfinite(value) && value > peak) peak = value;
}
publish_peak(&backend->meter_peaks[track][side],peak);
}
}
offset += n; position += n;
if (backend->loop_enabled && position == backend->loop_end) position = backend->loop_start;
}
if (backend->audio)
result =
xtc_audio_plan_render_stereo(backend->audio, position, backend->timeline,
right ? backend->timeline + backend->max_frames : NULL, (size_t)frames);
if (result == XTC_OK)
result = render_midi(backend, position, frames);
if (result == XTC_OK)
result =
xtc_realtime_process_stereo(backend->engine, input, backend->audio ? backend->timeline : NULL,
backend->audio && right ? backend->timeline + backend->max_frames : NULL,
backend->midi ? backend->instrument : NULL,
backend->midi && right ? backend->instrument + backend->max_frames : NULL,
output, right, (size_t)frames);
if (result == XTC_OK) __atomic_store_n(&backend->sample_position,position,__ATOMIC_RELAXED);
}
if (result == XTC_OK && backend->transition_frames) {
if (__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE) && !backend->stop_seen) {
@ -168,13 +191,6 @@ static int process(jack_nframes_t frames, void *context)
if (backend->stop_seen && !backend->ramp.remaining)
__atomic_store_n(&backend->stop_done, 1U, __ATOMIC_RELEASE);
}
if (result == XTC_OK) {
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
if (position > UINT64_MAX - frames)
result = XTC_ERR_CAPACITY;
else
__atomic_store_n(&backend->sample_position, position + frames, __ATOMIC_RELAXED);
}
if (result != XTC_OK) {
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
if (backend->recorder)
@ -195,18 +211,6 @@ static int process(jack_nframes_t frames, void *context)
}
publish_peak(backend->master_peaks, left_peak);
publish_peak(backend->master_peaks+1, right_peak);
if (result == XTC_OK) {
size_t track, side, frame;
for (track = 0; track < backend->meter_count; ++track)
for (side = 0; side < 2; ++side) {
float peak = 0;
for (frame = 0; frame < frames; ++frame) {
float value = fabsf(backend->monitor[(track*2+side)*backend->max_frames+frame]);
if (isfinite(value) && value > peak) peak = value;
}
publish_peak(&backend->meter_peaks[track][side], peak);
}
}
__atomic_store_n(&backend->peak_left, (unsigned int)(fminf(left_peak, 1000) * 1000000), __ATOMIC_RELAXED);
__atomic_store_n(&backend->peak_right, (unsigned int)(fminf(right_peak, 1000) * 1000000), __ATOMIC_RELAXED);
}
@ -592,6 +596,7 @@ const char *xtc_backend_asset_issue(const xtc_backend *backend)
*/
xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder)
{
if (backend && recorder && backend->loop_enabled) return XTC_ERR_INVALID_STATE;
if (!backend)
return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready || backend->running)
@ -639,6 +644,13 @@ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **o
}
/* Control-only configuration; applied to source plans on next preparation. */
xtc_result xtc_backend_set_loop(xtc_backend *backend, uint64_t start, uint64_t end, int enabled)
{
if (!backend || (enabled != 0 && enabled != 1) || (enabled && (end <= start || end-start < 64))) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready || backend->running || (enabled && backend->recorder)) return XTC_ERR_INVALID_STATE;
backend->loop_start = start; backend->loop_end = end; backend->loop_enabled = enabled; return XTC_OK;
}
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
{
if (!backend || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
@ -1099,6 +1111,12 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc
(void)remote;
return XTC_ERR_UNSUPPORTED;
}
xtc_result xtc_backend_set_loop(xtc_backend *backend, uint64_t start, uint64_t end, int enabled)
{
(void)start; (void)end; (void)enabled;
return backend ? XTC_ERR_UNSUPPORTED : XTC_ERR_INVALID_ARGUMENT;
}
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
{
(void)backend; (void)frames;

View file

@ -7,6 +7,9 @@
#include "theme.hpp"
#include "snap.hpp"
#include "velocity_lane.hpp"
#include "preferences_dialog.hpp"
#include "plugin_library.hpp"
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Value_Slider.H>
#include <FL/Fl_Native_File_Chooser.H>
@ -64,14 +67,21 @@ class BusyOperation {
* @param prompt_unsaved Whether open, new, and close actions ask before
* discarding unsaved changes.
*/
MainWindow::MainWindow(bool prompt_unsaved)
MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
: Fl_Double_Window(1280, 800, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved)
{
xtc_ui::initialize();
xtc_settings_defaults(&settings_);
char config_path[4096];
if(configuration) configuration_=configuration;
else if(xtc_settings_path(config_path,sizeof(config_path))==XTC_OK) configuration_=config_path;
xtc_result config_result=configuration_.empty() ? XTC_ERR_NOT_FOUND : xtc_settings_load(&settings_,configuration_.c_str());
xtc_project *initial = nullptr;
xtc_result result = xtc_project_create(&initial);
if (result != XTC_OK)
throw std::runtime_error(xtc_result_string(result));
project_defaults(initial);
project_.reset(initial);
xtc_history *history = nullptr;
result = xtc_history_create(initial, &history);
@ -87,6 +97,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("File/Save As", FL_CTRL + FL_SHIFT + 's', file_callback, this);
menu->add("File/Import WAV", FL_CTRL + 'i', audio_file_callback, this);
menu->add("File/Relink selected audio", 0, audio_file_callback, this);
menu->add("Edit/Preferences",FL_CTRL+',',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->show_preferences();},this);
menu->add("Edit/Undo", FL_CTRL + 'z', history_callback, this);
menu->add("Edit/Redo", FL_CTRL + FL_SHIFT + 'z', history_callback, this);
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
@ -150,7 +161,20 @@ MainWindow::MainWindow(bool prompt_unsaved)
transport_position_->tooltip("Musical position: bar : beat : tick (4/4, 960 ticks per beat)");
tempo_display_ = new Fl_Box(880,40,110,30,"120.0 BPM");
auto *hint = new Fl_Box(12,82,1256,28,"xtc / ARRANGEMENT Create tracks, arrange clips and shape your sound.");
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); hint->resize(12,82,970,28);
loop_button_=new Fl_Check_Button(1000,82,100,28,"Loop");
loop_button_->callback([](Fl_Widget *widget,void *c){auto *w=static_cast<MainWindow *>(c);
xtc_result r=w->set_loop(static_cast<uint64_t>(w->settings_.loop_start),static_cast<uint64_t>(w->settings_.loop_end),static_cast<Fl_Check_Button *>(widget)->value()!=0);
if(r!=XTC_OK) w->report_error("Loop unchanged",r);
w->refresh_status();},this);
auto *range=new Fl_Button(1110,82,150,28,"Loop range...");
range->tooltip("Shift-drag the sequencer ruler to select a loop, or enter start/end ticks here.");
range->callback([](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);char initial[100];
std::snprintf(initial,sizeof(initial),"%lld %lld",static_cast<long long>(w->settings_.loop_start),static_cast<long long>(w->settings_.loop_end));
const char *text=fl_input("Loop start and end ticks (end excluded)",initial);if(!text)return;
unsigned long long start,end;char extra;xtc_result r=XTC_ERR_INVALID_ARGUMENT;
if(std::sscanf(text,"%llu %llu %c",&start,&end,&extra)==2)r=w->set_loop(start,end,w->settings_.loop_enabled!=0);
if(r!=XTC_OK)w->report_error("Loop unchanged",r);},this);
tabs_ = new Fl_Tabs(216, 126, 1056, 578);
tabs_->begin();
@ -173,6 +197,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this);
timeline_scroll->end();
sequencer_->before_edit = before_arrangement_edit;
sequencer_->loop_select=[](void *c,uint64_t start,uint64_t end){auto *w=static_cast<MainWindow *>(c);return w->set_loop(start,end,w->settings_.loop_enabled!=0);};
sequencer_->open_clip = [](void *c) { piano_callback(nullptr,c); };
sequencer_->instrument_select = [](void *c, uint64_t track, const xtc_plugin_spec *spec, bool browse) {
auto *w = static_cast<MainWindow *>(c);
@ -361,8 +386,10 @@ MainWindow::MainWindow(bool prompt_unsaved)
resizable(tabs_);
size_range(1220, 720);
callback(close_callback, this);
apply_ui_preferences();
open_backend();
Fl::add_timeout(0.1, timer_callback, this);
if(config_result!=XTC_OK && config_result!=XTC_ERR_NOT_FOUND) report_error("Preferences file invalid; using defaults",config_result);
Fl::add_timeout(static_cast<double>(settings_.refresh_ms)/1000,timer_callback,this);
}
void MainWindow::draw()
@ -472,7 +499,7 @@ xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, siz
*/
void MainWindow::open_backend()
{
xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 2U};
xtc_backend_config config = {XTC_BACKEND_JACK, settings_.client_name, static_cast<size_t>(settings_.max_frames), static_cast<size_t>(settings_.channels)};
xtc_result result = xtc_backend_open(&config, &backend_);
ready_ = result == XTC_OK;
running_ = false;
@ -482,7 +509,7 @@ void MainWindow::open_backend()
xtc_backend_status format{};
result = xtc_backend_get_status(backend_, &format);
if (result == XTC_OK)
result = xtc_backend_set_transition_frames(backend_, format.sample_rate / 200U);
result = xtc_backend_set_transition_frames(backend_, static_cast<size_t>(format.sample_rate)*static_cast<size_t>(settings_.transition_ms)/1000U);
if (result == XTC_OK)
result = xtc_backend_submit(backend_, xtc_project_graph(project_.get()));
if (result != XTC_OK) {
@ -509,7 +536,8 @@ void MainWindow::start_engine()
}
if (running_)
return;
xtc_result result = xtc_backend_prepare_project(backend_, project_.get());
xtc_result result = configure_loop(project_.get(),settings_);
if(result==XTC_OK) result = xtc_backend_prepare_project(backend_, project_.get());
if (result == XTC_OK)
result = xtc_backend_start(backend_);
if (result != XTC_OK)
@ -849,7 +877,7 @@ void MainWindow::timer_callback(void *context)
}
window->refresh_status();
if (!window->closing_)
Fl::repeat_timeout(0.1, timer_callback, context);
Fl::repeat_timeout(static_cast<double>(window->settings_.refresh_ms)/1000,timer_callback,context);
}
/**
@ -965,19 +993,20 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
r = xtc_backend_stop(backend_);
if (r == XTC_OK)
r = xtc_backend_seek(backend_, 0);
if(r==XTC_OK) r=configure_loop(candidate,settings_);
if (r == XTC_OK)
r = xtc_backend_prepare_project(backend_, candidate);
}
xtc_audio_plan *offline = nullptr;
if (r == XTC_OK && !ready_)
r = xtc_audio_plan_create(candidate, 48000, &offline);
r = xtc_audio_plan_create(candidate, static_cast<unsigned int>(settings_.offline_rate), &offline);
xtc_midi_plan *offline_midi = nullptr;
if (r == XTC_OK && !ready_)
r = xtc_midi_plan_create(candidate, 48000, &offline_midi);
r = xtc_midi_plan_create(candidate, static_cast<unsigned int>(settings_.offline_rate), &offline_midi);
xtc_midi_plan_destroy(offline_midi);
if (r == XTC_OK && !ready_) {
xtc_plan *graph = nullptr;
r = xtc_graph_compile(xtc_project_graph(candidate),48000,256,&graph);
r = xtc_graph_compile(xtc_project_graph(candidate),static_cast<double>(settings_.offline_rate),256,&graph);
xtc_plan_destroy(graph);
}
if (r != XTC_OK) {
@ -1054,7 +1083,7 @@ xtc_result MainWindow::open_project(const char *path)
xtc_node_info node{}; xtc_plugin_spec spec{};
if (xtc_graph_get_node(graph,i,&node) != XTC_OK || node.type != XTC_NODE_PLUGIN ||
xtc_graph_get_plugin(graph,node.id,&spec) != XTC_OK) continue;
double rate = 48000; xtc_backend_status status{};
double rate = sample_rate(); xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin *check = nullptr;
xtc_result plugin_result = xtc_plugin_create(&spec,rate,256,&check);
@ -1081,6 +1110,7 @@ xtc_result MainWindow::new_project()
return XTC_ERR_INVALID_STATE;
xtc_project *candidate = nullptr;
xtc_result result = xtc_project_create(&candidate);
if(result==XTC_OK) project_defaults(candidate);
if (result == XTC_OK)
result = install_project(candidate);
if (result != XTC_OK)
@ -1217,7 +1247,7 @@ xtc_result MainWindow::import_audio(const char *path)
*/
xtc_result MainWindow::record_audio(const char *path)
{
if (!ready_ || recorder_)
if (!ready_ || recorder_ || settings_.loop_enabled)
return XTC_ERR_INVALID_STATE;
xtc_result r = xtc_backend_stop(backend_);
xtc_backend_status status{};
@ -1228,7 +1258,7 @@ xtc_result MainWindow::record_audio(const char *path)
if (r == XTC_OK)
r = xtc_backend_prepare_project(backend_, project_.get());
if (r == XTC_OK)
r = xtc_recorder_create(path, status.sample_rate, static_cast<size_t>(status.sample_rate) * 2, &recorder_);
r = xtc_recorder_create(path, status.sample_rate, static_cast<size_t>(status.sample_rate) * static_cast<size_t>(settings_.recording_seconds), &recorder_);
if (r != XTC_OK)
return r;
recording_path_ = path;
@ -1406,6 +1436,9 @@ xtc_result MainWindow::open_piano_roll()
xtc_ui::style(piano_window_.get());
piano_window_->resizable(scroll);
scroll->scroll_to(0, 60 * 16);
settings_.width=w();settings_.height=h();
settings_.sequencer_snap=static_cast<int64_t>(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level();
apply_ui_preferences();
} else
piano_->set_project(project_.get(), clip.id);
refresh_workspace();
@ -1525,7 +1558,7 @@ xtc_result MainWindow::create_track(bool midi)
uint64_t track = UINT64_MAX, clip = UINT64_MAX;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_add_track(candidate,midi ? "MIDI track" : "Audio track",&track);
if (r == XTC_OK && midi) r = xtc_project_add_midi_clip(candidate,track,position,3840,&clip);
if (r == XTC_OK && midi) r = xtc_project_add_midi_clip(candidate,track,position,static_cast<uint64_t>(settings_.clip_ticks),&clip);
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate);
@ -1537,6 +1570,7 @@ xtc_result MainWindow::create_track(bool midi)
}
void MainWindow::refresh_workspace()
{
if(loop_button_) {loop_button_->value(settings_.loop_enabled!=0);if(recorder_)loop_button_->deactivate();else loop_button_->activate();}
if (snap_) snap_->value(xtc_ui::snap_index(sequencer_->grid()));
if (piano_) {
piano_snap_->value(xtc_ui::snap_index(piano_->grid()));
@ -1579,7 +1613,7 @@ xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsig
if (recorder_) return XTC_ERR_INVALID_STATE;
xtc_project *candidate = nullptr;
xtc_plugin_spec spec{};
xtc_result r = path ? xtc_plugin_probe(path,index,48000,&spec) : XTC_OK;
xtc_result r = path ? xtc_plugin_probe(path,index,sample_rate(),&spec) : XTC_OK;
if (r == XTC_OK && path && !spec.instrument) return XTC_ERR_UNSUPPORTED;
if (r == XTC_OK) r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_set_instrument(candidate,track,path ? &spec : nullptr);
@ -1611,13 +1645,10 @@ void MainWindow::instrument_callback(Fl_Widget *widget, void *context)
else if (std::strcmp(label,"Live MIDI to selected instrument") == 0) r = w->select_live_instrument(track);
else if (std::strcmp(label,"Live MIDI to built-in sine") == 0) r = w->select_live_instrument(UINT64_MAX);
else {
Fl_Native_File_Chooser chooser;
chooser.title("Choose trusted mono instrument binary for selected track");
chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if (chooser.show() != 0 || !chooser.filename()) return;
unsigned int index = 0;
if (!xtc_choose_plugin(chooser.filename(),48000,index,true)) return;
r = w->assign_instrument(track,chooser.filename(),index);
std::string module; if(!xtc_choose_plugin_file(w->settings_,module)) return;
unsigned int index=0;
if(!xtc_choose_plugin(module.c_str(),w->sample_rate(),index,true))return;
r=w->assign_instrument(track,module.c_str(),index);
}
if (r != XTC_OK) w->report_error("Instrument assignment failed; select a non-bus track and compatible mono instrument",r);
}
@ -1668,7 +1699,7 @@ xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *
{
BusyOperation busy(*this,status_,"Loading plugin and preparing playback…");
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000;
double rate = sample_rate();
xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_spec spec{};
@ -1689,7 +1720,7 @@ xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int
{
BusyOperation busy(*this,status_,"Replacing plugin and preparing playback…");
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000; xtc_backend_status status{};
double rate = sample_rate(); xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_spec spec{};
xtc_result r = xtc_plugin_probe(path,index,rate,&spec);
@ -1700,7 +1731,7 @@ xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int
xtc_result MainWindow::reset_plugin(size_t id, unsigned int port)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000; xtc_backend_status status{};
double rate = sample_rate(); xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_spec spec{};
xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
@ -1710,7 +1741,7 @@ xtc_result MainWindow::reset_plugin(size_t id, unsigned int port)
xtc_result MainWindow::insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000; xtc_backend_status status{};
double rate = sample_rate(); xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_spec spec{};
xtc_result r = xtc_plugin_probe(path,index,rate,&spec);
@ -1744,7 +1775,7 @@ xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float va
xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000; xtc_backend_status status{};
double rate = sample_rate(); xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
@ -1762,7 +1793,7 @@ xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec,
if (r == XTC_OK) r = replace ? xtc_graph_replace_plugin(graph,id,&spec) : xtc_graph_set_plugin(graph,id,&spec);
/* Compile before adoption even with a mocked/disconnected backend. */
xtc_plan *check = nullptr;
xtc_backend_status status{}; double rate = 48000;
xtc_backend_status status{}; double rate = sample_rate();
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check);
xtc_plan_destroy(check);
@ -1793,28 +1824,26 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),selected,&old);
if (r != XTC_OK) { w->report_error("Select a plugin node to replace",r); return; }
}
Fl_Native_File_Chooser chooser;
chooser.title("Choose trusted plugin binary (LADSPA, CLAP, VST2, VST3)"); chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if (chooser.show() != 0 || !chooser.filename()) return;
double rate = 48000; xtc_backend_status status{};
std::string module; if(!xtc_choose_plugin_file(w->settings_,module))return;
double rate = w->sample_rate(); xtc_backend_status status{};
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
unsigned int index = 0;
xtc_plugin_spec probe{};
r = xtc_plugin_probe(chooser.filename(),0,rate,&probe);
r = xtc_plugin_probe(module.c_str(),0,rate,&probe);
if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) {
w->report_error("Cannot discover plugin; check binary path, format, mono layout and dependencies",r); return;
}
if (!xtc_choose_plugin(chooser.filename(),rate,index)) return;
if (replace) r = w->replace_plugin(selected,chooser.filename(),index);
else if (insert) r = w->insert_plugin(wire,chooser.filename(),index);
else r = w->add_plugin(chooser.filename(),index);
if (!xtc_choose_plugin(module.c_str(),rate,index)) return;
if (replace) r = w->replace_plugin(selected,module.c_str(),index);
else if (insert) r = w->insert_plugin(wire,module.c_str(),index);
else r = w->add_plugin(module.c_str(),index);
} else {
size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{};
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec);
if (r == XTC_OK) {
std::string prompt = std::string(spec.label) + " control ports and values:\n";
if (std::strcmp(label,"Selected plugin control") == 0) {
double rate = 48000; xtc_backend_status status{};
double rate = w->sample_rate(); xtc_backend_status status{};
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_details details{}; xtc_plugin_spec edited{};
r = xtc_plugin_describe(&spec,rate,&details);
@ -1823,7 +1852,7 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
return;
}
xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};
double rate = 48000; xtc_backend_status status{};
double rate = w->sample_rate(); xtc_backend_status status{};
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
r = xtc_plugin_parameters(&spec,rate,info);
if (r != XTC_OK) { w->report_error("Read plugin controls failed",r); return; }
@ -1849,3 +1878,71 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
}
if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r);
}
void MainWindow::project_defaults(xtc_project *project)
{
(void)xtc_project_set_tempo(project,static_cast<unsigned int>(settings_.tempo));
unsigned int cc[3]={static_cast<unsigned int>(settings_.volume_cc),static_cast<unsigned int>(settings_.pan_cc),static_cast<unsigned int>(settings_.sustain_cc)};
for(unsigned int channel=0;channel<16;++channel)(void)xtc_project_set_control_map(project,channel,cc);
}
double MainWindow::sample_rate() const
{
xtc_backend_status status{};
return ready_ && xtc_backend_get_status(backend_,&status)==XTC_OK ? status.sample_rate : static_cast<double>(settings_.offline_rate);
}
xtc_result MainWindow::configure_loop(const xtc_project *project,const xtc_settings &settings)
{
if(!ready_)return XTC_OK;
uint64_t start,end;unsigned int rate=static_cast<unsigned int>(sample_rate());
xtc_result r=xtc_project_tick_to_sample(project,static_cast<uint64_t>(settings.loop_start),rate,&start);
if(r==XTC_OK)r=xtc_project_tick_to_sample(project,static_cast<uint64_t>(settings.loop_end),rate,&end);
if(r==XTC_OK)r=xtc_backend_set_loop(backend_,start,end,settings.loop_enabled!=0);
return r;
}
xtc_result MainWindow::set_loop(uint64_t start,uint64_t end,bool enabled)
{
if(recorder_)return XTC_ERR_INVALID_STATE;
if(start>=end || end>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
xtc_settings candidate=settings_;candidate.loop_start=static_cast<int64_t>(start);candidate.loop_end=static_cast<int64_t>(end);candidate.loop_enabled=enabled;
bool resume=running_;xtc_result r=ready_ ? xtc_backend_stop(backend_) : XTC_OK;
if(r==XTC_OK)r=configure_loop(project_.get(),candidate);
if(r==XTC_OK) {settings_=candidate;sequencer_->loop_region(start,end,enabled);}
if(ready_ && resume && r==XTC_OK) {xtc_result restarted=xtc_backend_start(backend_);if(r==XTC_OK)r=restarted;}
refresh_status();return r;
}
void MainWindow::collect_preferences()
{
settings_.width=w();settings_.height=h();settings_.sequencer_snap=static_cast<int64_t>(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level();
if(piano_) {
settings_.piano_width=piano_window_->w();settings_.piano_height=piano_window_->h();settings_.piano_snap=static_cast<int64_t>(piano_->grid());
settings_.filter_channel=piano_->filter_channel();settings_.filter_low=piano_->filter_low();settings_.filter_high=piano_->filter_high();
}
}
void MainWindow::apply_ui_preferences()
{
resize(x(),y(),static_cast<int>(settings_.width),static_cast<int>(settings_.height));
sequencer_->grid(static_cast<uint64_t>(settings_.sequencer_snap));sequencer_->set_zoom(static_cast<int>(settings_.sequencer_zoom));
sequencer_->new_clip_ticks=static_cast<uint64_t>(settings_.clip_ticks);
sequencer_->loop_region(static_cast<uint64_t>(settings_.loop_start),static_cast<uint64_t>(settings_.loop_end),settings_.loop_enabled!=0);
if(piano_) {
piano_window_->resize(piano_window_->x(),piano_window_->y(),static_cast<int>(settings_.piano_width),static_cast<int>(settings_.piano_height));
piano_->set_grid(static_cast<uint64_t>(settings_.piano_snap));piano_->set_filter(static_cast<int>(settings_.filter_channel),static_cast<unsigned int>(settings_.filter_low),static_cast<unsigned int>(settings_.filter_high));
piano_->new_note_ticks=static_cast<uint64_t>(settings_.note_ticks);piano_->new_note_velocity=static_cast<unsigned int>(settings_.note_velocity);piano_->new_note_channel=static_cast<unsigned int>(settings_.note_channel);
}
}
xtc_result MainWindow::apply_preferences(const xtc_settings &candidate)
{
if(recorder_)return XTC_ERR_INVALID_STATE;
xtc_result r=xtc_settings_validate(&candidate);if(r!=XTC_OK)return r;
if(configuration_.empty())return XTC_ERR_INVALID_STATE;
if(ready_)r=xtc_backend_stop(backend_);
if(r==XTC_OK)r=configure_loop(project_.get(),candidate);
if(r==XTC_OK)r=xtc_settings_save(&candidate,configuration_.c_str());
if(r!=XTC_OK) {if(ready_)(void)configure_loop(project_.get(),settings_);refresh_status();return r;}
settings_=candidate;apply_ui_preferences();refresh_status();return XTC_OK;
}
void MainWindow::show_preferences()
{
if(recorder_) {report_error("Stop recording before preferences",XTC_ERR_INVALID_STATE);return;}
collect_preferences();PreferencesDialog dialog(settings_,configuration_,[this](const xtc_settings &s){return apply_preferences(s);});dialog.run();
}

View file

@ -9,6 +9,7 @@
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_control.h"
#include "xtc/xtc_settings.h"
class GraphWidget;
class MixerWidget;
@ -20,6 +21,7 @@ class Fl_Tabs;
class Fl_Menu_Bar;
class Fl_Choice;
class Fl_Value_Slider;
class Fl_Check_Button;
class MainWindow : public Fl_Double_Window {
public:
@ -27,6 +29,10 @@ class MainWindow : public Fl_Double_Window {
* Preparation stops/resets transport; recording and failed candidates retain
* the current document. No engine/model changes happen in audio callbacks. */
xtc_result create_track(bool midi);
const xtc_settings &settings() const { return settings_; }
xtc_result apply_preferences(const xtc_settings &settings);
void show_preferences();
xtc_result set_loop(uint64_t start,uint64_t end,bool enabled);
Fl_Widget *velocity_lane() const { return velocity_lane_; }
/* Control-thread plugin commands, transactional model adoption. */
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
@ -49,7 +55,7 @@ class MainWindow : public Fl_Double_Window {
* @param prompt_unsaved Whether open, new, and close actions ask before
* discarding unsaved changes.
*/
explicit MainWindow(bool prompt_unsaved = true);
explicit MainWindow(bool prompt_unsaved = true, const char *configuration = nullptr);
/**
* @brief Removes the status timer and releases backend and recording state.
*
@ -178,6 +184,14 @@ class MainWindow : public Fl_Double_Window {
xtc_result return_to_start();
private:
xtc_settings settings_{};
std::string configuration_;
Fl_Check_Button *loop_button_=nullptr;
xtc_result configure_loop(const xtc_project *project,const xtc_settings &settings);
void apply_ui_preferences();
void collect_preferences();
void project_defaults(xtc_project *project);
double sample_rate() const;
void draw() override;
void refresh_workspace();
Fl_Choice *snap_ = nullptr, *piano_snap_ = nullptr;

View file

@ -282,8 +282,8 @@ int PianoRollWidget::handle(int event)
uint64_t start = tick(Fl::event_x());
if (pitch >= static_cast<int>(low_) && pitch <= static_cast<int>(high_) && xtc_project_find_clip(project_, clip_, &c) == XTC_OK &&
start < c.source_length_tick)
add_note(start, std::min<uint64_t>(960, c.source_length_tick - start),
static_cast<unsigned int>(pitch), 100, channel_filter_ < 0 ? 0U : static_cast<unsigned int>(channel_filter_));
add_note(start, std::min<uint64_t>(new_note_ticks, c.source_length_tick - start),
static_cast<unsigned int>(pitch), new_note_velocity, channel_filter_ < 0 ? new_note_channel : static_cast<unsigned int>(channel_filter_));
}
redraw();
return 1;

View file

@ -72,6 +72,11 @@ class PianoRollWidget : public Fl_Widget {
xtc_project *project() const { return project_; }
bool note_visible(const xtc_note_info &note) const { return visible(note); }
int note_x(uint64_t tick) const { return x()+64+static_cast<int>(tick*96/960); }
uint64_t new_note_ticks=960;
unsigned int new_note_velocity=100,new_note_channel=0;
int filter_channel() const { return channel_filter_; }
unsigned int filter_low() const { return low_; }
unsigned int filter_high() const { return high_; }
uint64_t grid() const { return grid_; }
xtc_result set_velocity(uint64_t note, unsigned int velocity);
uint64_t selected_note() const { return selected_; }

View file

@ -0,0 +1,74 @@
#ifndef XTC_PLUGIN_LIBRARY_HPP
#define XTC_PLUGIN_LIBRARY_HPP
#include "xtc/xtc_settings.h"
#include "theme.hpp"
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Native_File_Chooser.H>
#include <filesystem>
#include <set>
#include <vector>
#include <string>
inline std::vector<std::string> xtc_plugin_files(const xtc_settings &settings)
{
namespace fs=std::filesystem;
std::set<std::string> files;
size_t visited=0;
for(const char *list : {settings.clap_paths,settings.vst2_paths,settings.vst3_paths,settings.ladspa_paths}) {
std::string paths=list; size_t start=0;
while(start<=paths.size()) {
size_t end=paths.find(':',start); std::string root=paths.substr(start,end==std::string::npos ? end : end-start);
if(root.rfind("~/",0)==0) { char config[4096]; if(xtc_settings_path(config,sizeof(config))==XTC_OK) {
std::string home=config; home.resize(home.size()-10); root=home+root.substr(1);
} }
std::error_code error;
if(!root.empty() && fs::path(root).is_absolute()) {
fs::recursive_directory_iterator it(root,fs::directory_options::skip_permission_denied,error), finish;
while(!error && it!=finish && visited<4096 && files.size()<512) {
++visited;
if(it.depth()>=6) it.disable_recursion_pending();
if(it->is_regular_file(error)) {
auto extension=it->path().extension().string();
if(extension==".so" || extension==".clap") files.insert(it->path().lexically_normal().string());
}
error.clear(); it.increment(error);
}
}
if(end==std::string::npos) break;
start=end+1;
}
}
return {files.begin(),files.end()};
}
inline bool xtc_choose_plugin_file(const xtc_settings &settings,std::string &path)
{
struct Library : Fl_Double_Window {
bool accepted=false,browse=false;
Library():Fl_Double_Window(900,520,"Plugin library — trusted binaries") {set_modal();}
} dialog;
auto files=xtc_plugin_files(settings);
auto *hint=new Fl_Box(16,10,868,44,"Files from Preferences search paths. Select a trusted binary to inspect its plugins.\nNo module code is executed while listing. Browse can select another binary.");
hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
auto *list=new Fl_Hold_Browser(16,62,868,370); list->format_char(0);
for(const auto &file:files) list->add(file.c_str());
if(!files.empty()) list->select(1);
auto *browse=new Fl_Button(16,462,130,32,"Browse files...");
browse->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->browse=true;d->hide();},&dialog);
auto *cancel=new Fl_Button(650,462,100,32,"Cancel");
cancel->callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},&dialog);
auto *choose=new Fl_Button(764,462,120,32,"Inspect plugin");
choose->callback([](Fl_Widget *,void *c){auto *d=static_cast<Library *>(c);d->accepted=true;d->hide();},&dialog);
if(files.empty()) choose->deactivate();
dialog.callback([](Fl_Widget *,void *c){static_cast<Library *>(c)->hide();},&dialog);
dialog.end(); xtc_ui::style(&dialog); dialog.show(); while(dialog.shown()) Fl::wait();
if(dialog.browse) {
Fl_Native_File_Chooser chooser; chooser.title("Choose trusted plugin binary"); chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if(chooser.show()!=0 || !chooser.filename()) return false;
path=chooser.filename(); return true;
}
if(!dialog.accepted || list->value()<1) return false;
path=files[static_cast<size_t>(list->value()-1)]; return true;
}
#endif

View file

@ -0,0 +1,69 @@
#ifndef XTC_PREFERENCES_DIALOG_HPP
#define XTC_PREFERENCES_DIALOG_HPP
#include "xtc/xtc_settings.h"
#include "theme.hpp"
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <functional>
#include <vector>
#include <string>
class PreferencesDialog : public Fl_Double_Window {
xtc_settings original_;
std::function<xtc_result(const xtc_settings &)> apply_;
std::vector<Fl_Input *> inputs_;
Fl_Box *message_;
static void apply_callback(Fl_Widget *,void *context) {
auto *d=static_cast<PreferencesDialog *>(context);
xtc_settings candidate=d->original_; size_t count=0;
const xtc_setting_info *schema=xtc_settings_schema(&count);
for(size_t i=0;i<count;++i) if(xtc_settings_set(&candidate,i,d->inputs_[i]->value())!=XTC_OK) {
std::string text=std::string("Invalid value: ")+schema[i].label;
d->message_->copy_label(text.c_str()); return;
}
xtc_result r=xtc_settings_validate(&candidate);
if(r==XTC_OK) r=d->apply_(candidate);
if(r==XTC_OK) {
d->original_=candidate;
d->message_->copy_label("Saved and applied. Audio options take effect on Reconnect; project defaults on New.");
} else {
std::string text=std::string("Preferences unchanged: ")+xtc_result_string(r)+". Check values, loop range and configuration file permissions.";
d->message_->copy_label(text.c_str());
}
d->redraw();
}
public:
PreferencesDialog(const xtc_settings &settings,const std::string &path,
std::function<xtc_result(const xtc_settings &)> apply)
: Fl_Double_Window(1050,660,"xtc — Preferences"),original_(settings),apply_(std::move(apply)) {
set_modal(); begin();
auto *location=new Fl_Box(16,12,1018,30); location->copy_label(path.c_str()); location->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
auto *tabs=new Fl_Tabs(12,50,1026,510); tabs->begin();
size_t count=0; const auto *schema=xtc_settings_schema(&count);
inputs_.resize(count); std::string section; Fl_Group *group=nullptr; Fl_Scroll *scroll=nullptr; int row=0;
for(size_t i=0;i<count;++i) {
if(section!=schema[i].section) {
if(scroll) { scroll->end(); group->end(); }
section=schema[i].section; tabs->begin();
group=new Fl_Group(12,82,1026,478); group->copy_label(section.c_str()); group->begin();
scroll=new Fl_Scroll(22,94,1000,450); scroll->begin(); row=0;
}
auto *input=new Fl_Input(510,108+row*48,480,30,schema[i].label);
input->align(FL_ALIGN_LEFT); input->maximum_size(schema[i].capacity ? static_cast<int>(schema[i].capacity-1) : 22);
char value[4096]; xtc_settings_get(&settings,i,value,sizeof(value)); input->value(value);
inputs_[i]=input; ++row;
}
if(scroll) { scroll->end(); group->end(); } tabs->end();
message_=new Fl_Box(16,568,1018,36,"Apply saves every tab. Existing project contents are not overwritten by defaults.");
message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
auto *apply_button=new Fl_Button(812,614,100,30,"Apply"); apply_button->callback(apply_callback,this);
auto *close=new Fl_Button(924,614,110,30,"Close");
close->callback([](Fl_Widget *,void *c){static_cast<PreferencesDialog *>(c)->hide();},this);
callback([](Fl_Widget *,void *c){static_cast<PreferencesDialog *>(c)->hide();},this);
end(); xtc_ui::style(this);
}
void run() { show(); while(shown()) Fl::wait(); }
};
#endif

View file

@ -161,7 +161,7 @@ xtc_result SequencerWidget::add_region()
track_ = t.id;
}
uint64_t id;
xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, 3840U, &id);
xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id);
if (r == XTC_OK)
clip_ = id;
changed(r);
@ -183,7 +183,7 @@ xtc_result SequencerWidget::add_midi_clip()
track_ = r == XTC_OK ? t.id : UINT64_MAX;
}
if (r == XTC_OK)
r = xtc_project_add_midi_clip(project_, track_, cursor_, 3840, &id);
r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id);
if (r == XTC_OK)
clip_ = id;
}
@ -321,6 +321,14 @@ void SequencerWidget::draw()
fl_draw("Create a MIDI track or import audio from the panel on the left.",x()+220,y()+135);
fl_draw("Arrange clips here. Open the piano roll to draw and edit MIDI notes.",x()+220,y()+161);
}
{
uint64_t begin=mode_==4 ? std::min(loop_anchor_,loop_preview_) : loop_begin_;
uint64_t end=mode_==4 ? std::max(loop_anchor_,loop_preview_) : loop_end_;
int left=x()+180+static_cast<int>(begin*static_cast<unsigned int>(scale_)/960);
int right=x()+180+static_cast<int>(end*static_cast<unsigned int>(scale_)/960);
fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted());
fl_rectf(left,y()+23,std::max(0,right-left),4);
}
int playhead = x()+180+static_cast<int>(cursor_*static_cast<unsigned int>(scale_)/960U);
if (playhead >= cx && playhead <= cx+cw) {
fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h());
@ -348,6 +356,10 @@ int SequencerWidget::handle(int event)
if (event == FL_PUSH && (Fl::event_button() == FL_LEFT_MOUSE || Fl::event_button() == FL_RIGHT_MOUSE)) {
take_focus();
mode_ = 0;
if (Fl::event_y() < y() + 28 && Fl::event_x() >= x()+180 &&
Fl::event_button()==FL_LEFT_MOUSE && Fl::event_state(FL_SHIFT)) {
loop_anchor_=loop_preview_=tick(Fl::event_x()); mode_=4; redraw(); return 1;
}
if (Fl::event_y() < y() + 28) {
uint64_t target = tick(Fl::event_x());
xtc_result r = seek_(context_, target);
@ -400,6 +412,16 @@ int SequencerWidget::handle(int event)
redraw();
return 1;
}
if (event==FL_DRAG && mode_==4) {loop_preview_=tick(Fl::event_x());redraw();return 1;}
if (event==FL_RELEASE && mode_==4) {
loop_preview_=tick(Fl::event_x()); mode_=0;
uint64_t begin=std::min(loop_anchor_,loop_preview_),end=std::max(loop_anchor_,loop_preview_);
if(end>begin && loop_select) {
xtc_result r=loop_select(context_,begin,end);
if(r!=XTC_OK) fl_alert("Loop unchanged: %s",xtc_result_string(r));
}
redraw();return 1;
}
if (event == FL_DRAG)
return mode_ != 0;
if (event == FL_RELEASE && mode_) {
@ -653,3 +675,9 @@ void SequencerWidget::instrument_callback(Fl_Widget *widget, void *context)
owner->refresh_instruments(); owner->redraw(); return;
}
}
xtc_result SequencerWidget::set_zoom(int scale)
{
if(scale!=24 && scale!=48 && scale!=96 && scale!=192 && scale!=384) return XTC_ERR_INVALID_ARGUMENT;
scale_=scale;extent();redraw();return XTC_OK;
}

View file

@ -16,6 +16,12 @@ class SequencerWidget : public Fl_Group {
*
* @return XTC_OK to allow the edit, or an error to cancel it.
*/
using LoopSelect = xtc_result (*)(void *,uint64_t,uint64_t);
LoopSelect loop_select = nullptr;
uint64_t new_clip_ticks = 3840;
int zoom_level() const { return scale_; }
xtc_result set_zoom(int scale);
void loop_region(uint64_t start,uint64_t end,bool enabled) { loop_begin_=start; loop_end_=end; loop_enabled_=enabled; redraw(); }
using OpenClip = void (*)(void *);
using InstrumentSelect = void (*)(void *, uint64_t, const xtc_plugin_spec *, bool);
OpenClip open_clip = nullptr;
@ -164,6 +170,8 @@ class SequencerWidget : public Fl_Group {
Seek seek_;
void *context_;
uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0, grid_ = 240;
uint64_t loop_begin_=0,loop_end_=3840,loop_anchor_=0,loop_preview_=0;
bool loop_enabled_=false;
int scale_ = 48, drag_x_ = 0, mode_ = 0;
xtc_clip_info original_{};
/**

173
src/settings/settings.c Normal file
View file

@ -0,0 +1,173 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_settings.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <inttypes.h>
#include <unistd.h>
#include <pwd.h>
#include <ctype.h>
#define NUM(section,field,label,min,max) {section,#field,label,offsetof(xtc_settings,field),0,min,max}
#define STR(section,field,label) {section,#field,label,offsetof(xtc_settings,field),sizeof(((xtc_settings *)0)->field),0,0}
static const xtc_setting_info schema[] = {
STR("audio",client_name,"JACK client name (Reconnect)"),
NUM("audio",max_frames,"Maximum block capacity (Reconnect)",32,65536),
NUM("audio",channels,"Output channels: 1 mono, 2 stereo (Reconnect)",1,2),
NUM("audio",transition_ms,"Start/stop and source fades, ms (Reconnect)",0,1000),
NUM("audio",offline_rate,"Offline preparation sample rate, Hz",8000,192000),
NUM("audio",recording_seconds,"Recording ring capacity, seconds (next take)",1,30),
STR("plugins",clap_paths,"CLAP search paths (colon-separated)"),
STR("plugins",vst2_paths,"VST2 search paths (colon-separated)"),
STR("plugins",vst3_paths,"VST3 search paths (colon-separated)"),
STR("plugins",ladspa_paths,"LADSPA search paths (colon-separated)"),
NUM("interface",width,"Main window width",1220,3840), NUM("interface",height,"Main window height",720,2160),
NUM("interface",piano_width,"Piano window width",800,3840), NUM("interface",piano_height,"Piano window height",520,2160),
NUM("interface",refresh_ms,"Display refresh interval, ms",20,1000),
NUM("editing",sequencer_snap,"Sequencer snap ticks (1 means Off)",1,3840),
NUM("editing",piano_snap,"Piano snap ticks (1 means Off)",1,3840),
NUM("editing",sequencer_zoom,"Sequencer pixels per beat: 24/48/96/192/384",24,384),
NUM("editing",clip_ticks,"New MIDI clip length, ticks",1,38400000),
NUM("editing",note_ticks,"New note length, ticks",1,38400000),
NUM("editing",note_velocity,"New note velocity",1,127), NUM("editing",note_channel,"New note channel",0,15),
NUM("editing",filter_channel,"Piano channel filter (-1 means all)",-1,15),
NUM("editing",filter_low,"Piano minimum pitch",0,127), NUM("editing",filter_high,"Piano maximum pitch",0,127),
NUM("project_defaults",tempo,"New project tempo, milli-BPM",20000,300000),
NUM("project_defaults",volume_cc,"New project volume CC (128 disables)",0,128),
NUM("project_defaults",pan_cc,"New project pan CC (128 disables)",0,128),
NUM("project_defaults",sustain_cc,"New project sustain CC (128 disables)",0,128),
NUM("transport",loop_start,"Loop start, ticks",0,38399999), NUM("transport",loop_end,"Loop end, ticks",1,38400000),
NUM("transport",loop_enabled,"Loop enabled: 0 or 1",0,1)
};
#define COUNT (sizeof(schema)/sizeof(schema[0]))
const xtc_setting_info *xtc_settings_schema(size_t *count) { if (count) *count = COUNT; return schema; }
void xtc_settings_defaults(xtc_settings *s)
{
if (!s) return;
memset(s,0,sizeof(*s)); strcpy(s->client_name,"xtc");
strcpy(s->clap_paths,"~/.clap:/usr/local/lib/clap:/usr/lib/clap");
strcpy(s->vst2_paths,"~/.vst:/usr/local/lib/vst:/usr/lib/vst");
strcpy(s->vst3_paths,"~/.vst3:/usr/local/lib/vst3:/usr/lib/vst3");
strcpy(s->ladspa_paths,"~/.ladspa:/usr/local/lib/ladspa:/usr/lib/ladspa");
s->max_frames=65536; s->channels=2; s->transition_ms=5; s->offline_rate=48000; s->recording_seconds=2;
s->width=1280; s->height=800; s->piano_width=900; s->piano_height=620; s->refresh_ms=100;
s->sequencer_snap=s->piano_snap=240; s->sequencer_zoom=48; s->clip_ticks=3840; s->note_ticks=960;
s->note_velocity=100; s->filter_channel=-1; s->filter_high=127; s->tempo=120000;
s->volume_cc=7; s->pan_cc=10; s->sustain_cc=64; s->loop_end=3840;
}
static int snap(int64_t v) { return v==1 || v==120 || v==160 || v==240 || v==320 || v==480 || v==960 || v==1920 || v==3840; }
xtc_result xtc_settings_validate(const xtc_settings *s)
{
size_t i;
if (!s) return XTC_ERR_INVALID_ARGUMENT;
for (i=0;i<COUNT;++i) {
const xtc_setting_info *p=schema+i; const char *field=(const char *)s+p->offset;
if (p->capacity) { size_t j; if (!memchr(field,0,p->capacity)) return XTC_ERR_INVALID_ARGUMENT;
for (j=0;field[j];++j) if ((unsigned char)field[j]<32 || (unsigned char)field[j]==127) return XTC_ERR_INVALID_ARGUMENT;
} else { int64_t v; memcpy(&v,field,sizeof(v)); if (v<p->minimum || v>p->maximum) return XTC_ERR_INVALID_ARGUMENT; }
}
if (!s->client_name[0] || !snap(s->sequencer_snap) || !snap(s->piano_snap) ||
(s->sequencer_zoom!=24 && s->sequencer_zoom!=48 && s->sequencer_zoom!=96 && s->sequencer_zoom!=192 && s->sequencer_zoom!=384) ||
s->filter_low>s->filter_high || s->loop_end<=s->loop_start) return XTC_ERR_INVALID_ARGUMENT;
{ int64_t cc[3]={s->volume_cc,s->pan_cc,s->sustain_cc};
for(i=0;i<3;++i) { size_t j; if(cc[i]==120 || cc[i]==123) return XTC_ERR_INVALID_ARGUMENT;
for(j=0;j<i;++j) if(cc[i]!=128 && cc[i]==cc[j]) return XTC_ERR_INVALID_ARGUMENT; } }
return XTC_OK;
}
xtc_result xtc_settings_get(const xtc_settings *s,size_t i,char *out,size_t size)
{
int n; if(!s || i>=COUNT || !out || !size) return XTC_ERR_INVALID_ARGUMENT;
if(schema[i].capacity && !memchr((const char *)s+schema[i].offset,0,schema[i].capacity)) return XTC_ERR_INVALID_ARGUMENT;
if(schema[i].capacity) n=snprintf(out,size,"%s",(const char *)s+schema[i].offset);
else { int64_t v; memcpy(&v,(const char *)s+schema[i].offset,sizeof(v)); n=snprintf(out,size,"%" PRId64,v); }
return n<0 || (size_t)n>=size ? XTC_ERR_CAPACITY : XTC_OK;
}
xtc_result xtc_settings_set(xtc_settings *s,size_t i,const char *value)
{
const xtc_setting_info *p; char *out;
if(!s || i>=COUNT || !value) return XTC_ERR_INVALID_ARGUMENT;
p=schema+i; out=(char *)s+p->offset;
if(p->capacity) { size_t j,n=strlen(value); if(n>=p->capacity) return XTC_ERR_CAPACITY;
for(j=0;j<n;++j) if((unsigned char)value[j]<32 || (unsigned char)value[j]==127) return XTC_ERR_INVALID_ARGUMENT;
memcpy(out,value,n+1);
} else { char *end; int64_t v; if(!*value) return XTC_ERR_INVALID_ARGUMENT;
errno=0; v=strtoimax(value,&end,10); if(errno || *end || v<p->minimum || v>p->maximum) return XTC_ERR_INVALID_ARGUMENT;
memcpy(out,&v,sizeof(v)); }
return XTC_OK;
}
xtc_result xtc_settings_path(char *out,size_t size)
{
const char *home=getenv("HOME"); int n;
if(!out || !size) return XTC_ERR_INVALID_ARGUMENT;
if(!home || !*home) { struct passwd *pw=getpwuid(getuid()); home=pw ? pw->pw_dir : NULL; }
if(!home || home[0]!='/') return XTC_ERR_NOT_FOUND;
n=snprintf(out,size,"%s/.xtc.conf",home);
return n<0 || (size_t)n>=size ? XTC_ERR_CAPACITY : XTC_OK;
}
static char *trim(char *p) { char *end; while(*p && isspace((unsigned char)*p)) ++p; end=p+strlen(p); while(end>p && isspace((unsigned char)end[-1])) --end; *end=0; return p; }
xtc_result xtc_settings_load(xtc_settings *s,const char *path)
{
FILE *f; xtc_settings candidate; char line[8192],section[64]="",data[65537]; unsigned char seen[COUNT]; size_t bytes=0,offset=0; xtc_result r=XTC_OK;
if(!s || !path || !*path) return XTC_ERR_INVALID_ARGUMENT;
f=fopen(path,"rb"); if(!f) return errno==ENOENT ? XTC_ERR_NOT_FOUND : XTC_ERR_IO;
xtc_settings_defaults(&candidate); memset(seen,0,sizeof(seen));
bytes=fread(data,1,sizeof(data),f);
if(ferror(f)) r=XTC_ERR_IO;
if(fclose(f)!=0) r=XTC_ERR_IO;
if(r!=XTC_OK)return r;
if(bytes==sizeof(data) || memchr(data,0,bytes))return XTC_ERR_FORMAT;
while (offset<bytes) {
char *p,*eq; size_t i,n=0;
while(offset<bytes && data[offset]!='\n') {
if(n+1>=sizeof(line)){r=XTC_ERR_FORMAT;break;}
line[n++]=data[offset++];
}
if(r!=XTC_OK)break;
if(offset<bytes)++offset;
line[n]=0;
p=trim(line); if(!*p || *p=='#' || *p==';') continue;
if(*p=='[') { char *end=strchr(p,']'); if(!end || end[1] || end==p+1 || (size_t)(end-p-1)>=sizeof(section)) {r=XTC_ERR_FORMAT;break;}
*end=0;
for(i=0;i<COUNT;++i) if(!strcmp(p+1,schema[i].section))break;
if(i==COUNT){r=XTC_ERR_FORMAT;break;}
strcpy(section,p+1); continue; }
eq=strchr(p,'='); if(!eq) {r=XTC_ERR_FORMAT;break;} *eq=0; p=trim(p); eq=trim(eq+1);
for(i=0;i<COUNT;++i) if(!strcmp(section,schema[i].section) && !strcmp(p,schema[i].key)) break;
if(i==COUNT || seen[i]) {r=XTC_ERR_FORMAT;break;} seen[i]=1;
if(*eq=='"') { char *src=eq+1,*dst=eq; int closed=0;
while(*src) { if(*src=='"') { ++src; closed=1; break; }
if(*src=='\\') { ++src; if(*src!='\\' && *src!='"') break; }
*dst++=*src++; }
if(!closed || *trim(src)) {r=XTC_ERR_FORMAT;break;} *dst=0;
}
if(xtc_settings_set(&candidate,i,eq)!=XTC_OK) {r=XTC_ERR_FORMAT;break;}
}
if(r==XTC_OK && xtc_settings_validate(&candidate)!=XTC_OK) r=XTC_ERR_FORMAT;
if(r==XTC_OK) *s=candidate;
return r;
}
xtc_result xtc_settings_save(const xtc_settings *s,const char *path)
{
char temporary[4096]; FILE *f; int fd,n; size_t i; const char *section=""; xtc_result r;
if(!path || !*path || xtc_settings_validate(s)!=XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
n=snprintf(temporary,sizeof(temporary),"%s.tmp-XXXXXX",path); if(n<0 || (size_t)n>=sizeof(temporary)) return XTC_ERR_CAPACITY;
fd=mkstemp(temporary); if(fd<0) return XTC_ERR_IO;
f=fdopen(fd,"wb"); if(!f) {close(fd);unlink(temporary);return XTC_ERR_IO;}
r=XTC_OK; if(fputs("; xtc application preferences. Saved by Preferences / Apply.\n",f)<0) r=XTC_ERR_IO;
for(i=0;i<COUNT && r==XTC_OK;++i) {
char value[4096]; const char *p;
if(strcmp(section,schema[i].section)) { section=schema[i].section; if(fprintf(f,"\n[%s]\n",section)<0) {r=XTC_ERR_IO;break;} }
if(xtc_settings_get(s,i,value,sizeof(value))!=XTC_OK || fprintf(f,"%s = ",schema[i].key)<0) {r=XTC_ERR_IO;break;}
if(schema[i].capacity) {
if(fputc('"',f)==EOF) r=XTC_ERR_IO;
for(p=value;*p;++p) { if((*p=='"' || *p=='\\') && fputc('\\',f)==EOF) r=XTC_ERR_IO; if(fputc(*p,f)==EOF) r=XTC_ERR_IO; }
if(fputc('"',f)==EOF) r=XTC_ERR_IO;
} else if(fputs(value,f)<0) r=XTC_ERR_IO;
if(fputc('\n',f)==EOF) r=XTC_ERR_IO;
}
if(fflush(f)!=0 || fsync(fd)!=0) r=XTC_ERR_IO;
if(fclose(f)!=0) r=XTC_ERR_IO;
if(r==XTC_OK && rename(temporary,path)!=0) r=XTC_ERR_IO;
if(r!=XTC_OK) unlink(temporary);
return r;
}

View file

@ -4,6 +4,8 @@
#include "../../src/gui/piano_roll.hpp"
#include "../../src/gui/mixer_widget.hpp"
#include "../../src/gui/plugin_dialog.hpp"
#include "../../src/gui/preferences_dialog.hpp"
#include "../../src/gui/plugin_library.hpp"
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Value_Slider.H>
@ -126,6 +128,8 @@ xtc_result xtc_backend_close(xtc_backend *backend)
}
xtc_result xtc_backend_start(xtc_backend *backend)
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) { handle.running = 1; engine_status.active_revision = engine_status.submitted_revision; } return start_result; }
xtc_result xtc_backend_set_loop(xtc_backend *,uint64_t start,uint64_t end,int enabled)
{ return enabled && (end<=start || end-start<64) ? XTC_ERR_INVALID_ARGUMENT : XTC_OK; }
xtc_result xtc_backend_set_transition_frames(xtc_backend *, size_t frames)
{ CHECK(frames == 240); return XTC_OK; }
xtc_result xtc_backend_request_stop(xtc_backend *backend)
@ -183,7 +187,7 @@ int main(int argc, char **argv)
{
reset();
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *play = find(&window, "Play");
auto *stop = find(&window, "Stop");
@ -535,7 +539,7 @@ int main(int argc, char **argv)
for (xtc_result failure : {XTC_ERR_IO, XTC_ERR_UNSUPPORTED}) {
reset();
open_result = failure;
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
CHECK(contains(&window, "Open JACK failed") && !find(&window, "Play")->active());
open_result = XTC_OK;
@ -549,21 +553,21 @@ int main(int argc, char **argv)
partial_open = true;
open_result = XTC_ERR_IO;
{
MainWindow window(false);
MainWindow window(false,"");
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(false);
MainWindow window(false,"");
press(find(&window, "Play"));
window.do_callback();
}
CHECK(alive == 0U);
reset();
{
MainWindow window(false); auto *s = timeline(&window);
MainWindow window(false,""); auto *s = timeline(&window);
CHECK(s->add_track() == XTC_OK && s->rename_selection("Keys") == XTC_OK);
CHECK(s->add_region() == XTC_OK && s->rename_selection("Verse") == XTC_OK);
CHECK(s->grid(120) == XTC_OK && s->grid() == 120 && s->grid(0) == XTC_ERR_INVALID_ARGUMENT);
@ -585,7 +589,7 @@ int main(int argc, char **argv)
}
{
reset();
MainWindow window(false);
MainWindow window(false,"");
auto *canvas = graph_canvas(&window);
CHECK(canvas != nullptr);
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
@ -628,7 +632,7 @@ int main(int argc, char **argv)
window.do_callback();
}
{
reset(); MainWindow window(false);
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window); auto *mixer = window.mixer();
CHECK(sequence->add_track() == XTC_OK);
CHECK(mixer->add_bus() == XTC_OK);
@ -649,7 +653,7 @@ int main(int argc, char **argv)
window.do_callback();
}
{
reset(); MainWindow window(false);
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window);
unsigned int bindings[3] = {1,74,11}, actual[3];
CHECK(window.set_control_map(2,bindings) == XTC_OK);
@ -674,7 +678,7 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-gui-controls-test.xtc"); window.do_callback();
}
{
reset(); MainWindow window(false);
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window); auto *canvas = graph_canvas(&window);
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
const uint64_t track = sequence->selected_track(), clip = sequence->selected_clip();
@ -705,7 +709,7 @@ int main(int argc, char **argv)
}
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);
MainWindow window(false,"");
auto *sequence = timeline(&window);
size_t id = SIZE_MAX;
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
@ -807,7 +811,7 @@ int main(int argc, char **argv)
}
for (const char *variable : {"XTC_TEST_CLAP","XTC_TEST_VST2","XTC_TEST_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false); auto *sequence = timeline(&window);
reset(); MainWindow window(false,""); auto *sequence = timeline(&window);
size_t id = SIZE_MAX; xtc_plugin_spec spec{};
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
@ -831,7 +835,7 @@ int main(int argc, char **argv)
}
for (const char *variable : {"XTC_TEST_SYNTH_CLAP","XTC_TEST_SYNTH_VST2","XTC_TEST_SYNTH_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false); auto *sequence = timeline(&window);
reset(); MainWindow window(false,""); auto *sequence = timeline(&window);
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
uint64_t track = sequence->selected_track(); xtc_plugin_spec spec{};
CHECK(window.assign_instrument(track,module,0) == XTC_OK);
@ -867,7 +871,7 @@ int main(int argc, char **argv)
// the current model. Exercise real sidebar buttons rather than mock actions.
reset();
{
MainWindow window(false); window.show();
MainWindow window(false,""); window.show();
auto *sequence = timeline(&window);
CHECK(sequence != nullptr);
auto *midi = find(&window,"New MIDI track");
@ -908,7 +912,7 @@ int main(int argc, char **argv)
}
reset();
{
MainWindow window(false); window.show(); auto *sequence = timeline(&window);
MainWindow window(false,""); window.show(); auto *sequence = timeline(&window);
CHECK(window.create_track(true) == XTC_OK);
uint64_t clip = sequence->selected_clip(), track = sequence->selected_track();
auto *snap = dynamic_cast<Fl_Choice *>(find(&window,"Snap")); CHECK(snap && snap->value() == 3);
@ -964,6 +968,44 @@ int main(int argc, char **argv)
xtc_project_get_note(sequence->project(),0,&note); CHECK(note.velocity > 29 && note.pitch == 60 && note.channel == 2);
unlink("/tmp/xtc-grid-velocity.xtc"); window.do_callback();
}
{
char dir[]="/tmp/xtc-preferences-XXXXXX"; CHECK(mkdtemp(dir));
std::string path=std::string(dir)+"/.xtc.conf";
reset();
{
MainWindow window(false,path.c_str());window.show();
xtc_settings settings=window.settings();settings.note_velocity=73;settings.sequencer_snap=480;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(timeline(&window)->grid()==480);
int old_state=Fl::e_state;Fl::e_state=FL_SHIFT;
gesture(timeline(&window),228,12,96,0);Fl::e_state=old_state;
CHECK(window.settings().loop_start==960 && window.settings().loop_end==2880);
CHECK(window.set_loop(960,3840,true)==XTC_OK);
CHECK(window.settings().loop_enabled==1 && window.settings().loop_start==960);
CHECK(window.set_loop(4000,2000,true)==XTC_ERR_INVALID_ARGUMENT);
settings=window.settings();CHECK(window.apply_preferences(settings)==XTC_OK);
settings.note_velocity=128;CHECK(window.apply_preferences(settings)==XTC_ERR_INVALID_ARGUMENT);
PreferencesDialog dialog(window.settings(),path,[&](const xtc_settings &v){return window.apply_preferences(v);});
auto *input=dynamic_cast<Fl_Input *>(find(&dialog,"New note velocity"));CHECK(input);
if(input)input->value("81");
press(find(&dialog,"Apply"));CHECK(window.settings().note_velocity==81);
if(input)input->value("82");
press(find(&dialog,"Close"));CHECK(window.settings().note_velocity==81);
window.do_callback();
}
reset();
{
MainWindow window(false,path.c_str());CHECK(window.settings().note_velocity==81);
CHECK(window.settings().loop_start==960 && window.settings().loop_enabled==1);
xtc_settings paths=window.settings();
std::string binary=std::string(dir)+"/test.clap";FILE *f=fopen(binary.c_str(),"wb");CHECK(f);if(f)fclose(f);
std::snprintf(paths.clap_paths,sizeof(paths.clap_paths),"%s:%s",dir,dir);
paths.vst2_paths[0]=paths.vst3_paths[0]=paths.ladspa_paths[0]=0;
auto files=xtc_plugin_files(paths);CHECK(files.size()==1 && files[0]==binary);
unlink(binary.c_str());window.do_callback();
}
unlink(path.c_str());rmdir(dir);
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -46,7 +46,7 @@ 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(false);
MainWindow window(false,"");
window.show(argc, argv);
CHECK(find(window, "ready / stopped") != nullptr);
auto *play = find(window, "Play");

View file

@ -98,7 +98,7 @@ int main(int argc, char **argv)
xtc_clip_info arranged{};
size_t nodes, edges;
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *timeline = widget<SequencerWidget>(window);
auto *tabs = widget<Fl_Tabs>(window);
@ -123,7 +123,7 @@ int main(int argc, char **argv)
window.do_callback(); CHECK(!window.shown());
} // Destroy all widgets/engine state before opening a fresh window.
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *timeline = widget<SequencerWidget>(window);
widget<Fl_Tabs>(window)->value(widget<Fl_Tabs>(window)->child(2));

View file

@ -361,6 +361,46 @@ int main(void)
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running);
CHECK(xtc_backend_close(backend) == XTC_OK);
}
{
config.channel_count = 1;config.max_frames = 128;
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 100, 163, 1) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_backend_set_loop(backend, 100, 164, 1) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 0, 64, 0) == XTC_ERR_INVALID_STATE);
CHECK(process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 132);
CHECK(process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 100);
CHECK(process_cb(17, context) == 0 && process_cb(32, context) == 0 && process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 117);
for(i=0;i<128;++i)input[i]=(float)i;
CHECK(process_cb(128,context)==0);
for(i=0;i<128;++i)CHECK(output[i]==input[i]*0.5f);
CHECK(xtc_backend_get_status(backend,&status)==XTC_OK && status.sample_position==117);
CHECK(xtc_backend_stop(backend) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 100, 164, 0) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK && process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 149);
CHECK(xtc_backend_stop(backend) == XTC_OK && xtc_backend_close(backend) == XTC_OK);
}
{
xtc_project *p = NULL;uint64_t track,clip,note;
config.max_frames=128;config.channel_count=1;
CHECK(xtc_backend_open(&config,&backend)==XTC_OK);
CHECK(xtc_project_create(&p)==XTC_OK);
CHECK(xtc_project_add_track(p,"Loop instrument",&track)==XTC_OK);
CHECK(xtc_project_add_midi_clip(p,track,0,3840,&clip)==XTC_OK);
CHECK(xtc_project_add_note(p,clip,0,960,69,100,0,&note)==XTC_OK);
CHECK(xtc_backend_prepare_project(backend,p)==XTC_OK);
CHECK(xtc_backend_set_loop(backend,0,64,1)==XTC_OK);
CHECK(xtc_backend_start(backend)==XTC_OK && process_cb(128,context)==0);
CHECK(output[1]!=0);
for(i=0;i<64;++i)CHECK(output[i]==output[i+64]);
CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_close(backend)==XTC_OK);
xtc_project_destroy(p);
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);

View file

@ -0,0 +1,29 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_settings.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"%d: %s\n",__LINE__,#x);return 1;}}while(0)
int main(void)
{
char path[]="/tmp/xtc-settings-XXXXXX",value[4096]; int fd=mkstemp(path); size_t i,n;
xtc_settings a,b,before; struct stat st; FILE *f;
CHECK(fd>=0);close(fd);unlink(path);
xtc_settings_defaults(&a);CHECK(xtc_settings_validate(&a)==XTC_OK);
b=a;CHECK(xtc_settings_load(&b,path)==XTC_ERR_NOT_FOUND);
strcpy(a.clap_paths,"~/space folder:/tmp/quoted\"name\\path");a.loop_enabled=1;a.note_velocity=87;
CHECK(xtc_settings_save(&a,path)==XTC_OK);CHECK(stat(path,&st)==0 && (st.st_mode&0777)==0600);
CHECK(xtc_settings_load(&b,path)==XTC_OK);CHECK(memcmp(&a,&b,sizeof(a))==0);
xtc_settings_schema(&n);
for(i=0;i<n;++i){CHECK(xtc_settings_get(&a,i,value,sizeof(value))==XTC_OK);CHECK(xtc_settings_set(&b,i,value)==XTC_OK);}
CHECK(memcmp(&a,&b,sizeof(a))==0);
{const char *bad[]={"[unknown]\n","[audio]\nchannels=3\n","[audio]\nchannels=1\nchannels=2\n","[audio]\nchannels=9999999999999999999999\n","[plugins]\nclap_paths=\"unterminated\n","[transport]\nloop_start=4000\nloop_end=2\n"};
for(i=0;i<sizeof(bad)/sizeof(bad[0]);++i){f=fopen(path,"wb");CHECK(f);CHECK(fputs(bad[i],f)>=0);CHECK(fclose(f)==0);before=b;CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);CHECK(memcmp(&before,&b,sizeof(b))==0);}}
f=fopen(path,"wb");CHECK(f);CHECK(fwrite(";x\0hidden",1,9,f)==9);fclose(f);CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);
CHECK(xtc_settings_save(&a,path)==XTC_OK);b=a;b.loop_end=b.loop_start;CHECK(xtc_settings_save(&b,path)==XTC_ERR_INVALID_ARGUMENT);CHECK(xtc_settings_load(&b,path)==XTC_OK && b.loop_end==a.loop_end);
CHECK(xtc_settings_save(&a,"/no-such-xtc-directory/settings")==XTC_ERR_IO);
CHECK(xtc_settings_path(value,sizeof(value))==XTC_OK && strstr(value,"/.xtc.conf"));
unlink(path);puts("Preferences INI validation, roundtrip, permissions and failure tests passed.");return 0;
}