Phase 35: add persistent preferences and sequencer loop playback
This commit is contained in:
parent
4873b70672
commit
47b7d38a66
21 changed files with 852 additions and 108 deletions
11
Makefile
11
Makefile
|
|
@ -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
68
docs/PREFERENCES.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
37
include/xtc/xtc_settings.h
Normal file
37
include/xtc/xtc_settings.h
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ class PianoRollWidget : public Fl_Widget {
|
|||
xtc_project *project() const { return project_; }
|
||||
bool note_visible(const xtc_note_info ¬e) 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_; }
|
||||
|
|
|
|||
74
src/gui/plugin_library.hpp
Normal file
74
src/gui/plugin_library.hpp
Normal 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
|
||||
69
src/gui/preferences_dialog.hpp
Normal file
69
src/gui/preferences_dialog.hpp
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
173
src/settings/settings.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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,¬e); 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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,¬e)==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);
|
||||
|
|
|
|||
29
tests/unit/test_settings.c
Normal file
29
tests/unit/test_settings.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue