This commit is contained in:
Johannes Findeisen 2026-10-28 17:16:01 +01:00
commit 02e27a33a2
47 changed files with 882 additions and 62 deletions

View file

@ -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
View 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.

View file

@ -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;

View file

@ -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).

View file

@ -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).

View file

@ -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.

View file

@ -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.

View file

@ -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 */

View file

@ -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

View file

@ -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;

View file

@ -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.
![Overview of xtc screenshots](overview.png)
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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

BIN
screenshots/commands.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

102
src/gui/command_palette.hpp Normal file
View 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

View file

@ -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,&note);
xtc_note_info note{};
xtc_project_get_note(project_.get(), i, &note);
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());
}

View file

@ -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;

View file

@ -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);

View file

@ -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');

View file

@ -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
View 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
View 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

View file

@ -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])

View file

@ -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;

View file

@ -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; }

View file

@ -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;

View file

@ -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')

View file

@ -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
View 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;}

View file

@ -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"));

View file

@ -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);