Update.
9
Makefile
|
|
@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
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
|
||||
CORE_SOURCES := src/plugin/builtin.c 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)
|
||||
|
|
@ -520,3 +520,10 @@ test-settings: $(BUILD_DIR)/test_settings
|
|||
test: test-settings
|
||||
|
||||
-include $(BUILD_DIR)/tests/unit/test_settings.d
|
||||
|
||||
$(BUILD_DIR)/test_builtin: tests/unit/test_builtin.c $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(BUILD_DIR)/libxtc.a -lm -ldl
|
||||
.PHONY: test-builtins
|
||||
test: test-builtins
|
||||
test-builtins: $(BUILD_DIR)/test_builtin
|
||||
./$(BUILD_DIR)/test_builtin
|
||||
|
|
|
|||
41
docs/BUILTIN_DEVICES.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Internal instruments and effects
|
||||
|
||||
No plugin downloads, search paths or external libraries are needed. The original
|
||||
built-in sine remains available for existing projects.
|
||||
|
||||
Choose **Internal: Soft Keys** or **Internal: Sub Bass** in a MIDI track's
|
||||
instrument dropdown. Soft Keys blends a sine with a third harmonic; Sub Bass
|
||||
uses a pure sine. Both are fixed presets with 32 voices, velocity sensitivity,
|
||||
short attack/release envelopes, sustain pedal, note-off and panic handling.
|
||||
High harmonics are omitted near Nyquist. These basic instruments have no preset
|
||||
editor, pitch bend, sample library or oscillator/filter controls.
|
||||
|
||||
In **Modular Graph**, select a wire and choose **Edit → Insert internal delay on
|
||||
wire** or **Insert internal reverb on wire**. This preserves the route and inserts
|
||||
the effect into the signal path. **File → Add internal delay/reverb** creates an
|
||||
unconnected device for manual wiring. Select its node and use **Edit → Selected
|
||||
plugin control** to edit it, or **Selected plugin automation** to automate it.
|
||||
Changes use the existing stopped-engine preparation and undo workflow.
|
||||
|
||||
- **Delay:** time (1–2000 ms), feedback (0–0.95), dry/wet mix (0–1).
|
||||
- **Reverb:** four parallel damped feedback combs, with decay (0–0.95), damping
|
||||
(0–0.95) and dry/wet mix (0–1). Decay is a feedback coefficient, not seconds.
|
||||
|
||||
These are basic mono processors; stereo plans use independent instances. Reverb
|
||||
is deliberately simple and has no room model or stereo diffusion. Delay time
|
||||
changes can click; there is no crossfade or tempo sync. Transport seeks retain
|
||||
existing effect history, like LADSPA. Effect tails require continued rendering;
|
||||
playback/export does not automatically extend the song for tails.
|
||||
|
||||
Project format 13 stores internal devices as format 4 with reserved module path
|
||||
`/xtc/internal`, stable numeric identity and index (0 keys, 1 bass, 2 delay,
|
||||
3 reverb). This is a virtual identifier, never an external binary. Effect controls
|
||||
and automation are saved using the existing plugin records; instruments are fixed
|
||||
presets. Readers still accept formats 1–12. Older xtc builds reject format 13.
|
||||
All buffers are allocated during preparation; processing performs no allocation,
|
||||
file access, library loading, locking or GUI calls.
|
||||
|
||||
Validation: `make BUILD_DIR=build/phase38 all gui test test-gui-instruments
|
||||
test-gui-modern-plugins test-gui-plugins analyze` (join onto one line).
|
||||
`test-builtins` checks impulse tails, dry mix, feedback bounds, block partition
|
||||
consistency, note release/panic and instrument/effect project round trips.
|
||||
|
|
@ -21,7 +21,7 @@ may be quoted, with `\\` and `\"` escapes. Apply rewrites comments and formattin
|
|||
| 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` |
|
||||
| transport | `loop_start=0`, `loop_end=3840`, `loop_enabled=0`, `follow_playhead=0` |
|
||||
|
||||
Audio client name, block capacity, output channels and fades take effect on
|
||||
**Reconnect**. JACK chooses the actual device, rate and hardware buffer size;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Project format v12 (reads v1–v11)
|
||||
# Project format v13 (reads v1–v12)
|
||||
|
||||
Phases 5–31 use a bounded text format implemented in C99 without a parser library.
|
||||
The extension is `.xtc`. File operations run exclusively on the control thread.
|
||||
|
|
@ -389,3 +389,10 @@ migrate to no bindings and built-in live sine. Parsing/inspection never executes
|
|||
modules; preparing playback verifies binary identity and configuration. Deleting
|
||||
tracks removes bindings; reordering preserves stable track identities. Undo
|
||||
snapshots include bindings. Public clients must rebuild for the new spec field.
|
||||
|
||||
## Version 13 internal devices
|
||||
|
||||
Format value 4 identifies dependency-free internal devices at `/xtc/internal`.
|
||||
Indices 0/1 bind fixed Soft Keys/Sub Bass instruments; 2/3 are Delay/Reverb
|
||||
effects with three controls and existing automation records. Identity and control
|
||||
ranges are validated without loading libraries. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md).
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ undo/redo and a generic LADSPA editor. JACK is the only live audio/MIDI backend;
|
|||
core validation and offline rendering work without it. Native/LADSPA processors
|
||||
are trusted in-process code. No proprietary energyXT resources are included.
|
||||
|
||||
Project format v12 reads versions 1–12 transactionally. Syntax diagnostics identify
|
||||
Project format v13 reads versions 1–13 transactionally. Syntax diagnostics identify
|
||||
records and lines. `xtc-check --inspect` lists dependency paths without executing
|
||||
plugins. Phase 29 adds timing/CPU/RSS reports, repeatable stress runs and virtual
|
||||
endurance. Phase 30 adds configurable install directories, shared install/remove
|
||||
|
|
@ -85,3 +85,13 @@ vertical dB faders, flat pan knobs, mute/solo, routing and stereo meters on ever
|
|||
track and bus, plus a pinned master monitor. Selected-track automation remains
|
||||
below the strips. No project format or engine changes. Screenshots are refreshed;
|
||||
see MIXER.md and screenshots/README.md.
|
||||
|
||||
|
||||
Phase 37 adds searchable Commands (Ctrl+P), editable project BPM, a live track
|
||||
inspector, session statistics, Fit song and an optional persistent Follow
|
||||
playhead control. Numeric input remains intact during display refreshes.
|
||||
Screenshots are refreshed, including the command palette. See WORKSPACE.md.
|
||||
|
||||
Phase 38 adds dependency-free Soft Keys/Sub Bass instruments and Delay/Reverb
|
||||
effects with saved controls and automation. Choose instruments in track headers;
|
||||
insert effects on selected graph wires from Edit. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md).
|
||||
|
|
|
|||
|
|
@ -1038,6 +1038,52 @@ and the updated [screenshots](../screenshots/README.md).
|
|||
|
||||
---
|
||||
|
||||
## Phase 37 — Workspace Navigation and Direct Editing
|
||||
|
||||
### Goal
|
||||
Reduce menu hunting and make the workspace useful for everyday project editing
|
||||
with a direct transport, track inspector and searchable commands.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [x] Add Ctrl+P command search with filtering, keyboard navigation, Enter and Escape.
|
||||
- [x] Expose active menu actions through existing callbacks, without duplicating command implementations.
|
||||
- [x] Make project BPM editable from the transport with validation, undo and failed-preparation rollback.
|
||||
- [x] Replace the passive sidebar with a context-aware gain/pan/mute/solo inspector.
|
||||
- [x] Preserve unfinished numeric input across timer refreshes and avoid no-op history edits.
|
||||
- [x] Add a session summary, Fit song and optional Follow playhead navigation.
|
||||
- [x] Persist Follow playhead through the existing Preferences / Apply workflow.
|
||||
- [x] Test command selection/cancellation, inspector edits, tempo rollback/undo and navigation.
|
||||
- [x] Refresh all screenshots, including a command-palette view, and document the workflow.
|
||||
|
||||
### Implementation status
|
||||
Implemented over the existing C model, history and backend APIs, with no new
|
||||
dependencies or project format change. Tempo edits stop and rewind transport;
|
||||
track/clip and piano selections survive. Fit song uses the supported 24..384
|
||||
pixels-per-beat zoom levels; very long projects still require scrolling.
|
||||
The inspector targets the selected mixer channel in Mixer and the arrangement
|
||||
track in other views. Command search lists active menu actions; it is not a
|
||||
plugin scanner or a key-binding editor. See WORKSPACE.md.
|
||||
|
||||
### Future usability work
|
||||
Dockable editor panels, arrangement multiselection, independent track inserts,
|
||||
resizable pinned track headers and customizable shortcuts remain future work.
|
||||
They require separate model/layout designs and acceptance tests.
|
||||
|
||||
---
|
||||
|
||||
## Phase 38 — Internal instruments and effects
|
||||
|
||||
- [x] Add always-available Soft Keys and Sub Bass MIDI instrument presets.
|
||||
- [x] Add Delay and Reverb with bounded controls and effect automation.
|
||||
- [x] Integrate track selection, graph wire insertion, undo and saved projects.
|
||||
- [x] Preserve older projects; reserve format 13 internal device identities.
|
||||
- [x] Verify DSP tails, block partition consistency, note release/panic and persistence.
|
||||
- [x] Refresh application screenshots and add internal device editor views.
|
||||
|
||||
See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md) for controls and explicit 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.
|
||||
|
|
|
|||
|
|
@ -108,3 +108,44 @@ horizontally for larger projects; the master output monitor stays visible.
|
|||
Select a channel name to edit its gain/pan automation below the bank. Existing
|
||||
undo, keyboard controls and project persistence remain available. See
|
||||
[MIXER.md](MIXER.md) for fader units, metering, automation and limits.
|
||||
|
||||
|
||||
## Phase 37 direct editing and navigation
|
||||
|
||||
**Commands** (Ctrl+P) searches active menu actions by case-insensitive substring.
|
||||
Type part of an action path such as `mixer`, `import` or `preferences`, use
|
||||
Up/Down to select, then Enter or **Run command**. Double-click also runs an
|
||||
entry. Escape or Cancel leaves the project unchanged. The palette closes before
|
||||
executing the existing menu callback; context and recording guards still apply.
|
||||
|
||||
The transport **BPM** field edits project tempo from 20 to 300 BPM, stored in
|
||||
milli-BPM. Enter or release commits one undoable change. It stops and rewinds
|
||||
transport and prepares the project, including converting loop ticks to samples
|
||||
at the new tempo. Failed validation/preparation retains the previous project;
|
||||
the field returns to its actual value. Existing track/clip and piano-roll
|
||||
selection survives. This is project tempo, separate from Preferences' new-project
|
||||
default. Tempo automation is not added.
|
||||
|
||||
The **Track inspector** now edits the selected track's linear Gain (0..4), Pan
|
||||
(-1..1), Mute and Solo. In Mixer it follows the selected channel; in Sequencer
|
||||
and Graph it follows arrangement selection. Controls synchronize with model
|
||||
changes and undo, while typing is protected from the display refresh timer.
|
||||
Only the edited field is committed. Unchanged values create no history entry.
|
||||
These are static values; automation still overrides gain and pan. Recording
|
||||
prevents edits. Track instruments and routing retain their existing selectors.
|
||||
|
||||
The session row shows track/bus count, clip count and the arrangement's bar span.
|
||||
**Fit song** (Ctrl+0) returns to the start and selects the largest supported zoom
|
||||
that fits the arrangement plus a beat of padding. Supported zoom remains
|
||||
24, 48, 96, 192 or 384 pixels per beat; a sufficiently long arrangement requires
|
||||
scrolling even at the minimum. Empty projects fit one bar.
|
||||
|
||||
**Follow playhead** pages the arrangement horizontally during playback, retaining
|
||||
vertical scroll. It operates only in Sequencer and waits while a mouse drag is
|
||||
active. It is off by default; Preferences / Apply persists it in
|
||||
`[transport] follow_playhead=0` or `1`. Manual scrolling remains available and
|
||||
Fit song resets the viewport without editing the project.
|
||||
|
||||
Verification covers real FLTK palette filtering, empty searches and cancellation,
|
||||
tempo callbacks/undo/failed adoption, inspector edits and in-progress numeric
|
||||
input, fit and follow navigation, and existing core/GUI/live JACK workflows.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef enum { XTC_PLUGIN_LADSPA = 0, XTC_PLUGIN_CLAP, XTC_PLUGIN_VST2, XTC_PLUGIN_VST3 } xtc_plugin_format;
|
||||
typedef enum { XTC_PLUGIN_LADSPA = 0, XTC_PLUGIN_CLAP, XTC_PLUGIN_VST2, XTC_PLUGIN_VST3, XTC_PLUGIN_BUILTIN } xtc_plugin_format;
|
||||
/* Internal device indices: 0 Soft Keys, 1 Sub Bass, 2 Delay, 3 Reverb. */
|
||||
#define XTC_BUILTIN_PATH "/xtc/internal"
|
||||
const char *xtc_plugin_format_name(xtc_plugin_format format);
|
||||
#define XTC_PLUGIN_CONTROLS 16U
|
||||
#define XTC_PLUGIN_POINTS 32U
|
||||
|
|
@ -13,7 +15,7 @@ const char *xtc_plugin_format_name(xtc_plugin_format format);
|
|||
* sample rounding collides. Integer/toggle controls require mode 1. */
|
||||
typedef struct { uint64_t tick; unsigned int port, step; float value; } xtc_plugin_point;
|
||||
/* Plain copied model: no library handles or callback pointers. Absolute trusted
|
||||
* binary path, format/index/integer or string identity/label verified on compile.
|
||||
* binary path (or reserved XTC_BUILTIN_PATH), identity/label verified on compile.
|
||||
* Controls are LADSPA port indices; modern effects use default configuration
|
||||
* with zero controls/points. Initialize the complete struct to zero. */
|
||||
typedef struct {
|
||||
|
|
@ -21,7 +23,7 @@ typedef struct {
|
|||
xtc_plugin_format format;
|
||||
char identifier[128]; /* CLAP stable ID / VST3 class hex; empty for LADSPA/VST2. */
|
||||
unsigned int index, unique_id, inputs, control_count;
|
||||
unsigned int instrument; /* Modern zero-input note instrument; track binding only. */
|
||||
unsigned int instrument; /* Zero-input note instrument; track binding only. */
|
||||
unsigned int ports[XTC_PLUGIN_CONTROLS];
|
||||
float values[XTC_PLUGIN_CONTROLS];
|
||||
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define XTC_PROJECT_FORMAT_VERSION 12U
|
||||
#define XTC_PROJECT_FORMAT_VERSION 13U
|
||||
#define XTC_PROJECT_MAX_TRACKS 64U
|
||||
#define XTC_PROJECT_MAX_CLIPS 1024U
|
||||
#define XTC_PROJECT_NAME_BYTES 64U
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ typedef struct {
|
|||
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;
|
||||
int64_t loop_start, loop_end, loop_enabled, follow_playhead;
|
||||
} xtc_settings;
|
||||
typedef struct {
|
||||
const char *section, *key, *label;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
# xtc screenshots
|
||||
|
||||
Captured from the Phase 36 FLTK application on 2026-10-28, using a virtual X11
|
||||
Captured from the Phase 38 FLTK application on 2026-10-28, using a virtual X11
|
||||
screen. Images show actual rendered widgets, without mockups or image edits.
|
||||
|
||||

|
||||
|
||||
Internal instrument dropdowns show Soft Keys and Sub Bass; internal delay and
|
||||
reverb captures show the actual editable effect controls.
|
||||
|
||||
The arrangement uses `examples/projects/modern-session.xtc`. Mixer captures add
|
||||
a bus, routing, pan/level settings and automation. The playing view shows actual
|
||||
JACK callback meters; additional captures show the minimum 1220 × 720 layout and
|
||||
|
|
@ -12,7 +15,9 @@ a larger session scrolled to its final channels, with the master monitor pinned.
|
|||
The audio view adds a temporary generated waveform. Plugin views use the
|
||||
repository's LADSPA, CLAP, VST2 and VST3 test fixtures; these are demonstration
|
||||
plugins, not commercial instruments. Preferences show application defaults and
|
||||
the staged loop. No user configuration or example project was overwritten.
|
||||
the staged loop. The command palette shows the available application actions.
|
||||
The workspace now includes direct BPM editing, a live track inspector and
|
||||
arrangement navigation. No user configuration or example project was overwritten.
|
||||
|
||||
- [Sequencer loop](sequencer-loop.png)
|
||||
- [Mixer automation](mixer-automation.png)
|
||||
|
|
@ -38,3 +43,9 @@ the staged loop. No user configuration or example project was overwritten.
|
|||
- [Mixer playing](mixer-playing.png)
|
||||
- [Mixer minimum size](mixer-minimum-size.png)
|
||||
- [Mixer scrolled session](mixer-scrolled-session.png)
|
||||
|
||||
- [Commands](commands.png)
|
||||
|
||||
- [Internal instruments](internal-instruments.png)
|
||||
- [Internal delay](internal-delay.png)
|
||||
- [Internal reverb](internal-reverb.png)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
BIN
screenshots/commands.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
screenshots/internal-delay.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
screenshots/internal-instruments.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
screenshots/internal-reverb.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
102
src/gui/command_palette.hpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#ifndef XTC_COMMAND_PALETTE_HPP
|
||||
#define XTC_COMMAND_PALETTE_HPP
|
||||
#include "theme.hpp"
|
||||
#include <FL/Fl_Double_Window.H>
|
||||
#include <FL/Fl_Input.H>
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/* UI command search only; execute the chosen action after this modal is gone. */
|
||||
class CommandPalette : public Fl_Double_Window
|
||||
{
|
||||
std::vector<std::string> commands_, visible_;
|
||||
Fl_Input *search_;
|
||||
Fl_Hold_Browser *list_;
|
||||
std::string selected_;
|
||||
static std::string lower(std::string value)
|
||||
{
|
||||
for (auto &c : value)
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return value;
|
||||
}
|
||||
void filter()
|
||||
{
|
||||
list_->clear();
|
||||
visible_.clear();
|
||||
const auto query = lower(search_->value());
|
||||
for (const auto &command : commands_)
|
||||
if (lower(command).find(query) != std::string::npos) {
|
||||
visible_.push_back(command);
|
||||
list_->add(command.c_str());
|
||||
}
|
||||
if (!visible_.empty())
|
||||
list_->select(1);
|
||||
}
|
||||
void accept()
|
||||
{
|
||||
int i = list_->value();
|
||||
if (i > 0 && static_cast<size_t>(i) <= visible_.size()) {
|
||||
selected_ = visible_[static_cast<size_t>(i - 1)];
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit CommandPalette(std::vector<std::string> commands)
|
||||
: Fl_Double_Window(700, 440, "xtc — Commands"), commands_(std::move(commands))
|
||||
{
|
||||
set_modal();
|
||||
begin();
|
||||
search_ = new Fl_Input(136, 18, 544, 32, "Search commands");
|
||||
search_->when(FL_WHEN_CHANGED);
|
||||
search_->callback([](Fl_Widget *, void *p) { static_cast<CommandPalette *>(p)->filter(); }, this);
|
||||
list_ = new Fl_Hold_Browser(20, 64, 660, 302);
|
||||
list_->format_char(0);
|
||||
list_->callback(
|
||||
[](Fl_Widget *, void *p) {
|
||||
if (Fl::event_clicks())
|
||||
static_cast<CommandPalette *>(p)->accept();
|
||||
},
|
||||
this);
|
||||
auto *cancel = new Fl_Button(450, 390, 100, 30, "Cancel");
|
||||
cancel->callback([](Fl_Widget *, void *p) { static_cast<CommandPalette *>(p)->hide(); }, this);
|
||||
auto *run = new Fl_Button(564, 390, 116, 30, "Run command");
|
||||
run->callback([](Fl_Widget *, void *p) { static_cast<CommandPalette *>(p)->accept(); }, this);
|
||||
callback([](Fl_Widget *, void *p) { static_cast<CommandPalette *>(p)->hide(); }, this);
|
||||
end();
|
||||
xtc_ui::style(this);
|
||||
filter();
|
||||
}
|
||||
int handle(int event) override
|
||||
{
|
||||
if (event == FL_KEYDOWN) {
|
||||
int key = Fl::event_key();
|
||||
if (key == FL_Enter || key == FL_KP_Enter) {
|
||||
accept();
|
||||
return 1;
|
||||
}
|
||||
if (key == FL_Escape) {
|
||||
hide();
|
||||
return 1;
|
||||
}
|
||||
if ((key == FL_Up || key == FL_Down) && !visible_.empty()) {
|
||||
list_->select(
|
||||
std::clamp(list_->value() + (key == FL_Up ? -1 : 1), 1, static_cast<int>(visible_.size())));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return Fl_Double_Window::handle(event);
|
||||
}
|
||||
std::string run()
|
||||
{
|
||||
show();
|
||||
search_->take_focus();
|
||||
while (shown())
|
||||
Fl::wait();
|
||||
return selected_;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
#include "snap.hpp"
|
||||
#include "velocity_lane.hpp"
|
||||
#include "preferences_dialog.hpp"
|
||||
#include "command_palette.hpp"
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
#include "plugin_library.hpp"
|
||||
#include <FL/Fl_Check_Button.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
|
|
@ -32,6 +34,11 @@
|
|||
#include <cerrno>
|
||||
|
||||
namespace {
|
||||
bool editing_number(Fl_Value_Input *input)
|
||||
{
|
||||
return Fl::focus() == input || Fl::focus() == &input->input || Fl::pushed() == input ||
|
||||
Fl::pushed() == &input->input;
|
||||
}
|
||||
/**
|
||||
* @brief Updates a box label only when the text differs.
|
||||
*
|
||||
|
|
@ -103,12 +110,18 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
|
||||
menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this);
|
||||
menu->add("Audio/Panic", FL_F + 9, panic_callback, this);
|
||||
menu->add("View/Commands",FL_CTRL+'p',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->show_commands();},this);
|
||||
menu->add("View/Fit song",FL_CTRL+'0',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->fit_song();},this);
|
||||
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
|
||||
menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
|
||||
menu->add("Track/Load instrument plugin", 0, instrument_callback, this);
|
||||
menu->add("Track/Use built-in instrument", 0, instrument_callback, this);
|
||||
menu->add("Track/Live MIDI to selected instrument", 0, instrument_callback, this);
|
||||
menu->add("Track/Live MIDI to built-in sine", 0, instrument_callback, this);
|
||||
menu->add("Edit/Insert internal delay on wire", 0, plugin_callback, this);
|
||||
menu->add("Edit/Insert internal reverb on wire", 0, plugin_callback, this);
|
||||
menu->add("File/Add internal delay", 0, plugin_callback, this);
|
||||
menu->add("File/Add internal reverb", 0, plugin_callback, this);
|
||||
menu->add("File/Load plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Replace selected plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Insert plugin on selected wire", 0, plugin_callback, this);
|
||||
|
|
@ -159,10 +172,43 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
indicator_->labelsize(12);
|
||||
transport_position_ = new Fl_Box(680,40,190,30,"001 : 1 : 000");
|
||||
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->resize(12,82,970,28);
|
||||
loop_button_=new Fl_Check_Button(1000,82,100,28,"Loop");
|
||||
tempo_display_ = new Fl_Value_Input(880, 40, 78, 30, "BPM");
|
||||
tempo_display_->align(FL_ALIGN_RIGHT);
|
||||
tempo_display_->bounds(20, 300);
|
||||
tempo_display_->step(.1);
|
||||
tempo_display_->when(FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY);
|
||||
tempo_display_->tooltip("Project tempo in BPM. Enter or release to apply; undoable. Stops playback.");
|
||||
tempo_display_->callback(
|
||||
[](Fl_Widget *, void *c) {
|
||||
auto *w = static_cast<MainWindow *>(c);
|
||||
double bpm = w->tempo_display_->value();
|
||||
xtc_result r = (!std::isfinite(bpm) || bpm < 20 || bpm > 300)
|
||||
? XTC_ERR_INVALID_ARGUMENT
|
||||
: w->set_tempo(static_cast<unsigned int>(std::lround(bpm * 1000)));
|
||||
if (r != XTC_OK)
|
||||
w->report_error("Tempo unchanged", r);
|
||||
w->tempo_display_->value(xtc_project_tempo(w->project_.get()) / 1000.0);
|
||||
w->refresh_status();
|
||||
},
|
||||
this);
|
||||
auto *hint = new Fl_Box(12, 82, 160, 28, "xtc / SESSION");
|
||||
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
session_info_ = new Fl_Box(180, 82, 280, 28);
|
||||
session_info_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
auto *commands = new Fl_Button(480, 82, 120, 28, "Commands");
|
||||
commands->tooltip("Search actions (Ctrl+P)");
|
||||
commands->callback([](Fl_Widget *, void *c) { static_cast<MainWindow *>(c)->show_commands(); }, this);
|
||||
follow_ = new Fl_Check_Button(620, 82, 154, 28, "Follow playhead");
|
||||
follow_->tooltip("Keep playback visible in the arrangement; pauses while dragging.");
|
||||
follow_->callback(
|
||||
[](Fl_Widget *widget, void *c) {
|
||||
static_cast<MainWindow *>(c)->settings_.follow_playhead = static_cast<Fl_Check_Button *>(widget)->value();
|
||||
},
|
||||
this);
|
||||
auto *fit = new Fl_Button(790, 82, 100, 28, "Fit song");
|
||||
fit->tooltip("Choose the largest supported zoom that fits the song (Ctrl+0)");
|
||||
fit->callback([](Fl_Widget *, void *c) { static_cast<MainWindow *>(c)->fit_song(); }, this);
|
||||
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);
|
||||
|
|
@ -194,6 +240,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
timeline_hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
timeline_hint->labelsize(12);
|
||||
auto *timeline_scroll = new Fl_Scroll(228, 206, 1032, 452);
|
||||
timeline_scroll_=timeline_scroll;
|
||||
sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this);
|
||||
timeline_scroll->end();
|
||||
sequencer_->before_edit = before_arrangement_edit;
|
||||
|
|
@ -361,11 +408,27 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
load->callback(instrument_callback,this);
|
||||
auto *editor = new Fl_Button(24,344,168,34,"Edit MIDI notes");
|
||||
editor->callback(piano_callback,this);
|
||||
auto *selection_title = new Fl_Box(24,404,168,24,"SELECTED TRACK");
|
||||
auto *selection_title = new Fl_Box(24,404,168,24,"TRACK INSPECTOR");
|
||||
selection_title->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
selection_info_ = new Fl_Box(24,438,168,170,"Select a track to inspect it.");
|
||||
selection_info_ = new Fl_Box(24,438,168,82,"Select a track to inspect it.");
|
||||
selection_info_->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
|
||||
auto *tip = new Fl_Box(24,620,168,64,"Ctrl+1 Arrange\nCtrl+2 Mixer\nCtrl+3 Routing");
|
||||
inspector_gain_ = new Fl_Value_Input(78, 528, 114, 26, "Gain");
|
||||
inspector_gain_->bounds(0, 4);
|
||||
inspector_gain_->step(.01);
|
||||
inspector_pan_ = new Fl_Value_Input(78, 562, 114, 26, "Pan");
|
||||
inspector_pan_->bounds(-1, 1);
|
||||
inspector_pan_->step(.01);
|
||||
inspector_mute_ = new Fl_Check_Button(24, 596, 78, 24, "Mute");
|
||||
inspector_solo_ = new Fl_Check_Button(108, 596, 84, 24, "Solo");
|
||||
for (auto *control : {static_cast<Fl_Widget *>(inspector_gain_), static_cast<Fl_Widget *>(inspector_pan_),
|
||||
static_cast<Fl_Widget *>(inspector_mute_), static_cast<Fl_Widget *>(inspector_solo_)}) {
|
||||
control->when(FL_WHEN_RELEASE);
|
||||
control->callback(inspector_callback, this);
|
||||
}
|
||||
inspector_gain_->tooltip(
|
||||
"Selected track linear gain: 0 silence, 1 unity, 4 maximum. Automation overrides static gain.");
|
||||
inspector_pan_->tooltip("Selected track pan: -1 left, 0 center, +1 right.");
|
||||
auto *tip = new Fl_Box(24, 632, 168, 52, "Ctrl+P Commands\nCtrl+1/2/3 Views\nCtrl+0 Fit song");
|
||||
tip->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
sidebar->end();
|
||||
status_ = new Fl_Box(12,714,1256,26,"JACK: not connected");
|
||||
|
|
@ -870,6 +933,7 @@ void MainWindow::timer_callback(void *context)
|
|||
window->stop_engine();
|
||||
}
|
||||
window->refresh_status();
|
||||
window->follow_playhead();
|
||||
if (!window->closing_)
|
||||
Fl::repeat_timeout(static_cast<double>(window->settings_.refresh_ms)/1000,timer_callback,context);
|
||||
}
|
||||
|
|
@ -1564,41 +1628,97 @@ 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 (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()));
|
||||
size_t count = 0; bool selected = false; xtc_project_note_count(project_.get(),&count);
|
||||
size_t count = 0;
|
||||
bool selected = false;
|
||||
xtc_project_note_count(project_.get(), &count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{}; xtc_project_get_note(project_.get(),i,¬e);
|
||||
xtc_note_info note{};
|
||||
xtc_project_get_note(project_.get(), i, ¬e);
|
||||
if (note.id == piano_->selected_note() && piano_->note_visible(note)) {
|
||||
selected = true; if (Fl::pushed() != note_velocity_) note_velocity_->value(note.velocity); break;
|
||||
selected = true;
|
||||
if (Fl::pushed() != note_velocity_)
|
||||
note_velocity_->value(note.velocity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected) note_velocity_->activate(); else note_velocity_->deactivate();
|
||||
if (selected)
|
||||
note_velocity_->activate();
|
||||
else
|
||||
note_velocity_->deactivate();
|
||||
velocity_lane_->redraw();
|
||||
}
|
||||
char position[64], tempo[40], info[512];
|
||||
char position[64], info[512];
|
||||
uint64_t tick = sequencer_->position();
|
||||
std::snprintf(position,sizeof(position),"%03llu : %u : %03u",
|
||||
static_cast<unsigned long long>(tick/3840+1),static_cast<unsigned int>((tick/960)%4+1),
|
||||
static_cast<unsigned int>(tick%960));
|
||||
set_label(transport_position_,position);
|
||||
std::snprintf(tempo,sizeof(tempo),"%.1f BPM",xtc_project_tempo(project_.get())/1000.0);
|
||||
set_label(tempo_display_,tempo);
|
||||
size_t tracks = 0, clips = 0; xtc_project_get_counts(project_.get(),&tracks,&clips);
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
xtc_track_info track{}; xtc_project_get_track(project_.get(),i,&track);
|
||||
if (track.id != sequencer_->selected_track()) continue;
|
||||
xtc_plugin_spec spec{};
|
||||
bool plugin = xtc_project_get_instrument(project_.get(),track.id,&spec) == XTC_OK;
|
||||
std::snprintf(info,sizeof(info),"%s\n\n%s\nGain %.2f | Pan %.2f\n%s%s\n\n%s",
|
||||
track.name,track.is_bus ? "BUS" : "TRACK",static_cast<double>(track.gain),static_cast<double>(track.pan),
|
||||
track.mute ? "Muted " : "",track.solo ? "Solo" : "",plugin ? spec.label : "Built-in MIDI sine");
|
||||
set_label(selection_info_,info); return;
|
||||
std::snprintf(position, sizeof(position), "%03llu : %u : %03u", static_cast<unsigned long long>(tick / 3840 + 1),
|
||||
static_cast<unsigned int>((tick / 960) % 4 + 1), static_cast<unsigned int>(tick % 960));
|
||||
set_label(transport_position_, position);
|
||||
if (!editing_number(tempo_display_))
|
||||
tempo_display_->value(xtc_project_tempo(project_.get()) / 1000.0);
|
||||
if (recorder_)
|
||||
tempo_display_->deactivate();
|
||||
else
|
||||
tempo_display_->activate();
|
||||
size_t tracks = 0, clips = 0;
|
||||
xtc_project_get_counts(project_.get(), &tracks, &clips);
|
||||
uint64_t end = 0;
|
||||
for (size_t i = 0; i < clips; ++i) {
|
||||
xtc_clip_info clip{};
|
||||
xtc_project_get_clip(project_.get(), i, &clip);
|
||||
end = std::max(end, clip.start_tick + clip.length_tick);
|
||||
}
|
||||
std::snprintf(info,sizeof(info),"No track selected\n\n%zu tracks / buses\n%zu clips\n\nCreate a MIDI track to start drawing notes, or import a WAV file.",tracks,clips);
|
||||
set_label(selection_info_,info);
|
||||
std::snprintf(info, sizeof(info), "%zu tracks / %zu clips / %llu bars", tracks, clips,
|
||||
static_cast<unsigned long long>((end + 3839) / 3840));
|
||||
set_label(session_info_, info);
|
||||
uint64_t target = tabs_->value() == tabs_->child(1) ? mixer_->selected_track() : sequencer_->selected_track();
|
||||
bool selection_changed = target != inspected_track_;
|
||||
inspected_track_ = UINT64_MAX;
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
xtc_track_info track{};
|
||||
xtc_project_get_track(project_.get(), i, &track);
|
||||
if (track.id != target)
|
||||
continue;
|
||||
inspected_track_ = track.id;
|
||||
for (auto *control : {static_cast<Fl_Widget *>(inspector_gain_), static_cast<Fl_Widget *>(inspector_pan_),
|
||||
static_cast<Fl_Widget *>(inspector_mute_), static_cast<Fl_Widget *>(inspector_solo_)}) {
|
||||
if (recorder_)
|
||||
control->deactivate();
|
||||
else
|
||||
control->activate();
|
||||
}
|
||||
if (selection_changed || !editing_number(inspector_gain_))
|
||||
inspector_gain_->value(track.gain);
|
||||
if (selection_changed || !editing_number(inspector_pan_))
|
||||
inspector_pan_->value(track.pan);
|
||||
if (Fl::pushed() != inspector_mute_)
|
||||
inspector_mute_->value(track.mute);
|
||||
if (Fl::pushed() != inspector_solo_)
|
||||
inspector_solo_->value(track.solo);
|
||||
xtc_plugin_spec spec{};
|
||||
bool plugin = xtc_project_get_instrument(project_.get(), track.id, &spec) == XTC_OK;
|
||||
std::snprintf(info, sizeof(info), "%s\n%s %s%s\n%s", track.name, track.is_bus ? "BUS" : "TRACK",
|
||||
track.mute ? "Muted " : "", track.solo ? "Solo" : "",
|
||||
track.is_bus ? "Bus output"
|
||||
: plugin ? spec.label
|
||||
: "Built-in MIDI sine");
|
||||
set_label(selection_info_, info);
|
||||
return;
|
||||
}
|
||||
for (auto *control : {static_cast<Fl_Widget *>(inspector_gain_), static_cast<Fl_Widget *>(inspector_pan_),
|
||||
static_cast<Fl_Widget *>(inspector_mute_), static_cast<Fl_Widget *>(inspector_solo_)})
|
||||
control->deactivate();
|
||||
std::snprintf(info, sizeof(info), "No track selected\nCreate a MIDI track or import a WAV to begin.");
|
||||
set_label(selection_info_, info);
|
||||
}
|
||||
|
||||
xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index)
|
||||
|
|
@ -1801,7 +1921,12 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
|||
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
|
||||
const char *label = menu->mvalue()->label();
|
||||
xtc_result r;
|
||||
if (std::strcmp(label,"Reset selected plugin") == 0) {
|
||||
if (std::strcmp(label,"Insert internal delay on wire")==0 || std::strcmp(label,"Insert internal reverb on wire")==0) {
|
||||
xtc_connection_info wire{};
|
||||
r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert internal delay on wire")==0 ? 2U : 3U) : XTC_ERR_INVALID_STATE;
|
||||
} else if (std::strcmp(label,"Add internal delay") == 0 || std::strcmp(label,"Add internal reverb") == 0) {
|
||||
size_t id=0; r=w->add_plugin(XTC_BUILTIN_PATH, std::strcmp(label,"Add internal delay")==0 ? 2U : 3U, &id);
|
||||
} else if (std::strcmp(label,"Reset selected plugin") == 0) {
|
||||
r = w->reset_plugin(w->graph_widget_->selected_node());
|
||||
} else if (std::strcmp(label,"Load plugin") == 0 ||
|
||||
std::strcmp(label,"Replace selected plugin") == 0 ||
|
||||
|
|
@ -1914,6 +2039,7 @@ void MainWindow::collect_preferences()
|
|||
}
|
||||
void MainWindow::apply_ui_preferences()
|
||||
{
|
||||
follow_->value(static_cast<int>(settings_.follow_playhead));
|
||||
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);
|
||||
|
|
@ -1940,3 +2066,119 @@ 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();
|
||||
}
|
||||
|
||||
xtc_result MainWindow::set_tempo(unsigned int milli_bpm)
|
||||
{
|
||||
if (recorder_)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
if (milli_bpm < 20000 || milli_bpm > 300000)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (milli_bpm == xtc_project_tempo(project_.get()))
|
||||
return XTC_OK;
|
||||
uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip(), mix = mixer_->selected_track();
|
||||
uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX, note = piano_ ? piano_->selected_note() : UINT64_MAX;
|
||||
bool piano_visible = piano_window_ && piano_window_->shown();
|
||||
xtc_project *candidate = nullptr;
|
||||
xtc_result r = xtc_project_clone(project_.get(), &candidate);
|
||||
if (r == XTC_OK)
|
||||
r = xtc_project_set_tempo(candidate, milli_bpm);
|
||||
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);
|
||||
else {
|
||||
sequencer_->select(track, clip);
|
||||
mixer_->select_track(mix);
|
||||
xtc_clip_info info{};
|
||||
if (piano_ && xtc_project_find_clip(project_.get(), piano_clip, &info) == XTC_OK) {
|
||||
piano_->set_project(project_.get(), piano_clip);
|
||||
piano_->select_note(note);
|
||||
if (piano_visible)
|
||||
piano_window_->show();
|
||||
}
|
||||
mark_dirty();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
void MainWindow::inspector_callback(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *w = static_cast<MainWindow *>(context);
|
||||
const uint64_t id = w->inspected_track_;
|
||||
float gain = static_cast<float>(w->inspector_gain_->value()), pan = static_cast<float>(w->inspector_pan_->value());
|
||||
int mute = w->inspector_mute_->value(), solo = w->inspector_solo_->value();
|
||||
// Editing a single field must not commit another field's unfinished input.
|
||||
size_t count = 0, clips = 0;
|
||||
xtc_project_get_counts(w->project_.get(), &count, &clips);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_track_info track{};
|
||||
xtc_project_get_track(w->project_.get(), i, &track);
|
||||
if (track.id != id)
|
||||
continue;
|
||||
if (widget != w->inspector_gain_)
|
||||
gain = track.gain;
|
||||
if (widget != w->inspector_pan_)
|
||||
pan = track.pan;
|
||||
if (widget != w->inspector_mute_)
|
||||
mute = track.mute;
|
||||
if (widget != w->inspector_solo_)
|
||||
solo = track.solo;
|
||||
if (gain != track.gain || pan != track.pan || mute != track.mute || solo != track.solo)
|
||||
if (w->mixer_->select_track(id))
|
||||
w->mixer_->mix(gain, pan, mute, solo);
|
||||
break;
|
||||
}
|
||||
w->inspected_track_ = UINT64_MAX;
|
||||
w->refresh_status();
|
||||
}
|
||||
void MainWindow::show_commands()
|
||||
{
|
||||
std::vector<std::string> commands;
|
||||
for (int i = 0; i < menu_->size(); ++i) {
|
||||
const auto &item = menu_->menu()[i];
|
||||
if (!item.label() || item.submenu() || !item.active())
|
||||
continue;
|
||||
char path[512];
|
||||
if (menu_->item_pathname(path, sizeof(path), &item) == 0)
|
||||
commands.emplace_back(path);
|
||||
}
|
||||
std::string selected;
|
||||
{
|
||||
CommandPalette palette(std::move(commands));
|
||||
selected = palette.run();
|
||||
}
|
||||
if (!selected.empty()) {
|
||||
auto *item = menu_->find_item(selected.c_str());
|
||||
if (item && item->active())
|
||||
menu_->picked(item);
|
||||
}
|
||||
}
|
||||
void MainWindow::fit_song()
|
||||
{
|
||||
size_t tracks = 0, clips = 0;
|
||||
uint64_t end = 3840;
|
||||
xtc_project_get_counts(project_.get(), &tracks, &clips);
|
||||
for (size_t i = 0; i < clips; ++i) {
|
||||
xtc_clip_info clip{};
|
||||
xtc_project_get_clip(project_.get(), i, &clip);
|
||||
end = std::max(end, clip.start_tick + clip.length_tick + 960);
|
||||
}
|
||||
int scale = 24;
|
||||
for (int candidate : {24, 48, 96, 192, 384})
|
||||
if (end * static_cast<unsigned int>(candidate) / 960 <=
|
||||
static_cast<uint64_t>(std::max(1, timeline_scroll_->w() - 204)))
|
||||
scale = candidate;
|
||||
sequencer_->set_zoom(scale);
|
||||
timeline_scroll_->scroll_to(0, 0);
|
||||
timeline_scroll_->redraw();
|
||||
}
|
||||
void MainWindow::follow_playhead()
|
||||
{
|
||||
if (!running_ || !follow_->value() || tabs_->value() != tabs_->child(0) || Fl::pushed())
|
||||
return;
|
||||
int pixel = static_cast<int>(sequencer_->position() * static_cast<unsigned int>(sequencer_->zoom_level()) / 960);
|
||||
int screen = sequencer_->x() + 180 + pixel;
|
||||
if (screen > timeline_scroll_->x() + timeline_scroll_->w() - 48 || screen < timeline_scroll_->x() + 180)
|
||||
timeline_scroll_->scroll_to(std::max(0, pixel - 160), timeline_scroll_->yposition());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ class Fl_Tabs;
|
|||
class Fl_Menu_Bar;
|
||||
class Fl_Choice;
|
||||
class Fl_Value_Slider;
|
||||
class Fl_Value_Input;
|
||||
class Fl_Scroll;
|
||||
class Fl_Check_Button;
|
||||
|
||||
class MainWindow : public Fl_Double_Window {
|
||||
|
|
@ -29,6 +31,9 @@ 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);
|
||||
xtc_result set_tempo(unsigned int milli_bpm);
|
||||
void show_commands();
|
||||
void fit_song();
|
||||
const xtc_settings &settings() const { return settings_; }
|
||||
xtc_result apply_preferences(const xtc_settings &settings);
|
||||
void show_preferences();
|
||||
|
|
@ -198,7 +203,14 @@ class MainWindow : public Fl_Double_Window {
|
|||
Fl_Value_Slider *note_velocity_ = nullptr;
|
||||
Fl_Widget *velocity_lane_ = nullptr;
|
||||
Fl_Box *transport_position_ = nullptr;
|
||||
Fl_Box *tempo_display_ = nullptr;
|
||||
Fl_Value_Input *tempo_display_ = nullptr;
|
||||
Fl_Value_Input *inspector_gain_=nullptr,*inspector_pan_=nullptr;
|
||||
Fl_Check_Button *inspector_mute_=nullptr,*inspector_solo_=nullptr,*follow_=nullptr;
|
||||
Fl_Scroll *timeline_scroll_=nullptr;
|
||||
Fl_Box *session_info_=nullptr;
|
||||
uint64_t inspected_track_=UINT64_MAX;
|
||||
static void inspector_callback(Fl_Widget *,void *);
|
||||
void follow_playhead();
|
||||
Fl_Box *selection_info_ = nullptr;
|
||||
MixerWidget *mixer_ = nullptr;
|
||||
Fl_Menu_Bar *menu_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -616,6 +616,7 @@ void SequencerWidget::refresh_instruments() try
|
|||
size_t tracks = 0, clips = 0;
|
||||
xtc_project_get_counts(project_,&tracks,&clips);
|
||||
catalog_.clear();
|
||||
for (unsigned int i=0;i<2;++i) { xtc_plugin_spec s{}; if (xtc_plugin_probe(XTC_BUILTIN_PATH,i,48000,&s)==XTC_OK) catalog_.push_back(s); }
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
xtc_track_info t{}; xtc_plugin_spec spec{};
|
||||
xtc_project_get_track(project_,i,&t);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ static int inspect(const xtc_project *project, uint64_t version)
|
|||
xtc_node_info node; xtc_plugin_spec spec;
|
||||
xtc_graph_get_node(graph,i,&node);
|
||||
if (xtc_graph_get_plugin(graph,node.id,&spec) == XTC_OK) {
|
||||
int available = readable_file(spec.path);
|
||||
int available = spec.format == XTC_PLUGIN_BUILTIN || readable_file(spec.path);
|
||||
printf("%s node %zu ID %u descriptor %u ",xtc_plugin_format_name(spec.format),node.id,spec.unique_id,spec.index);
|
||||
if (spec.identifier[0]) { quoted(spec.identifier); putchar(' '); }
|
||||
quoted(spec.label); printf(": %s ",available ? "available" : "missing/unreadable");
|
||||
|
|
@ -66,7 +66,7 @@ static int inspect(const xtc_project *project, uint64_t version)
|
|||
xtc_track_info track; xtc_plugin_spec spec;
|
||||
xtc_project_get_track(project,i,&track);
|
||||
if (xtc_project_get_instrument(project,track.id,&spec) == XTC_OK) {
|
||||
int available = readable_file(spec.path);
|
||||
int available = spec.format == XTC_PLUGIN_BUILTIN || readable_file(spec.path);
|
||||
printf("%s instrument track %" PRIu64 " descriptor %u ",xtc_plugin_format_name(spec.format),track.id,spec.index);
|
||||
quoted(spec.identifier); putchar(' '); quoted(spec.label);
|
||||
printf(": %s ",available ? "available" : "missing/unreadable"); quoted(spec.path); putchar('\n');
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
|
|||
if (!decode_bytes(input->tokens[1],p->path,sizeof(p->path))) return XTC_ERR_FORMAT;
|
||||
if (format_version >= 11U) {
|
||||
READ("FORMAT",3U); NUM(1U,value);
|
||||
if (value > XTC_PLUGIN_VST3) return XTC_ERR_UNSUPPORTED;
|
||||
if (value > (format_version >= 13U ? XTC_PLUGIN_BUILTIN : XTC_PLUGIN_VST3)) return XTC_ERR_UNSUPPORTED;
|
||||
p->format = (xtc_plugin_format)value;
|
||||
if (strcmp(input->tokens[2],"-") && !decode_bytes(input->tokens[2],p->identifier,sizeof(p->identifier))) return XTC_ERR_FORMAT;
|
||||
}
|
||||
|
|
@ -454,7 +454,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
|
|||
for (slot = 0; slot < project->track_count && project->tracks[slot].id != track; ++slot) {}
|
||||
if (slot == project->track_count || project->tracks[slot].is_bus || project->instruments[slot].instrument) return XTC_ERR_FORMAT;
|
||||
s = project->instruments+slot; s->instrument = 1; s->tempo = project->tempo;
|
||||
NUM(2U,value); if (value < XTC_PLUGIN_CLAP || value > XTC_PLUGIN_VST3) return XTC_ERR_UNSUPPORTED;
|
||||
NUM(2U,value); if (value < XTC_PLUGIN_CLAP || value > (format_version >= 13U ? XTC_PLUGIN_BUILTIN : XTC_PLUGIN_VST3)) return XTC_ERR_UNSUPPORTED;
|
||||
s->format = (xtc_plugin_format)value;
|
||||
NUM(3U,value); if (value >= 1024) return XTC_ERR_FORMAT; s->index = (unsigned int)value;
|
||||
NUM(4U,value); if (value > UINT32_MAX) return XTC_ERR_FORMAT; s->unique_id = (unsigned int)value;
|
||||
|
|
|
|||
149
src/plugin/builtin.c
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/* Original, dependency-free mono devices. All storage is prepared off-thread. */
|
||||
#include "builtin.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#define TAU 6.2831853071795864769
|
||||
static const char *names[] = {"Soft Keys", "Sub Bass", "Delay", "Reverb"};
|
||||
static const char *controls[2][3] = {{"Time", "Feedback", "Mix"}, {"Decay", "Damping", "Mix"}};
|
||||
static const float defaults[2][3] = {{350, .35f, .25f}, {.7f, .45f, .25f}};
|
||||
static const float lows[2][3] = {{1,0,0},{0,0,0}};
|
||||
static const float highs[2][3] = {{2000,.95f,1},{.95f,.95f,1}};
|
||||
typedef struct { double phase, step; float level, velocity; unsigned int note, channel; int active, released, held; } voice;
|
||||
struct xtc_builtin {
|
||||
xtc_plugin_spec spec;
|
||||
double rate;
|
||||
float attack, release;
|
||||
size_t max_frames, length, cursor;
|
||||
float *line, damp[4];
|
||||
size_t comb[4];
|
||||
voice voices[32];
|
||||
double frequencies[128];
|
||||
int sustain[16];
|
||||
};
|
||||
xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out)
|
||||
{
|
||||
unsigned int i; xtc_plugin_spec s = {0};
|
||||
if (!out) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (index >= 4) return XTC_ERR_NOT_FOUND;
|
||||
strcpy(s.path,XTC_BUILTIN_PATH); strcpy(s.label,names[index]);
|
||||
s.format = XTC_PLUGIN_BUILTIN; s.index = index; s.unique_id = 0x58544300U+index;
|
||||
s.instrument = index < 2; s.inputs = index < 2 ? 0U : 1U; s.tempo = 120000;
|
||||
s.control_count = index < 2 ? 0U : 3U;
|
||||
for (i=0;i<s.control_count;++i) { s.ports[i]=i; s.values[i]=defaults[index-2][i]; }
|
||||
*out=s; return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_builtin_validate(const xtc_plugin_spec *s)
|
||||
{
|
||||
xtc_plugin_spec a; unsigned int i;
|
||||
if (!s || xtc_builtin_probe(s->index,&a)!=XTC_OK || strcmp(s->path,a.path) || strcmp(s->label,a.label) ||
|
||||
s->identifier[0] || s->unique_id!=a.unique_id || s->instrument!=a.instrument || s->inputs!=a.inputs ||
|
||||
s->control_count!=a.control_count) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<s->control_count;++i) if(s->ports[i]!=i || !isfinite(s->values[i]) ||
|
||||
s->values[i]<lows[s->index-2][i] || s->values[i]>highs[s->index-2][i]) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<s->point_count;++i) if(s->index<2 || s->points[i].port>=3 || !isfinite(s->points[i].value) ||
|
||||
s->points[i].value<lows[s->index-2][s->points[i].port] || s->points[i].value>highs[s->index-2][s->points[i].port]) return XTC_ERR_INVALID_ARGUMENT;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out)
|
||||
{
|
||||
unsigned int i;
|
||||
if (!out || xtc_plugin_spec_validate(s)!=XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
|
||||
memset(out,0,sizeof(*out)); snprintf(out->name,sizeof(out->name),"%s",s->label);
|
||||
strcpy(out->maker,"xtc"); strcpy(out->copyright,"See xtc LICENSE"); out->hard_realtime=1;
|
||||
for(i=0;i<s->control_count;++i) {
|
||||
xtc_parameter_info *p=out->parameters+i; p->id=i;
|
||||
strcpy(p->name,controls[s->index-2][i]); if(s->index==2 && i==0) strcpy(p->unit,"ms");
|
||||
p->minimum=lows[s->index-2][i]; p->maximum=highs[s->index-2][i]; p->default_value=defaults[s->index-2][i];
|
||||
out->flags[i]=XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_builtin **out)
|
||||
{
|
||||
xtc_builtin *p; unsigned int i;
|
||||
if (!out || xtc_plugin_spec_validate(s)!=XTC_OK || !isfinite(rate) || rate<1 || rate>768000 || !frames || frames>65536) return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out=NULL; p=calloc(1,sizeof(*p)); if(!p) return XTC_ERR_OUT_OF_MEMORY;
|
||||
p->spec=*s; p->rate=rate; p->attack=(float)(1-exp(-1/(.008*rate))); p->release=(float)exp(-1/(.08*rate)); p->max_frames=frames; p->length=(size_t)ceil(rate*2)+2;
|
||||
if(!s->instrument) { p->line=calloc(p->length*4,sizeof(float)); if(!p->line) {free(p);return XTC_ERR_OUT_OF_MEMORY;} }
|
||||
for(i=0;i<128;++i) p->frequencies[i]=440*pow(2,((double)i-69)/12)/rate;
|
||||
for(i=0;i<4;++i) { static const double times[]={.0297,.0371,.0411,.0437}; p->comb[i]=(size_t)(rate*times[i])+1; }
|
||||
*out=p; return XTC_OK;
|
||||
}
|
||||
void xtc_builtin_destroy(xtc_builtin *p) { if(p) {free(p->line);free(p);} }
|
||||
static float parameter(const xtc_builtin *p, unsigned int port, uint64_t position)
|
||||
{
|
||||
const xtc_plugin_point *a=NULL,*b=NULL; unsigned int i;
|
||||
double tick=(double)position*p->spec.tempo*XTC_TICKS_PER_QUARTER/(p->rate*60000);
|
||||
for(i=0;i<p->spec.point_count;++i) { const xtc_plugin_point *q=p->spec.points+i;
|
||||
if(q->port!=port) continue;
|
||||
if((double)q->tick<=tick) { if(!a || q->tick>a->tick) a=q; }
|
||||
else if(!b || q->tick<b->tick) b=q;
|
||||
}
|
||||
if(!a) return b ? b->value : p->spec.values[port];
|
||||
if(!b || a->step) return a->value;
|
||||
return (float)(a->value+(b->value-a->value)*(tick-(double)a->tick)/(double)(b->tick-a->tick));
|
||||
}
|
||||
xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position)
|
||||
{
|
||||
size_t i; unsigned int j;
|
||||
if(!p || !out || frames>p->max_frames || p->spec.instrument || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<frames;++i) {
|
||||
float x=in && isfinite(in[i]) ? in[i] : 0, wet=0;
|
||||
float a=parameter(p,0,position+i), b=parameter(p,1,position+i), mix=parameter(p,2,position+i);
|
||||
if(p->spec.index==2) {
|
||||
size_t delay=(size_t)(a*p->rate/1000); if(!delay) delay=1;
|
||||
wet=p->line[(p->cursor+p->length-delay)%p->length];
|
||||
p->line[p->cursor]=x+wet*b;
|
||||
} else {
|
||||
for(j=0;j<4;++j) {
|
||||
float *line=p->line+j*p->length;
|
||||
float y=line[(p->cursor+p->length-p->comb[j])%p->length];
|
||||
p->damp[j]=(1-b)*y+b*p->damp[j];
|
||||
line[p->cursor]=x+a*p->damp[j]; wet+=y*.25f;
|
||||
}
|
||||
}
|
||||
out[i]=(1-mix)*x+mix*wet; p->cursor=(p->cursor+1)%p->length;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
static void midi_event(xtc_builtin *p, const xtc_midi_event *e)
|
||||
{
|
||||
unsigned int j,slot=0;
|
||||
if(e->type==XTC_MIDI_ALL_OFF) { memset(p->voices,0,sizeof(p->voices));memset(p->sustain,0,sizeof(p->sustain));return; }
|
||||
if(e->type==XTC_MIDI_CONTROL) {
|
||||
if(e->pitch==64) {p->sustain[e->channel]=e->velocity>=64; if(e->velocity<64) for(j=0;j<32;++j) if(p->voices[j].channel==e->channel && p->voices[j].held) p->voices[j].released=1;}
|
||||
if(e->pitch==120 || e->pitch==123) for(j=0;j<32;++j) if(p->voices[j].channel==e->channel) p->voices[j].active=0;
|
||||
return;
|
||||
}
|
||||
if(e->type==XTC_MIDI_NOTE_OFF || !e->velocity) {
|
||||
for(j=0;j<32;++j) if(p->voices[j].channel==e->channel && p->voices[j].note==e->pitch) {
|
||||
p->voices[j].held=1; p->voices[j].released=!p->sustain[e->channel];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for(j=0;j<32;++j) { if(!p->voices[j].active) {slot=j;break;} if(p->voices[j].level<p->voices[slot].level) slot=j; }
|
||||
{voice *v=p->voices+slot; memset(v,0,sizeof(*v));v->active=1;v->note=e->pitch;v->channel=e->channel;v->velocity=(float)e->velocity/127;v->step=p->frequencies[e->pitch];}
|
||||
}
|
||||
xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames)
|
||||
{
|
||||
size_t i,k=0; unsigned int j;
|
||||
if(!p || !p->spec.instrument || !out || frames>p->max_frames || count>256 || (count && !events)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i) if(events[i].offset>=frames || (i && events[i].offset<events[i-1].offset) ||
|
||||
events[i].channel>15 || events[i].pitch>127 || events[i].velocity>127 || events[i].type<XTC_MIDI_NOTE_ON || events[i].type>XTC_MIDI_CONTROL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<frames;++i) {
|
||||
float sum=0; while(k<count && events[k].offset==i) midi_event(p,events+k++);
|
||||
for(j=0;j<32;++j) { voice *v=p->voices+j; double wave;
|
||||
if(!v->active) continue;
|
||||
if(v->released) v->level *= p->release;
|
||||
else v->level += (1-v->level)*p->attack;
|
||||
if(v->released && v->level<.00001f) {v->active=0;continue;}
|
||||
wave=v->step<.49 ? sin(TAU*v->phase) : 0;
|
||||
if(p->spec.index==0 && v->step<.16) wave+=.3*sin(3*TAU*v->phase);
|
||||
sum+=(float)wave*v->level*v->velocity*.12f;
|
||||
v->phase+=v->step; v->phase-=floor(v->phase);
|
||||
} out[i]=sum;
|
||||
} return XTC_OK;
|
||||
}
|
||||
12
src/plugin/builtin.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef XTC_BUILTIN_H
|
||||
#define XTC_BUILTIN_H
|
||||
#include "xtc/xtc_plugin.h"
|
||||
typedef struct xtc_builtin xtc_builtin;
|
||||
xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out);
|
||||
xtc_result xtc_builtin_validate(const xtc_plugin_spec *s);
|
||||
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out);
|
||||
xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_builtin **out);
|
||||
void xtc_builtin_destroy(xtc_builtin *p);
|
||||
xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position);
|
||||
xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames);
|
||||
#endif
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#include "modern.h"
|
||||
#include "builtin.h"
|
||||
#include <dlfcn.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -10,14 +11,15 @@ xtc_result xtc_ladspa_describe(const xtc_plugin_spec *, double, xtc_plugin_detai
|
|||
xtc_result xtc_ladspa_create(const xtc_plugin_spec *, double, size_t, xtc_ladspa_plugin **);
|
||||
void xtc_ladspa_destroy(xtc_ladspa_plugin *);
|
||||
xtc_result xtc_ladspa_process_at(xtc_ladspa_plugin *, const float *, float *, size_t, uint64_t);
|
||||
struct xtc_plugin { xtc_ladspa_plugin *ladspa; xtc_modern *modern; uint64_t next; };
|
||||
struct xtc_plugin { xtc_ladspa_plugin *ladspa; xtc_modern *modern; xtc_builtin *builtin; uint64_t next; };
|
||||
const char *xtc_plugin_format_name(xtc_plugin_format f)
|
||||
{ static const char *names[] = {"LADSPA", "CLAP", "VST2", "VST3"}; return f >= 0 && f <= XTC_PLUGIN_VST3 ? names[f] : "Unknown"; }
|
||||
{ static const char *names[] = {"LADSPA", "CLAP", "VST2", "VST3", "Internal"}; return f >= 0 && f <= XTC_PLUGIN_BUILTIN ? names[f] : "Unknown"; }
|
||||
xtc_result xtc_plugin_probe_format(xtc_plugin_format f, const char *path, unsigned int index, double rate, xtc_plugin_spec *out)
|
||||
{
|
||||
xtc_modern *p = NULL; xtc_result r;
|
||||
if (!out || !path || path[0] != '/' || strlen(path) >= 512 || index >= 1024 || !isfinite(rate) || rate < 1 || rate > 768000)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (f == XTC_PLUGIN_BUILTIN) return !strcmp(path,XTC_BUILTIN_PATH) ? xtc_builtin_probe(index,out) : XTC_ERR_INVALID_ARGUMENT;
|
||||
if (f == XTC_PLUGIN_LADSPA) return xtc_ladspa_probe(path,index,rate,out);
|
||||
if (f < XTC_PLUGIN_CLAP || f > XTC_PLUGIN_VST3) return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = xtc_modern_open(f,path,index,rate,256,&p,out);
|
||||
|
|
@ -27,6 +29,7 @@ xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, x
|
|||
{
|
||||
void *module; xtc_plugin_format f = XTC_PLUGIN_LADSPA;
|
||||
if (!path || path[0] != '/' || strlen(path) >= 512) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!strcmp(path,XTC_BUILTIN_PATH)) return xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,path,index,rate,out);
|
||||
module = dlopen(path,RTLD_NOW | RTLD_LOCAL);
|
||||
if (!module) return XTC_ERR_NOT_FOUND;
|
||||
if (dlsym(module,"clap_entry")) f = XTC_PLUGIN_CLAP;
|
||||
|
|
@ -39,6 +42,8 @@ xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin
|
|||
{
|
||||
xtc_plugin_spec actual; xtc_result r;
|
||||
if (!out || xtc_plugin_spec_validate(s) != XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!isfinite(rate) || rate < 1 || rate > 768000) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (s->format == XTC_PLUGIN_BUILTIN) return xtc_builtin_describe(s,out);
|
||||
if (s->format == XTC_PLUGIN_LADSPA) return xtc_ladspa_describe(s,rate,out);
|
||||
r = xtc_plugin_probe_format(s->format,s->path,s->index,rate,&actual);
|
||||
if (r != XTC_OK) return r;
|
||||
|
|
@ -49,7 +54,7 @@ xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin
|
|||
return XTC_OK;
|
||||
}
|
||||
void xtc_plugin_destroy(xtc_plugin *p)
|
||||
{ if (p) { xtc_ladspa_destroy(p->ladspa); xtc_modern_destroy(p->modern); free(p); } }
|
||||
{ if (p) { xtc_builtin_destroy(p->builtin); xtc_ladspa_destroy(p->ladspa); xtc_modern_destroy(p->modern); free(p); } }
|
||||
xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_plugin **out)
|
||||
{
|
||||
xtc_plugin *p; xtc_result r; xtc_plugin_spec actual;
|
||||
|
|
@ -58,7 +63,8 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame
|
|||
r = xtc_plugin_spec_validate(s); if (r != XTC_OK) return r;
|
||||
if (!frames || frames > 65536 || !isfinite(rate) || rate < 1 || rate > 768000) return XTC_ERR_INVALID_ARGUMENT;
|
||||
p = calloc(1,sizeof(*p)); if (!p) return XTC_ERR_OUT_OF_MEMORY;
|
||||
if (s->format == XTC_PLUGIN_LADSPA) r = xtc_ladspa_create(s,rate,frames,&p->ladspa);
|
||||
if (s->format == XTC_PLUGIN_BUILTIN) r = xtc_builtin_create(s,rate,frames,&p->builtin);
|
||||
else if (s->format == XTC_PLUGIN_LADSPA) r = xtc_ladspa_create(s,rate,frames,&p->ladspa);
|
||||
else {
|
||||
r = xtc_modern_open(s->format,s->path,s->index,rate,frames,&p->modern,&actual);
|
||||
if (r == XTC_OK && (actual.instrument != s->instrument || actual.unique_id != s->unique_id || actual.inputs != s->inputs ||
|
||||
|
|
@ -71,12 +77,12 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *in, float *out, siz
|
|||
{
|
||||
xtc_result r;
|
||||
if (!p) return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = p->modern ? xtc_modern_process(p->modern,in,out,frames) : xtc_ladspa_process_at(p->ladspa,in,out,frames,position);
|
||||
r = p->builtin ? xtc_builtin_process(p->builtin,in,out,frames,position) : p->modern ? xtc_modern_process(p->modern,in,out,frames) : xtc_ladspa_process_at(p->ladspa,in,out,frames,position);
|
||||
if (r == XTC_OK) p->next = position <= UINT64_MAX-frames ? position+frames : 0;
|
||||
return r;
|
||||
}
|
||||
xtc_result xtc_plugin_process_midi(xtc_plugin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames)
|
||||
{ return p && p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; }
|
||||
{ return p && p->builtin ? xtc_builtin_midi(p->builtin,events,count,out,frames) : p && p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; }
|
||||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *in, float *out, size_t frames)
|
||||
{ return xtc_plugin_process_at(p,in,out,frames,p ? p->next : 0); }
|
||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
/* Header-only optional dependency; absent development header gives explicit
|
||||
* unsupported stubs, preserving metadata-only load/save of plugin documents. */
|
||||
#include "config.h"
|
||||
#include "builtin.h"
|
||||
static int terminated(const char *s, size_t n) { return memchr(s, 0, n) != NULL; }
|
||||
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
|
||||
{
|
||||
|
|
@ -24,9 +25,11 @@ xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
|
|||
!terminated(s->label,sizeof(s->label)) || !s->label[0] || s->index >= 1024 ||
|
||||
s->inputs > 1 || s->control_count > XTC_PLUGIN_CONTROLS ||
|
||||
s->tempo < 20000 || s->tempo > 300000 || s->point_count > XTC_PLUGIN_POINTS) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (s->format < XTC_PLUGIN_LADSPA || s->format > XTC_PLUGIN_VST3 ||
|
||||
if (s->format < XTC_PLUGIN_LADSPA || s->format > XTC_PLUGIN_BUILTIN ||
|
||||
!terminated(s->identifier,sizeof(s->identifier))) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (s->format != XTC_PLUGIN_LADSPA) {
|
||||
if (s->format == XTC_PLUGIN_BUILTIN) {
|
||||
if (xtc_builtin_validate(s) != XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
|
||||
} else if (s->format != XTC_PLUGIN_LADSPA) {
|
||||
if (s->instrument > 1 || s->inputs != (s->instrument ? 0U : 1U) || s->control_count || s->point_count ||
|
||||
((s->format == XTC_PLUGIN_CLAP || s->format == XTC_PLUGIN_VST3) && !s->identifier[0]))
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ static const xtc_setting_info schema[] = {
|
|||
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)
|
||||
NUM("transport",loop_enabled,"Loop enabled: 0 or 1",0,1),
|
||||
NUM("transport",follow_playhead,"Follow playhead: 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; }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "../../src/gui/mixer_widget.hpp"
|
||||
#include "../../src/gui/plugin_dialog.hpp"
|
||||
#include "../../src/gui/preferences_dialog.hpp"
|
||||
#include "../../src/gui/command_palette.hpp"
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
#include "../../src/gui/plugin_library.hpp"
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
|
|
@ -856,7 +858,7 @@ int main(int argc, char **argv)
|
|||
CHECK(window.create_track(true) == XTC_OK);
|
||||
uint64_t second = sequence->selected_track();
|
||||
auto *selector = sequence->instrument_choice(second); CHECK(selector && selector->value() == 0);
|
||||
CHECK(selector->size() == 4); // Sine, current project instrument, load, terminator.
|
||||
CHECK(selector->size() == 6); // Sine, two internal presets, project instrument, load, terminator.
|
||||
selector->value(1); selector->do_callback();
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_OK && spec.instrument);
|
||||
CHECK(sequence->selected_track() == second);
|
||||
|
|
@ -1080,6 +1082,58 @@ int main(int argc, char **argv)
|
|||
}
|
||||
xtc_project_destroy(project);
|
||||
}
|
||||
{
|
||||
reset();MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK);
|
||||
auto *seq=timeline(&window);auto clip=seq->selected_clip(),track=seq->selected_track();
|
||||
CHECK(window.set_tempo(135500)==XTC_OK && xtc_project_tempo(seq->project())==135500);
|
||||
CHECK(seq->selected_clip()==clip && seq->selected_track()==track);
|
||||
CHECK(window.undo()==XTC_OK && xtc_project_tempo(seq->project())==120000);
|
||||
CHECK(window.undo(true)==XTC_OK && xtc_project_tempo(seq->project())==135500);
|
||||
const auto *before=seq->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;
|
||||
CHECK(window.set_tempo(100000)==XTC_ERR_OUT_OF_MEMORY && seq->project()==before);
|
||||
reserve_result=XTC_OK;CHECK(window.set_tempo(0)==XTC_ERR_INVALID_ARGUMENT);
|
||||
submit_result=XTC_ERR_CAPACITY;CHECK(window.set_tempo(100000)==XTC_ERR_CAPACITY && seq->project()==before);submit_result=XTC_OK;
|
||||
CHECK(window.set_tempo(135500)==XTC_OK);
|
||||
auto *bpm=dynamic_cast<Fl_Value_Input *>(find(&window,"BPM"));CHECK(bpm);
|
||||
if(bpm){bpm->value(140);bpm->do_callback();CHECK(xtc_project_tempo(seq->project())==140000);
|
||||
bpm->value(900);bpm->do_callback();CHECK(xtc_project_tempo(seq->project())==140000 && bpm->value()==140);}
|
||||
|
||||
auto *gain=dynamic_cast<Fl_Value_Input *>(find(&window,"Gain"));CHECK(gain);pump();
|
||||
if(gain){gain->value(.6);gain->do_callback();}xtc_track_info info{};xtc_project_get_track(seq->project(),0,&info);CHECK(info.gain==.6f);
|
||||
if(gain){Fl::focus(&gain->input);gain->input.value("0.123");pump();CHECK(std::strcmp(gain->input.value(),"0.123")==0);Fl::focus(nullptr);}
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_get_track(seq->project(),0,&info);CHECK(info.gain==1);
|
||||
CHECK(seq->set_zoom(384)==XTC_OK);window.fit_song();CHECK(seq->zoom_level()<384);
|
||||
CHECK(contains(&window,"1 tracks / 1 clips"));
|
||||
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();auto *search=dynamic_cast<Fl_Input *>(find(dialog,"Search commands"));CHECK(search);
|
||||
if(search){search->value("view/mixer");search->do_callback();}press(find(dialog,"Run command"));if(dialog->shown()){CHECK(false);dialog->hide();}});
|
||||
window.show_commands();CHECK(window.mixer()->visible_r());
|
||||
auto *bar=dynamic_cast<Fl_Menu_Bar *>(window.child(0));CHECK(bar);if(bar)bar->picked(bar->find_item("View/Sequencer"));
|
||||
auto *follow=dynamic_cast<Fl_Check_Button *>(find(&window,"Follow playhead"));CHECK(follow);
|
||||
if(follow){follow->value(1);follow->do_callback();CHECK(window.settings().follow_playhead==1);}
|
||||
press(find(&window,"Play"));engine_status.sample_position=960000;pump();
|
||||
auto *scroll=dynamic_cast<Fl_Scroll *>(seq->parent());CHECK(scroll && scroll->xposition()>0);
|
||||
press(find(&window,"Stop"));window.fit_song();CHECK(scroll && scroll->xposition()==0);
|
||||
|
||||
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();auto *search=dynamic_cast<Fl_Input *>(find(dialog,"Search commands"));
|
||||
CHECK(search);if(search){search->value("does not exist");search->do_callback();}press(find(dialog,"Run command"));CHECK(dialog->shown());press(find(dialog,"Cancel"));});
|
||||
window.show_commands();
|
||||
CHECK(window.create_track(true)==XTC_OK);uint64_t second=seq->selected_track();
|
||||
window.mixer()->select_track(track);if(bar)bar->picked(bar->find_item("View/Mixer"));pump();
|
||||
if(gain){gain->value(.4);gain->do_callback();}xtc_project_get_track(seq->project(),0,&info);CHECK(info.id==track && info.gain==.4f);
|
||||
xtc_project_get_track(seq->project(),1,&info);CHECK(info.id==second && info.gain==1);
|
||||
window.do_callback();
|
||||
}
|
||||
{
|
||||
CommandPalette palette({"View/Mixer","File/Open"});
|
||||
Fl::add_timeout(.05,[](void *context){auto *p=static_cast<CommandPalette *>(context);auto *search=dynamic_cast<Fl_Input *>(find(p,"Search commands"));CHECK(search);
|
||||
if(search){search->value("FILE");search->do_callback();}int old=Fl::e_keysym;Fl::e_keysym=FL_Enter;CHECK(p->handle(FL_KEYDOWN)==1);Fl::e_keysym=old;},&palette);
|
||||
CHECK(palette.run()=="File/Open");
|
||||
}
|
||||
{
|
||||
CommandPalette palette({"View/Mixer"});
|
||||
Fl::add_timeout(.05,[](void *context){int old=Fl::e_keysym;Fl::e_keysym=FL_Escape;static_cast<CommandPalette *>(context)->handle(FL_KEYDOWN);Fl::e_keysym=old;},&palette);
|
||||
CHECK(palette.run().empty());
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ def main():
|
|||
run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')])
|
||||
assert (docs / 'WORKSPACE.md').is_file()
|
||||
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
|
||||
assert 'Project formats: 1..12' in diagnose and 'JACK compiled:' in diagnose
|
||||
assert 'Project formats: 1..13' in diagnose and 'JACK compiled:' in diagnose
|
||||
# Replacing a file symlink must not overwrite an unrelated target.
|
||||
protected = stage / 'unrelated-target'
|
||||
protected.write_text('keep unrelated target\n')
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
|
|||
project.write_text(malformed)
|
||||
check("--validate", project, 1)
|
||||
for text, messages in [
|
||||
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..12")),
|
||||
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..13")),
|
||||
("XTC_PROJECT 10\n", ("line 2", "APP")),
|
||||
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
|
||||
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),
|
||||
|
|
|
|||
73
tests/unit/test_builtin.c
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_plugin.h"
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
static void effects(unsigned int id)
|
||||
{
|
||||
xtc_plugin_spec s; xtc_plugin *a=NULL,*b=NULL; float in[256]={0},x[256],y[256]; size_t k,i; double energy=0;
|
||||
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&s)==XTC_OK);
|
||||
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK);
|
||||
assert(xtc_plugin_create(&s,48000,256,&b)==XTC_OK);
|
||||
for(k=0;k<600;++k) {
|
||||
in[0]=k==0 ? 1 : 0;
|
||||
assert(xtc_plugin_process(a,in,x,256)==XTC_OK);
|
||||
assert(xtc_plugin_process(b,in,y,128)==XTC_OK);
|
||||
assert(xtc_plugin_process(b,in+128,y+128,128)==XTC_OK);
|
||||
for(i=0;i<256;++i) {assert(isfinite(x[i]));assert(fabsf(x[i]-y[i])<1e-6f);if(k>0)energy+=fabs(x[i]);}
|
||||
}
|
||||
assert(energy>.01);xtc_plugin_destroy(a);xtc_plugin_destroy(b);
|
||||
s.values[2]=0; assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK);
|
||||
in[0]=1;assert(xtc_plugin_process(a,in,x,256)==XTC_OK);assert(!memcmp(in,x,sizeof(in)));xtc_plugin_destroy(a);
|
||||
s.values[1]=2;assert(xtc_plugin_spec_validate(&s)!=XTC_OK);
|
||||
}
|
||||
static void synth(unsigned int id)
|
||||
{
|
||||
xtc_plugin_spec s; xtc_plugin *p=NULL; float out[256]; size_t i,k; double energy=0;
|
||||
xtc_midi_event e={0,XTC_MIDI_NOTE_ON,0,60,100};
|
||||
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&s)==XTC_OK);
|
||||
assert(xtc_plugin_create(&s,48000,256,&p)==XTC_OK);
|
||||
assert(xtc_plugin_process_midi(p,&e,1,out,256)==XTC_OK);
|
||||
for(i=0;i<256;++i)energy+=fabs(out[i]);
|
||||
assert(energy>0);
|
||||
e.type=XTC_MIDI_NOTE_OFF;assert(xtc_plugin_process_midi(p,&e,1,out,256)==XTC_OK);
|
||||
for(k=0;k<200;++k)assert(xtc_plugin_process_midi(p,NULL,0,out,256)==XTC_OK);
|
||||
for(i=0;i<256;++i)assert(fabsf(out[i])<1e-5f);
|
||||
e.type=XTC_MIDI_ALL_OFF;assert(xtc_plugin_process_midi(p,&e,1,out,256)==XTC_OK);
|
||||
for(i=0;i<256;++i)assert(out[i]==0);
|
||||
xtc_plugin_destroy(p);
|
||||
}
|
||||
static void automation(void)
|
||||
{
|
||||
xtc_plugin_spec s; xtc_plugin *p=NULL; float in[128],out[128]; size_t i;
|
||||
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,2,960,&s)==XTC_OK);
|
||||
s.tempo=60000; s.values[0]=2000; s.point_count=2;
|
||||
s.points[0]=(xtc_plugin_point){0,2,0,0}; s.points[1]=(xtc_plugin_point){100,2,0,1};
|
||||
assert(xtc_plugin_create(&s,960,128,&p)==XTC_OK);
|
||||
for(i=0;i<128;++i)in[i]=1;
|
||||
assert(xtc_plugin_process_at(p,in,out,128,0)==XTC_OK);
|
||||
for(i=0;i<128;++i) assert(fabsf(out[i]-(i<100 ? 1-(float)i/100 : 0))<1e-6f);
|
||||
xtc_plugin_destroy(p);
|
||||
}
|
||||
static void roundtrip(void)
|
||||
{
|
||||
xtc_project *p=NULL,*q=NULL; xtc_plugin_spec s,t; uint64_t track; size_t id; xtc_graph *g=NULL;
|
||||
char path[]="/tmp/xtc-builtin-XXXXXX"; int fd=mkstemp(path); assert(fd>=0); close(fd);
|
||||
assert(xtc_project_create(&p)==XTC_OK); assert(xtc_project_create(&q)==XTC_OK);
|
||||
assert(xtc_project_add_track(p,"Keys",&track)==XTC_OK);
|
||||
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,0,48000,&s)==XTC_OK);
|
||||
assert(xtc_project_set_instrument(p,track,&s)==XTC_OK);
|
||||
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,2,48000,&s)==XTC_OK); s.values[0]=250;
|
||||
s.point_count=2; s.points[0]=(xtc_plugin_point){0,2,0,.1f}; s.points[1]=(xtc_plugin_point){960,2,0,.6f};
|
||||
assert(xtc_graph_clone(xtc_project_graph(p),&g)==XTC_OK);
|
||||
assert(xtc_graph_add_plugin(g,&s,&id)==XTC_OK); assert(xtc_project_set_graph(p,g)==XTC_OK);xtc_graph_destroy(g);
|
||||
assert(xtc_project_save(p,path)==XTC_OK); assert(xtc_project_load(q,path)==XTC_OK);
|
||||
assert(xtc_graph_get_plugin(xtc_project_graph(q),id,&t)==XTC_OK); assert(t.values[0]==250 && t.point_count==2);
|
||||
assert(xtc_project_get_instrument(q,track,&t)==XTC_OK && t.index==0 && t.format==XTC_PLUGIN_BUILTIN);
|
||||
xtc_project_destroy(p);xtc_project_destroy(q);unlink(path);
|
||||
}
|
||||
int main(void) {automation();roundtrip();effects(2);effects(3);synth(0);synth(1);puts("Built-in devices passed.");return 0;}
|
||||
|
|
@ -68,7 +68,7 @@ int main(void)
|
|||
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
|
||||
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
|
||||
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
|
||||
diagnostic.format_version == 12 && !diagnostic.line);
|
||||
diagnostic.format_version == 13 && !diagnostic.line);
|
||||
for (mode = 1; mode <= 5; ++mode) {
|
||||
fault = mode;
|
||||
assert(xtc_project_save(next,song) == XTC_ERR_IO);
|
||||
|
|
@ -81,7 +81,7 @@ int main(void)
|
|||
}
|
||||
write_text(bad,"XTC_PROJECT 999\n");
|
||||
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
|
||||
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..12"));
|
||||
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..13"));
|
||||
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
|
||||
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
|
||||
!strcmp(diagnostic.record,"TEMPO"));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ int main(void)
|
|||
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;
|
||||
strcpy(a.clap_paths,"~/space folder:/tmp/quoted\"name\\path");a.loop_enabled=1;a.follow_playhead=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);
|
||||
|
|
|
|||