1
0
Fork 0
forked from hanez/xtc

Phase 26: add common parameter targets, step automation and MIDI control mapping

This commit is contained in:
Johannes Findeisen 2026-10-27 16:37:59 +01:00
commit 901a536f10
38 changed files with 740 additions and 67 deletions

View file

@ -15,7 +15,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/audio/waveform.c src/plugin/ladspa.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/control/model.c src/audio/waveform.c src/plugin/ladspa.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)
@ -409,3 +409,11 @@ test: test-buses
-include $(BUILD_DIR)/tests/unit/test_buses.d
-include $(BUILD_DIR)/src/gui/plugin_dialog.d
$(BUILD_DIR)/test_parameters: $(BUILD_DIR)/tests/unit/test_parameters.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-parameters
test-parameters: $(BUILD_DIR)/test_parameters
./$(BUILD_DIR)/test_parameters
test: test-parameters
-include $(BUILD_DIR)/tests/unit/test_parameters.d

View file

@ -145,3 +145,7 @@ clipping indicators, persisted in v9 (reading v1–v8). See [MIXER.md](docs/MIXE
Phase 25 adds LADSPA descriptor browsing, an identity/parameter editor with
reset, transactional replacement and insertion into graph wires. See
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md) for compatibility and lifecycle limits.
Phase 26 adds common parameter targets, Linear/Hold mixer automation and saved
per-channel live MIDI CC bindings. Project v10 reads v1–v9. See
[CONTROL.md](docs/CONTROL.md) for timing, precedence and qualification limits.

View file

@ -1,7 +1,7 @@
# Automation and expressive control (Phase 16)
# Automation and expressive control (Phases 16 and 26)
Track Gain/Pan lanes in the Mixer apply to audio and scheduled instrument voices.
They use absolute project ticks, linear interpolation, hold-first/last outside
They use absolute project ticks, linear or hold interpolation, hold-first/last outside
points and the constant project tempo snapshot. A clip trim/move does not move
track or effect automation: lanes belong to the project timeline. Instrument
volume is controlled by its track Gain lane; the current sine has no timbre
@ -25,9 +25,9 @@ clock is called from the host audio path. Native ABI 2 callers continue to use
sample-offset parameter events, applied before equal-offset MIDI events.
Live MIDI input maps CC7 to per-channel volume (0..1), CC10 to pan (64 center),
and CC64 to sustain (>=64 down). Volume/pan ramp over 5 ms. These fixed mappings
and CC64 to sustain (>=64 down). Volume/pan ramp over 5 ms. These default mappings
apply to live sine voices, not track faders or prerecorded clips; controller
capture, configurable learn and plugin MIDI are not supported. CC120/123 and
capture, automatic learn and plugin MIDI are not supported. CC120/123 and
Panic release all live voices and sustain and suppress already-started scheduled
notes. Note-on velocity zero decodes to note-off. Repeated live note-on on one
channel/pitch retriggers one voice; note-off releases it or marks it pending while
@ -47,3 +47,101 @@ make test test-control test-plugins test-gui-plugins test-plugins-real
Physical controller devices and listening remain unqualified; no tempo map,
latency compensation, MIDI capture, pitch bend or plugin crash isolation is added.
## Phase 26: common parameter targets
`xtc_control.h` identifies a parameter by `(kind, stable owner ID, parameter ID)`.
IDs never mean a dense list index and are not DSP pointers:
| Kind | Owner | Parameter IDs | Metadata/reset defaults |
| --- | --- | --- | --- |
| TRACK | Track or mix-bus stable uint64 ID | 0 gain, 1 pan | Gain 0..4 linear, default 1; pan -1..1 balance, default 0 |
| PLUGIN | LADSPA graph node ID | Actual input-control port | Descriptor name, bounds, default and discrete/logarithmic hints; units unspecified |
| LIVE | MIDI channel 0..15 | 0 volume, 1 pan, 2 sustain | Volume 0..1 default 1; pan -1..1 default 0; sustain boolean default 0 |
`xtc_project_parameter` returns copied metadata, static configuration and whether
a lane exists. LIVE values are reset defaults, not callback-owned controller
readbacks. `xtc_project_set_parameter` changes TRACK/PLUGIN static configuration
without deleting automation. `xtc_project_automate` sets/replaces/removes points
for TRACK/PLUGIN through one C contract. LIVE static setters and timeline lanes
are unsupported: these controls belong to incoming MIDI events, not the arranged
instrument. Plugin metadata/control edits execute trusted discovery on the
control thread; they never run inside audio. Model-only clients must compile and
publish accepted snapshots before rendering; GUI edits do this transactionally.
Unknown owners/ports fail without touching the destination model/output. Missing
plugin modules fail discovery rather than guessing ranges or installing a dummy.
Track deletion removes its lanes; plugin deletion removes its embedded lanes.
Compatible plugin replacement resets controls/automation as described in
PLUGIN_HOSTING.md. Persisted dangling references reject the complete load;
the current project remains intact. The GUI reports rejected edits/Open failures.
## Interpolation and precedence
The Mixer Interpolation chooser selects Linear or Hold (step) for the point
being added/replaced. A point owns the segment beginning there. At a hold point,
its value remains until the next point's exact sample. Linear segments interpolate
between endpoint values without an extra smoothing stage. Hold first/last before/
after the lane. Gain/pan for tracks and buses support both; integer/toggle LADSPA
controls require hold. Existing files migrate mixer points to linear.
Automation is absolute project time, independent of clip starts, ends, trims,
splits and moves; it neither restarts nor resets at a clip boundary. Tick-to-sample
rounding uses the prepared constant tempo/rate; distinct ticks landing on the
same sample choose the greatest tick. Empty lanes restore the static value.
Static edits while a lane exists change the fallback value, not the audible lane.
GUI changes stop/rebuild snapshots; live mutation of faders/plugin instances is
not introduced. Plugin reset removes lanes only when explicitly applied.
At each sample, arranged track/bus and plugin lanes apply to their own targets.
Incoming MIDI volume/pan/sustain affect only live sine voices on that channel,
never those lanes or the static project controls. Equal-offset MIDI events run
in input order before synthesis. A controller event then affects that sample's
ramp; gain/pan reach the target over max(1, rate/200) frames. Sustain changes at
the event sample. Panic CC120/123 remains reserved and also suppresses already
started arranged notes according to the existing panic contract.
Seek/discontinuous positions clear live voices and runtime controllers to their
reset values but retain prepared bindings. Arranged lanes evaluate directly at
the new position. Backward jumps evaluate the earlier timeline again; tests
exercise explicit wraps. There is no loop transport feature or promise to
restore opaque LADSPA delay history across jumps (transport reset retains it).
## Configurable live MIDI bindings
Audio → MIDI controller mapping lists the current 16 channels. Enter
`channel volume_CC pan_CC sustain_CC`, for example `2 1 74 11`. Channels are
zero-based; CCs are 0..127, or 128 to disable a target. Enabled CCs must be distinct
within a channel; CC120 and CC123 cannot be rebound. All channels default to
7/10/64. Rebinding replaces the old binding; it is not an additional route. An
unmapped CC does nothing. Other channels remain independent. The same volume,
pan and sustain transfer functions apply regardless of the chosen CC number.
Disabling a target leaves it at its runtime reset value until plan replacement.
Changing a map stops/rebuilds playback and is one undoable edit; maps survive
save/Open and undo/redo. Cancel and invalid/duplicate/reserved bindings retain
the prior configuration. There is no automatic learn, arbitrary plugin/track CC
mapping, MIDI capture or write/touch/latch automation mode.
Plans copy only 48 binding bytes on the control thread. The callback compares
at most three CC numbers per control event, uses existing preallocated ramps/
voices and the existing 256-event block budget. Decode now accepts all valid
three-byte CC messages; even unmapped CCs count against that event budget.
Mixer points compile to bounded sorted arrays with binary lane lookup; processing
does no allocation, sort, file/module I/O, locking or GUI work. LADSPA automated
run(1) cost and native plugin trust limits remain unchanged.
## Persistence and verification
Project v10 appends a mixer interpolation field and 16 controller-map records;
v1–v9 migrate to linear mixer lanes and default 7/10/64 maps. LADSPA point encoding
is unchanged. Public `xtc_automation_point` gains `step`: rebuild clients and
initialize it to 0 (linear) or 1 (hold). See PROJECT_FORMAT.md for exact grammar.
`test_parameters` covers metadata, missing targets, static/automation precedence,
exact hold/linear boundaries, save/reopen, malformed maps, low-rate sample
collisions, odd blocks, seeks and backward jumps. `test_control` compares default
and remapped event streams, stereo pan, disabled/unmapped controls and immutable
plan maps across resets. Plugin tests cover common-target edits and persistence;
GUI tests cover interpolation, map rollback, undo/redo and save/Open. Existing
callback guards and sanitizers verify host ownership constraints. Physical
controller devices, listening and extended high-density endurance are pending.

View file

@ -11,7 +11,7 @@
events/block, 32 voices, 65536 frames/block. See API headers for exact bounds.
- WAV samples preload with the memory/format limits in AUDIO_FILES.md. Callback
overflow reports/silences; there is no CPU preemption or expensive-plugin rescue.
- Live MIDI fixed volume/pan/sustain mapping; no controller learn, capture, SMF
- Live MIDI configurable per-channel volume/pan/sustain CC mapping; no controller learn, capture, SMF
import, pitch bend, custom-instrument chooser or instrument timbre controls.
- Native ABI 1 graph nodes are session-only. ABI 2 state is parameter-only. LADSPA
supports the documented mono amp/inverter/sine set; no opaque state, native
@ -21,7 +21,7 @@
zoom/selection and graph positions are session UI state. Undo retains 32 edits
plus baseline; allocation failures are reported, not silently hidden. Media is
never removed by undo/delete. Broad screen-reader/accessibility testing is pending.
- Format v9 reads v1–v8 and uses relative local media references. External absolute
- Format v10 reads v1–v9 and uses relative local media references. External absolute
media/module paths need manual repair after relocation. Saving does not bundle
media; missing media stays referenced, missing plugins reject GUI installation.
Unknown data is rejected rather than stripped. Post-rename directory-sync errors
@ -54,3 +54,8 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
requires the same mono audio layout and resets controls/automation. Missing
modules reject Open while retaining the current project; GUI module relink for
unopened documents is deferred. In-process plugin crashes/hangs are uncontained.
- Common parameter targets cover track/bus gain/pan, LADSPA controls and metadata
for live sine volume/pan/sustain. Live mappings do not drive arranged lanes,
faders or plugins. No automation capture, touch/latch/write modes or loop
transport. Backward jumps are deterministic for lanes, not opaque plugin tails.

View file

@ -131,3 +131,8 @@ repeats/fresh IDs, rejected batch rollback, capacity, undo snapshots, equal-offs
live order, adjacent/overlapping notes, exact block partitions and seek chase.
`make test-gui` covers mouse edits and the new commands with undo/redo and engine
reinstallation. Listening and physical MIDI-device validation remain pending.
Phase 26: CC7/10/64 are defaults, configurable per channel through Audio → MIDI
controller mapping. Disabled/unmapped CCs have no effect; CC120/123 stay panic.
Decode accepts valid CC0..127 messages within the existing event budget. See
CONTROL.md for the exact bindings, ramp, sustain and automation precedence rules.

View file

@ -26,7 +26,8 @@ static control. There are at most 1024 points globally, with one per
track/parameter/tick. Track deletion cascades to its automation.
A nonempty lane replaces that parameter's static value. Between points the C
engine interpolates linearly at each sample, holding the first/last value outside
engine interpolates linearly or holds each segment (Interpolation chooser),
holding the first/last value outside
their range. Preparation converts ticks to integer sample endpoints, sorts and
indexes lanes. When several ticks quantize to one sample at very low sample
rates, the last point wins at that sample. Rendering searches prepared bounded
@ -137,3 +138,8 @@ project performance remain unverified; no new dependency was introduced.
`test_buses` covers bus routing, exact taps, audio/MIDI cancellation, controls,
automation, persistence, invalid routes, deletion/undo and dBFS ballistics. Backend
tests cover consuming interval peaks; GUI tests cover routing undo and clipping.
Phase 26 adds Linear/Hold interpolation for each point added or replaced. The
point's mode applies to its following segment. Static controls remain fallbacks
while a lane exists; moving/splitting clips does not move absolute track/bus lanes.
See CONTROL.md for common parameter IDs and controller/automation precedence.

View file

@ -193,3 +193,14 @@ replacements with host allocation/free/dlopen/dlclose guards. Native ABI tests
cover process-error silence/reset. The separately installed amp/inverter/sine
modules remain the qualified LADSPA set. Human listening, arbitrary third-party
editors/plugins and extended realtime endurance remain unverified.
## Phase 26 parameter targets
`xtc_control.h` supplies common stable owner/parameter IDs and control-thread
metadata/edit APIs for track/bus gain/pan and LADSPA input ports. The GUI's plugin
automation command uses this contract. Linear/hold LADSPA encoding and its
integer/toggle restrictions remain unchanged; v10 adds mixer interpolation and
live-sine CC maps without changing plugin state records. Direct static edits do
not override existing lanes. Live CCs do not map to plugins; missing targets or
modules fail without installing partial state. See CONTROL.md for timing,
precedence, reset and backward-jump behavior.

View file

@ -1,6 +1,6 @@
# Project format v9 (reads v1–v8)
# Project format v10 (reads v1–v9)
Phases 5–15 use a bounded text format implemented in C99 without a parser library.
Phases 5–26 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
files; MIDI notes are stored inline.
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
Counts determine the number of subsequent records:
```text
XTC_PROJECT 7
XTC_PROJECT 10
APP 0 1 0
TEMPO 120000
IDS 1 1
@ -23,12 +23,31 @@ EDGE 0 1
TRACKS 1
TRACK 0 547261636b
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 3840 2880 960 3840 526567696f6e
ASSET -
MIDI 0
AUDIO 3f800000 0 0
NOTES 0 0
AUTOMATION 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END
```
@ -47,6 +66,8 @@ END
- `TRACK`: ID and hex-encoded name bytes. In v4/v5 each is immediately followed
by `MIX gain_bits pan_bits mute solo`: finite binary32 gain 0..4, pan -1..1
encoded as eight hex digits, mute and solo unsigned 0/1.
- `ROUTE`: since v9, required after MIX: bus flag and destination bus ID;
UINT64_MAX denotes master. See Phase 24 constraints below.
- `CLIP`: ID, track ID, start tick, length tick, source offset tick, source
length tick, hex-encoded name bytes. Trimming
preserves source bounds, so a region can re-extend only within its original
@ -63,7 +84,8 @@ END
within source bounds. Pitch is 0..127, velocity 1..127, channel 0..15.
Note IDs are unique, monotonic uint64 IDs with UINT64_MAX reserved.
- `AUTOMATION`: global point count, after the last NOTE and before END in v4/v5.
Follow with `POINT track_id parameter tick value_bits` for each point.
Follow with `POINT track_id parameter tick value_bits step` in v10 (v4–v9
omit step and migrate to linear). Step is 0 linear or 1 hold for the next segment.
Parameter 0 is gain (0..4), 1 pan (-1..1); values use finite binary32 hex.
Track must exist; tick <= MAX_TICK; duplicate track/parameter/tick is invalid.
Record order is preserved; rendering sorts a separate prepared copy.
@ -227,3 +249,23 @@ Bus controls and automation use existing MIX and POINT records. Older readers
reject v9 rather than discard routing. The public `xtc_track_info` grew fields;
API consumers must rebuild. Fixtures cover v1–v8 migration and tests exercise
v9 exact roundtrips and malformed routing.
## Phase 26: version 10 interpolation and controller bindings
The current writer emits v10 and reads v1–v9. Track/bus automation points are
`POINT track_id parameter tick value_bits step`, where step is 0 linear or 1
hold for the segment starting there. v1–v9 points migrate to step=0. LADSPA
`PARAM` encoding remains unchanged (already has interpolation mode).
Immediately after all POINT records, before END, v10 requires `CONTROL_MAPS 16`
and exactly 16 records in ascending channel order:
`MAP channel volume_CC pan_CC sustain_CC`. Channels are 0..15. Controllers are
0..127 or 128 (disabled); enabled numbers must be distinct within a row and may
not be 120 or 123. Duplicate/missing/out-of-order channels, invalid CCs, reserved
bindings and invalid interpolation modes reject the complete candidate. v1–v9
supply default 7/10/64 bindings on every channel. No event history/current live
controller values are serialized. Older readers reject v10.
Fixtures cover v1–v9 migration. Exact roundtrips, malformed metadata, step/linear
renders and remapped live events cover the new writer/reader. Public clients
must rebuild and initialize the appended `xtc_automation_point.step` field.

View file

@ -672,10 +672,36 @@ Enable repeatable changes to mix and instrument parameters over time, with clear
- Document precedence rules for automation, controller input, and direct user changes.
### Acceptance criteria
- Supported parameters can be automated and saved in a project.
- Playback produces deterministic automation values for a given project and transport position.
- Missing targets fail safely and are reported to the user.
- Automation processing meets the engine's real-time safety requirements.
- [x] Supported parameters can be automated and saved in a project.
- [x] Playback produces deterministic automation values for a given project and transport position.
- [x] Missing targets fail safely and are reported to the user.
- [x] Automation processing meets the engine's real-time safety requirements.
Implemented: common stable owner/parameter keys and copied metadata for
track/bus gain/pan, LADSPA input controls and live sine controller targets;
validated common static/lane edit APIs; per-point linear/hold mixer automation;
per-channel saved volume/pan/sustain CC bindings with explicit disable and reserved
panic controllers. GUI adds Interpolation and Audio/MIDI controller mapping;
edits participate in undo/redo. Project v10 reads v1–v9 with linear/default-map
migration. CONTROL.md documents target identity, clip boundaries, precedence,
missing-target handling, controller ramps and real-time ownership.
Validation: warning-enabled core/GUI builds and full tests; common metadata,
missing owner/port/module failures and retained state; exact step/ramp endpoints,
low-rate collisions, odd blocks, seeks and explicit backward wraps; default/
remapped/disabled/unmapped CCs and stereo pan, immutable maps after preparation;
GUI map/interpolation/save/Open/undo/redo and invalid-edit rollback; v1–v9
migration, v10 exact persistence and malformed records; fixture/installed plugin
tests, callback-allocation guards, reliability/static analysis, performance and
capacity tests; software JACK GUI/audio/MIDI and workflow; dependency-free build,
installation/package relocation, core/GUI ASan+UBSan (existing fontconfig leak
suppression only for GUI/system font allocations).
No loop transport, MIDI learn/capture, write/touch/latch modes, arbitrary MIDI
mapping to arranged faders/plugins, timbre parameter for the built-in sine or
opaque-plugin seek restoration is claimed. Live MIDI targets have reset-default
metadata and controller bindings, not timeline lanes. Physical controllers,
listening and extended third-party-plugin endurance remain unverified.
## Phase 27 — Editing Workflow, Undo/Redo, and Usability

View file

@ -138,3 +138,10 @@ reset and insertion of an effect on a selected wire. Graph wires determine
processing order; Delete removes a selected plugin and its wires. Changes are
undoable. Replacement keeps compatible audio wiring but resets plugin controls
and automation. See PLUGIN_HOSTING.md for supported formats and failure behavior.
Phase 26: Mixer Interpolation selects Linear or Hold for each point added or
replaced. Audio → MIDI controller mapping lists all channel bindings; enter
`channel volume_CC pan_CC sustain_CC` (channels 0..15, 128 disables, enabled CCs
must be distinct, 120/123 reserved). Maps are undoable and saved. Controllers
modify live sine voices; arranged track/bus/plugin automation takes precedence
over its static controls and is unaffected by those MIDI bindings. See CONTROL.md.

40
include/xtc/xtc_control.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef XTC_CONTROL_H
#define XTC_CONTROL_H
#include "xtc_mixer.h"
#include "xtc_plugin.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Stable owner ID + parameter ID, never a dense list index or pointer.
* TRACK includes mix buses: parameters 0 gain / 1 pan.
* PLUGIN owner is a graph node; parameter is its actual LADSPA port.
* LIVE owner is MIDI channel 0..15; parameters 0 volume / 1 pan / 2 sustain.
* Live metadata describes reset defaults, not callback-owned current values. */
typedef enum { XTC_PARAMETER_TRACK = 1, XTC_PARAMETER_PLUGIN, XTC_PARAMETER_LIVE } xtc_parameter_kind;
typedef struct { xtc_parameter_kind kind; uint64_t owner; unsigned int parameter; } xtc_parameter_key;
typedef struct {
xtc_parameter_info info;
unsigned int flags; /* XTC_PLUGIN_* hint flags; units empty if unspecified. */
float value; /* Static configuration; automation may override it. */
int automated;
} xtc_parameter_state;
/* Control thread only. Plugin metadata executes trusted module discovery.
* Failure leaves output/model unchanged. Setter changes static configuration,
* never erases a lane. Compile/publish snapshots before callback use. */
xtc_result xtc_project_parameter(const xtc_project *project, xtc_parameter_key key, double rate, xtc_parameter_state *out);
xtc_result xtc_project_set_parameter(xtc_project *project, xtc_parameter_key key, double rate, float value);
/* Absolute ticks; step 0 linear / 1 hold for segment starting at this point.
* Integer/toggle plugin controls require hold. Live automation is unsupported.
* Remove ignores value/step and fails NOT_FOUND when no point exists. */
xtc_result xtc_project_automate(xtc_project *project, xtc_parameter_key key, double rate,
uint64_t tick, unsigned int step, float value, int remove);
#define XTC_CC_DISABLED 128U
/* Per-channel bindings in order volume, pan, sustain. Defaults 7,10,64.
* Enabled controllers must be distinct and <=127; 120/123 are reserved panic.
* No learn/capture. Copied into MIDI plans; never read from model in callback. */
xtc_result xtc_project_get_control_map(const xtc_project *project, unsigned int channel, unsigned int out[3]);
xtc_result xtc_project_set_control_map(xtc_project *project, unsigned int channel, const unsigned int controllers[3]);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -124,11 +124,12 @@ xtc_result xtc_project_edit_note(xtc_project *project, uint64_t id, uint64_t sta
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_remove_note(xtc_project *project, uint64_t id);
/* Complete 3-byte note on/off and CC7/10/64/120/123. Velocity-zero on -> off.
/* Complete 3-byte note on/off and CC0..127. Velocity-zero on -> off.
* CC120/123 decode to ALL_OFF; other CCs are interpreted by prepared bindings.
* Unsupported messages return UNSUPPORTED; malformed supported messages INVALID.
* Output unchanged on error. No running-status/SysEx/program/bend support. */
/**
* @brief Decodes a complete note or all-notes-off MIDI message into a block event.
* @brief Decodes a complete note, controller or all-notes-off MIDI message into a block event.
*
* @param bytes Borrowed MIDI message bytes.
* @param size Number of message bytes.

View file

@ -19,6 +19,7 @@ typedef struct {
uint64_t track_id, tick;
xtc_automation_parameter parameter;
float value;
unsigned int step; /* 0 linear, 1 hold; mode starts at this point. */
} xtc_automation_point;
/* Buses share the 64 track/automation capacities. A bus cannot contain clips
@ -35,7 +36,7 @@ xtc_result xtc_peak_meter_update(xtc_peak_meter *meter, float linear_peak, doubl
/* Gain 0..4, pan -1..1, mute/solo 0 or 1. Track audio and scheduled MIDI only.
* Linear pan: center unity on both outputs; hard pan attenuates opposite side.
* Automation replaces the corresponding static value, linear between points,
* Automation replaces the corresponding static value, linear/hold per point,
* holds first/last outside the lane. Duplicate track/parameter/tick replaces. */
/**
* @brief Sets a track's gain, pan, mute, and solo controls after validation.

View file

@ -7,7 +7,7 @@
extern "C" {
#endif
#define XTC_PROJECT_FORMAT_VERSION 9U
#define XTC_PROJECT_FORMAT_VERSION 10U
#define XTC_PROJECT_MAX_TRACKS 64U
#define XTC_PROJECT_MAX_CLIPS 1024U
#define XTC_PROJECT_NAME_BYTES 64U

130
src/control/model.c Normal file
View file

@ -0,0 +1,130 @@
#include "xtc/xtc_control.h"
#include "../project/project_internal.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
xtc_result xtc_project_get_control_map(const xtc_project *p, unsigned int channel, unsigned int out[3])
{
size_t i;
if (!p || !out || channel > 15) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < 3; ++i) out[i] = p->control_map[channel][i];
return XTC_OK;
}
xtc_result xtc_project_set_control_map(xtc_project *p, unsigned int channel, const unsigned int cc[3])
{
size_t i, j;
if (!p || !cc || channel > 15) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < 3; ++i) {
if (cc[i] > XTC_CC_DISABLED || cc[i] == 120 || cc[i] == 123) return XTC_ERR_INVALID_ARGUMENT;
for (j = 0; j < i; ++j) if (cc[i] != XTC_CC_DISABLED && cc[i] == cc[j]) return XTC_ERR_INVALID_ARGUMENT;
}
for (i = 0; i < 3; ++i) p->control_map[channel][i] = (unsigned char)cc[i];
return XTC_OK;
}
xtc_result xtc_project_parameter(const xtc_project *p, xtc_parameter_key key, double rate, xtc_parameter_state *out)
{
xtc_parameter_state state = {0};
size_t i;
if (!p || !out || !isfinite(rate) || rate < 1 || rate > 768000) return XTC_ERR_INVALID_ARGUMENT;
state.info.id = key.parameter;
if (key.kind == XTC_PARAMETER_TRACK) {
xtc_track_info track;
xtc_result r = XTC_ERR_NOT_FOUND;
if (key.parameter > 1) return XTC_ERR_NOT_FOUND;
for (i = 0; i < p->track_count; ++i) if (p->tracks[i].id == key.owner) { track = p->tracks[i]; r = XTC_OK; break; }
if (r != XTC_OK) return r;
snprintf(state.info.name,sizeof(state.info.name),"%s",key.parameter ? "Pan" : "Gain");
snprintf(state.info.unit,sizeof(state.info.unit),"%s",key.parameter ? "balance" : "linear");
state.info.minimum = key.parameter ? -1 : 0;
state.info.maximum = key.parameter ? 1 : 4;
state.info.default_value = key.parameter ? 0 : 1;
state.value = key.parameter ? track.pan : track.gain;
state.flags = XTC_PLUGIN_BOUNDED_BELOW | XTC_PLUGIN_BOUNDED_ABOVE;
for (i = 0; i < p->automation_count; ++i)
if (p->automation[i].track_id == key.owner && (unsigned int)p->automation[i].parameter == key.parameter) state.automated = 1;
} else if (key.kind == XTC_PARAMETER_PLUGIN) {
xtc_plugin_spec spec;
xtc_plugin_details details;
xtc_result r;
if (key.owner > SIZE_MAX) return XTC_ERR_NOT_FOUND;
r = xtc_graph_get_plugin(p->graph,(size_t)key.owner,&spec);
if (r != XTC_OK) return r;
r = xtc_plugin_describe(&spec,rate,&details);
if (r != XTC_OK) return r;
for (i = 0; i < spec.control_count && spec.ports[i] != key.parameter; ++i) {}
if (i == spec.control_count) return XTC_ERR_NOT_FOUND;
state.info = details.parameters[i]; state.flags = details.flags[i]; state.value = spec.values[i];
for (i = 0; i < spec.point_count; ++i) if (spec.points[i].port == key.parameter) state.automated = 1;
} else if (key.kind == XTC_PARAMETER_LIVE) {
if (key.owner > 15 || key.parameter > 2) return XTC_ERR_NOT_FOUND;
snprintf(state.info.name,sizeof(state.info.name),"%s",key.parameter == 0 ? "Live volume" : key.parameter == 1 ? "Live pan" : "Live sustain");
snprintf(state.info.unit,sizeof(state.info.unit),"%s",key.parameter == 0 ? "linear" : key.parameter == 1 ? "balance" : "boolean");
state.info.minimum = key.parameter == 1 ? -1 : 0;
state.info.maximum = 1;
state.info.default_value = key.parameter == 0 ? 1 : 0;
state.value = state.info.default_value;
state.flags = XTC_PLUGIN_BOUNDED_BELOW | XTC_PLUGIN_BOUNDED_ABOVE | (key.parameter == 2 ? XTC_PLUGIN_TOGGLED : 0U);
} else return XTC_ERR_INVALID_ARGUMENT;
/* Keep all output untouched on failure. */
*out = state;
return XTC_OK;
}
static int valid_value(const xtc_parameter_state *state, float value)
{
return isfinite(value) && value >= state->info.minimum && value <= state->info.maximum &&
(!(state->flags & XTC_PLUGIN_INTEGER) || value == floorf(value)) &&
(!(state->flags & XTC_PLUGIN_TOGGLED) || value == 0 || value == 1);
}
xtc_result xtc_project_set_parameter(xtc_project *p, xtc_parameter_key key, double rate, float value)
{
xtc_parameter_state state;
xtc_result r = xtc_project_parameter(p,key,rate,&state);
size_t i;
if (r != XTC_OK) return r;
if (!valid_value(&state,value)) return XTC_ERR_INVALID_ARGUMENT;
if (key.kind == XTC_PARAMETER_TRACK) {
for (i = 0; i < p->track_count; ++i) if (p->tracks[i].id == key.owner) {
if (key.parameter) p->tracks[i].pan = value; else p->tracks[i].gain = value;
return XTC_OK;
}
} else if (key.kind == XTC_PARAMETER_PLUGIN) {
xtc_plugin_spec spec;
r = xtc_graph_get_plugin(p->graph,(size_t)key.owner,&spec);
if (r != XTC_OK) return r;
for (i = 0; i < spec.control_count; ++i) if (spec.ports[i] == key.parameter) spec.values[i] = value;
return xtc_graph_set_plugin(p->graph,(size_t)key.owner,&spec);
}
return XTC_ERR_UNSUPPORTED;
}
xtc_result xtc_project_automate(xtc_project *p, xtc_parameter_key key, double rate,
uint64_t tick, unsigned int step, float value, int remove)
{
xtc_parameter_state state;
xtc_result r = xtc_project_parameter(p,key,rate,&state);
if (r != XTC_OK) return r;
if ((remove != 0 && remove != 1) || tick > XTC_PROJECT_MAX_TICK) return XTC_ERR_INVALID_ARGUMENT;
if (key.kind == XTC_PARAMETER_LIVE) return XTC_ERR_UNSUPPORTED;
if (!remove && (step > 1 || !valid_value(&state,value) ||
(!step && (state.flags & (XTC_PLUGIN_INTEGER|XTC_PLUGIN_TOGGLED))))) return XTC_ERR_INVALID_ARGUMENT;
if (key.kind == XTC_PARAMETER_TRACK) {
xtc_automation_point point = {key.owner,tick,(xtc_automation_parameter)key.parameter,value,step};
return remove ? xtc_project_remove_automation(p,key.owner,point.parameter,tick) : xtc_project_set_automation(p,&point);
} else {
xtc_plugin_spec spec;
unsigned int i;
r = xtc_graph_get_plugin(p->graph,(size_t)key.owner,&spec);
if (r != XTC_OK) return r;
for (i = 0; i < spec.point_count; ++i) if (spec.points[i].port == key.parameter && spec.points[i].tick == tick) break;
if (remove) {
if (i == spec.point_count) return XTC_ERR_NOT_FOUND;
memmove(spec.points+i,spec.points+i+1,(spec.point_count-i-1)*sizeof(*spec.points));
--spec.point_count;
} else {
if (i == XTC_PLUGIN_POINTS) return XTC_ERR_CAPACITY;
spec.points[i] = (xtc_plugin_point){tick,key.parameter,step,value};
if (i == spec.point_count) ++spec.point_count;
}
return xtc_graph_set_plugin(p->graph,(size_t)key.owner,&spec);
}
}

View file

@ -73,6 +73,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("File/Save As", FL_CTRL + FL_SHIFT + 's', file_callback, this);
menu->add("File/Import WAV", FL_CTRL + 'i', audio_file_callback, this);
menu->add("File/Relink selected audio", 0, audio_file_callback, this);
menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this);
menu->add("Audio/Panic", FL_F + 9, panic_callback, 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);
@ -1366,6 +1367,36 @@ void MainWindow::history_callback(Fl_Widget *widget, void *context)
window->report_error("Undo/redo failed", r);
}
xtc_result MainWindow::set_control_map(unsigned int channel, const unsigned int controllers[3])
{
if (recorder_) return XTC_ERR_INVALID_STATE;
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_set_control_map(candidate,channel,controllers);
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty();
return r;
}
void MainWindow::control_map_callback(Fl_Widget *, void *context)
{
auto *w = static_cast<MainWindow *>(context);
if (w->recorder_) { w->report_error("Stop recording before controller mapping",XTC_ERR_INVALID_STATE); return; }
unsigned int controllers[3]{};
std::string prompt = "Live sine MIDI bindings (channels 0..15; 128 disables). CC120/123 reserved.\n";
for (unsigned int channel = 0; channel < 16; ++channel) {
xtc_project_get_control_map(w->project_.get(),channel,controllers);
prompt += std::to_string(channel) + ": volume " + std::to_string(controllers[0]) +
", pan " + std::to_string(controllers[1]) + ", sustain " + std::to_string(controllers[2]) + "\n";
}
prompt += "Enter channel volume_CC pan_CC sustain_CC (distinct enabled CCs):";
const char *answer = fl_input("%s","0 7 10 64",prompt.c_str());
if (!answer) return;
unsigned int channel; char extra;
xtc_result r = std::sscanf(answer,"%u %u %u %u %c",&channel,controllers,controllers+1,controllers+2,&extra) == 4 ?
w->set_control_map(channel,controllers) : XTC_ERR_INVALID_ARGUMENT;
if (r != XTC_OK) w->report_error("Controller mapping rejected: check channel, unique CCs and reserved panic controllers",r);
}
xtc_result MainWindow::install_plugin_graph(xtc_graph *graph)
{
xtc_project *candidate = nullptr;
@ -1454,21 +1485,16 @@ xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float va
xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000; xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
xtc_parameter_key key{XTC_PARAMETER_PLUGIN,static_cast<uint64_t>(id),point.port};
if (r == XTC_OK) r = xtc_project_automate(candidate,key,rate,point.tick,point.step,point.value,remove ? 1 : 0);
xtc_plugin_spec spec{};
xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
if (r != XTC_OK) return r;
unsigned i;
for (i = 0; i < spec.point_count; ++i) if (spec.points[i].port == point.port && spec.points[i].tick == point.tick) break;
if (remove) {
if (i == spec.point_count) return XTC_ERR_NOT_FOUND;
for (unsigned j = i+1; j < spec.point_count; ++j) spec.points[j-1] = spec.points[j];
--spec.point_count;
} else {
if (i == XTC_PLUGIN_POINTS) return XTC_ERR_CAPACITY;
spec.points[i] = point;
if (i == spec.point_count) ++spec.point_count;
}
return apply_plugin_spec(id,spec);
if (r == XTC_OK) r = xtc_graph_get_plugin(xtc_project_graph(candidate),id,&spec);
xtc_project_destroy(candidate);
return r == XTC_OK ? apply_plugin_spec(id,spec) : r;
}
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace)
{

View file

@ -8,6 +8,7 @@
#include "xtc/xtc_backend.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_control.h"
class GraphWidget;
class MixerWidget;
@ -20,6 +21,7 @@ class Fl_Tabs;
class MainWindow : public Fl_Double_Window {
public:
/* Control-thread plugin commands, transactional model adoption. */
xtc_result set_control_map(unsigned int channel, const unsigned int controllers[3]);
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
xtc_result replace_plugin(size_t id, const char *path, unsigned int index);
@ -292,6 +294,7 @@ class MainWindow : public Fl_Double_Window {
* @param widget Triggering Fl_Menu_Bar whose selected label names the action.
* @param context MainWindow * owning the project and file path.
*/
static void control_map_callback(Fl_Widget *widget, void *context);
static void plugin_callback(Fl_Widget *widget, void *context);
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false);
xtc_result install_plugin_graph(xtc_graph *graph);

View file

@ -1,4 +1,5 @@
#include "mixer_widget.hpp"
#include "xtc/xtc_control.h"
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Check_Button.H>
@ -88,6 +89,7 @@ class AutomationLane : public Fl_Widget {
if (it == points.end())
return points.back().value;
const auto &a = *(it - 1);
if (a.step) return a.value;
return a.value +
static_cast<float>(static_cast<double>(tick - a.tick) / static_cast<double>(it->tick - a.tick)) *
(it->value - a.value);
@ -222,6 +224,8 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit
start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks);
start_->step(3840);
start_->callback(select, this);
mode_ = new Fl_Choice(x + 705, y + 146, 150, 26, "Interpolation");
mode_->add("Linear|Hold (step)"); mode_->value(0);
auto *hint = new Fl_Box(x + 10, y + 180, w - 20, 24,
"Gain 0..4; pan -1 left / +1 right. M/S toggles; arrows change gain, "
"Shift+arrows pan; Up/Down selects track.");
@ -413,10 +417,9 @@ xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo)
xtc_result MixerWidget::point(uint64_t tick, float value, bool remove)
{
xtc_result r = before_ ? before_(context_) : XTC_OK;
xtc_automation_point a{selected_track(), tick, parameter(), value};
xtc_parameter_key key{XTC_PARAMETER_TRACK,selected_track(),static_cast<unsigned int>(parameter())};
if (r == XTC_OK)
r = remove ? xtc_project_remove_automation(project_, a.track_id, a.parameter, tick)
: xtc_project_set_automation(project_, &a);
r = xtc_project_automate(project_,key,48000,tick,mode_->value() == 1 ? 1U : 0U,value,remove ? 1 : 0);
return changed(r);
}

View file

@ -143,7 +143,7 @@ class MixerWidget : public Fl_Group {
xtc_project *project_;
BeforeEdit before_;
void *context_;
Fl_Choice *tracks_, *parameter_, *routes_;
Fl_Choice *tracks_, *parameter_, *routes_, *mode_;
std::vector<uint64_t> route_ids_;
Fl_Value_Slider *gain_, *pan_;
Fl_Check_Button *mute_, *solo_;

View file

@ -30,6 +30,7 @@ struct xtc_midi_plan {
uint64_t monitor_position;
xtc_gain_ramp volume[16], pan[16];
unsigned int sustain[16];
unsigned char control_map[16][3];
xtc_mix_snapshot mix;
span notes[XTC_PROJECT_MAX_NOTES];
size_t count;
@ -39,7 +40,7 @@ struct xtc_midi_plan {
};
/**
* @brief Decodes a complete note or all-notes-off MIDI message into a block event.
* @brief Decodes a complete note, controller or all-notes-off MIDI message into a block event.
*
* @param bytes Borrowed MIDI message bytes.
* @param size Number of message bytes.
@ -58,8 +59,6 @@ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int
return XTC_ERR_UNSUPPORTED;
if (size != 3 || bytes[1] > 127 || bytes[2] > 127)
return XTC_ERR_INVALID_ARGUMENT;
if (command == 176 && bytes[1] != 120 && bytes[1] != 123 && bytes[1] != 7 && bytes[1] != 10 && bytes[1] != 64)
return XTC_ERR_UNSUPPORTED;
e.offset = offset;
e.channel = bytes[0] & 15U;
e.pitch = bytes[1];
@ -190,6 +189,7 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
}
}
free(points);
memcpy(p->control_map,project->control_map,sizeof(p->control_map));
p->controller_frames = rate / 200U;
if (!p->controller_frames) p->controller_frames = 1;
xtc_midi_plan_reset(p);
@ -255,10 +255,10 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
return;
}
if (e->type == XTC_MIDI_CONTROL) {
if (e->pitch == 7) (void)xtc_gain_ramp_target(p->volume+e->channel,(float)e->velocity/127.0f,p->controller_frames);
else if (e->pitch == 10) (void)xtc_gain_ramp_target(p->pan+e->channel,
if (e->pitch == p->control_map[e->channel][0]) (void)xtc_gain_ramp_target(p->volume+e->channel,(float)e->velocity/127.0f,p->controller_frames);
else if (e->pitch == p->control_map[e->channel][1]) (void)xtc_gain_ramp_target(p->pan+e->channel,
e->velocity >= 64 ? (float)(e->velocity-64)/63.0f : ((float)e->velocity-64)/64.0f,p->controller_frames);
else if (e->pitch == 64) {
else if (e->pitch == p->control_map[e->channel][2]) {
p->sustain[e->channel] = e->velocity >= 64;
if (!p->sustain[e->channel]) for (i = 0; i < XTC_MIDI_VOICES; ++i)
if (p->voices[i].channel == e->channel && p->voices[i].released) p->voices[i].active = 0;

View file

@ -7,7 +7,7 @@
typedef struct {
uint64_t sample, tick;
size_t track;
unsigned int parameter;
unsigned int parameter, step;
float value;
} xtc_mix_point;

View file

@ -55,7 +55,7 @@ xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pa
xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point *a)
{
size_t i;
if (!p || !a || !isfinite(a->value) || a->tick > XTC_PROJECT_MAX_TICK ||
if (!p || !a || a->step > 1 || !isfinite(a->value) || a->tick > XTC_PROJECT_MAX_TICK ||
(a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) ||
a->value < (a->parameter == XTC_AUTOMATION_GAIN ? 0 : -1) ||
a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1))

View file

@ -63,6 +63,7 @@ xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snap
return XTC_ERR_INVALID_STATE;
b->tick = a->tick;
b->value = a->value;
b->step = a->step;
r = xtc_project_tick_to_sample(p, a->tick, rate, &b->sample);
if (r != XTC_OK)
return r;
@ -102,8 +103,8 @@ static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param,
}
if (!low)
return p[0].value;
if (low == count)
return p[count - 1].value;
if (low == count || p[low-1].step)
return p[low - 1].value;
return (float)(p[low - 1].value + ((double)sample - (double)p[low - 1].sample) /
(double)(p[low].sample - p[low - 1].sample) *
(p[low].value - p[low - 1].value));

View file

@ -403,7 +403,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
project->automation_count = (size_t)value;
for (i = 0; i < project->automation_count; ++i) {
xtc_automation_point *a = project->automation + i;
READ("POINT", 5U);
READ("POINT", format_version >= 10U ? 6U : 5U);
NUM(1U, a->track_id);
NUM(2U, value);
if (value > 1)
@ -412,6 +412,22 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
NUM(3U, a->tick);
if (!decode_float(input->tokens[4], &a->value))
return XTC_ERR_FORMAT;
if (format_version >= 10U) {
NUM(5U,value); if (value > 1) return XTC_ERR_FORMAT;
a->step = (unsigned int)value;
}
}
}
if (format_version >= 10U) {
READ("CONTROL_MAPS",2U); NUM(1U,value);
if (value != 16) return XTC_ERR_FORMAT;
for (i = 0; i < 16; ++i) {
READ("MAP",5U); NUM(1U,value);
if (value != i) return XTC_ERR_FORMAT;
for (j = 0; j < 3; ++j) {
NUM(j+2,value); if (value > 128) return XTC_ERR_FORMAT;
project->control_map[i][j] = (unsigned char)value;
}
}
}
READ("END", 1U);
@ -549,10 +565,14 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch
uint32_t bits;
const xtc_automation_point *a = project->automation + i;
memcpy(&bits, &a->value, sizeof(bits));
if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 "\n", a->track_id, (unsigned int)a->parameter,
a->tick, bits) < 0)
if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 " %u\n", a->track_id, (unsigned int)a->parameter,
a->tick, bits, a->step) < 0)
return XTC_ERR_IO;
}
if (fputs("CONTROL_MAPS 16\n",file) == EOF) return XTC_ERR_IO;
for (i = 0; i < 16; ++i)
if (fprintf(file,"MAP %zu %u %u %u\n",i,(unsigned int)project->control_map[i][0],
(unsigned int)project->control_map[i][1],(unsigned int)project->control_map[i][2]) < 0) return XTC_ERR_IO;
return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
}

View file

@ -119,6 +119,13 @@ xtc_result xtc_project_create(xtc_project **out_project)
}
project->graph->require_output_nodes = 1;
project->tempo = 120000U;
{ size_t channel;
for (channel = 0; channel < 16; ++channel) {
project->control_map[channel][0] = 7;
project->control_map[channel][1] = 10;
project->control_map[channel][2] = 64;
}
}
*out_project = project;
return XTC_OK;
}
@ -516,6 +523,13 @@ xtc_result xtc_project_validate(const xtc_project *project)
if (project->tempo < 20000U || project->tempo > 300000U || project->track_count > XTC_PROJECT_MAX_TRACKS ||
project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES)
return XTC_ERR_FORMAT;
for (i = 0; i < 16; ++i) for (j = 0; j < 3; ++j) {
unsigned int cc = project->control_map[i][j];
size_t k;
if (cc > 128 || cc == 120 || cc == 123) return XTC_ERR_FORMAT;
for (k = 0; k < j; ++k)
if (cc != 128 && cc == project->control_map[i][k]) return XTC_ERR_FORMAT;
}
for (i = 0U; i < project->track_count; ++i) {
if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name))
return XTC_ERR_FORMAT;
@ -577,7 +591,7 @@ xtc_result xtc_project_validate(const xtc_project *project)
}
for (i = 0; i < project->automation_count; ++i) {
const xtc_automation_point *a = project->automation + i;
if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || !isfinite(a->value) ||
if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || a->step > 1 || !isfinite(a->value) ||
(a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) ||
a->value < (a->parameter == XTC_AUTOMATION_GAIN ? 0 : -1) ||
a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1))

View file

@ -9,6 +9,7 @@ struct xtc_project {
xtc_graph *graph;
uint64_t next_track_id, next_clip_id;
unsigned int tempo;
unsigned char control_map[16][3];
size_t track_count, clip_count;
uint64_t next_note_id;
size_t note_count;

View file

@ -3,7 +3,7 @@
Store small, redistributable fixtures here. Document their origin and license.
Do not commit large recordings or assets with unclear licensing.
`project-v1.xtc` through `project-v8.xtc` are hand-authored xtc metadata fixtures
`project-v1.xtc` through `project-v9.xtc` are hand-authored xtc metadata fixtures
created for this repository, under the project license. They contain no audio
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
missing module to exercise metadata migration without loading code.

22
tests/fixtures/project-v9.xtc vendored Normal file
View file

@ -0,0 +1,22 @@
XTC_PROJECT 9
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3f000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000
END

View file

@ -5,6 +5,7 @@
#include "../../src/gui/mixer_widget.hpp"
#include "../../src/gui/plugin_dialog.hpp"
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Choice.H>
#include <unistd.h>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
@ -630,6 +631,31 @@ int main(int argc, char **argv)
mixer->reset_meters(); CHECK(!mixer->master_clipped());
window.do_callback();
}
{
reset(); MainWindow window(false);
auto *sequence = timeline(&window);
unsigned int bindings[3] = {1,74,11}, actual[3];
CHECK(window.set_control_map(2,bindings) == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 1);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 7);
CHECK(window.undo(true) == XTC_OK);
const xtc_project *before = sequence->project();
bindings[1] = 1;
CHECK(window.set_control_map(2,bindings) == XTC_ERR_INVALID_ARGUMENT && sequence->project() == before);
auto *mixer_ui = window.mixer();
CHECK(sequence->add_track() == XTC_OK);
auto *mode = dynamic_cast<Fl_Choice *>(find(&window,"Interpolation")); CHECK(mode);
if (mode) mode->value(1);
CHECK(mixer_ui->point(0,.25f) == XTC_OK);
xtc_automation_point point{};
CHECK(xtc_project_get_automation(sequence->project(),0,&point) == XTC_OK && point.step == 1);
CHECK(window.save_project("/tmp/xtc-gui-controls-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-gui-controls-test.xtc") == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 1 && actual[1] == 74);
CHECK(xtc_project_get_automation(sequence->project(),0,&point) == XTC_OK && point.step == 1);
unlink("/tmp/xtc-gui-controls-test.xtc"); window.do_callback();
}
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);

View file

@ -37,7 +37,7 @@ def main():
assert os.access(root / 'bin/xtc-check', os.X_OK)
run([str(root / 'bin/xtc-check')])
diagnose = subprocess.check_output([str(root / 'bin/xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..9' in diagnose and 'JACK compiled:' in diagnose
assert 'Project formats: 1..10' in diagnose and 'JACK compiled:' in diagnose
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
(root / 'include/xtc/xtc.h').write_text('old installation marker\n')
run(make + ['install'] + variables, env=env)

View file

@ -56,7 +56,7 @@ int main(int argc, char **argv)
for (j = 0; j < 2; ++j)
REQUIRE(xtc_project_add_note(project, clip, 0, 384000, 48 + (unsigned int)i, 100, 0,
&note) == XTC_OK);
point = (xtc_automation_point){track, 0, XTC_AUTOMATION_GAIN, 0.1f};
point = (xtc_automation_point){track, 0, XTC_AUTOMATION_GAIN, 0.1f,0};
REQUIRE(xtc_project_set_automation(project, &point) == XTC_OK);
point.tick = 384000;
point.value = 0.2f;

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_control.h"
#include "xtc/xtc_project.h"
#include "xtc/xtc_realtime.h"
#include <assert.h>
@ -79,6 +80,12 @@ static void graph_roundtrip(const xtc_plugin_spec *spec)
assert(xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_graph_compile(xtc_project_graph(loaded),48000,31,&again) == XTC_ERR_NOT_FOUND && !again);
{
xtc_parameter_key key = {XTC_PARAMETER_PLUGIN,plugin,copy.control_count ? copy.ports[0] : 0};
xtc_parameter_state state = {0}; state.value = .75f;
assert(xtc_project_parameter(loaded,key,48000,&state) == XTC_ERR_NOT_FOUND && state.value == .75f);
assert(xtc_project_automate(loaded,key,48000,0,1,1,0) == XTC_ERR_NOT_FOUND);
}
assert(xtc_project_save(loaded,path) == XTC_OK);
xtc_graph_destroy(g); xtc_project_destroy(project); xtc_project_destroy(loaded);
xtc_plan_destroy(p); unlink(path);
@ -167,6 +174,25 @@ int main(int argc, char **argv)
assert(xtc_plugin_process_at(other,in,split,1,25) == XTC_OK && split[0] == 2);
xtc_plugin_destroy(p); xtc_plugin_destroy(other); p = NULL;
}
{
xtc_project *project = NULL; xtc_graph *g = NULL;
xtc_parameter_key key;
xtc_parameter_state state;
xtc_plugin_spec copy;
size_t id;
assert(xtc_project_create(&project) == XTC_OK && xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,&s,&id) == XTC_OK && xtc_project_set_graph(project,g) == XTC_OK);
key = (xtc_parameter_key){XTC_PARAMETER_PLUGIN,id,s.ports[0]};
assert(xtc_project_parameter(project,key,48000,&state) == XTC_OK && state.automated && state.info.id == s.ports[0]);
assert(xtc_project_set_parameter(project,key,48000,.75f) == XTC_OK);
assert(xtc_project_automate(project,key,48000,3,1,1,0) == XTC_OK);
assert(xtc_project_automate(project,key,48000,3,99,NAN,1) == XTC_OK); // Removal ignores mode/value.
assert(xtc_project_set_parameter(project,key,48000,3) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_graph_get_plugin(xtc_project_graph(project),id,&copy) == XTC_OK && copy.values[0] == .75f && copy.point_count == 2);
key.parameter = 63;
assert(xtc_project_automate(project,key,48000,0,1,0,0) == XTC_ERR_NOT_FOUND);
xtc_graph_destroy(g); xtc_project_destroy(project);
}
s.point_count = 0;
s.values[0] = 3; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
assert(xtc_plugin_probe(argv[1],1,48000,&s) == XTC_OK && !s.inputs);

View file

@ -3,6 +3,7 @@
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_midi.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_control.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
@ -18,7 +19,7 @@ int main(void)
assert(mkdtemp(root));
assert(xtc_project_create(&p) == XTC_OK);
snprintf(file,sizeof(file),"%s/migration.xtc",root);
for (version = 1; version <= 8; ++version) {
for (version = 1; version <= 9; ++version) {
xtc_track_info t; xtc_clip_info c; size_t notes, points;
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
assert(xtc_project_load(p,old) == XTC_OK);
@ -28,6 +29,9 @@ int main(void)
assert(c.gain == 1 && c.fade_in_tick == 0 && c.fade_out_tick == 0);
assert(xtc_project_note_count(p,&notes) == XTC_OK && notes == (size_t)(version >= 3));
assert(xtc_project_automation_count(p,&points) == XTC_OK && points == (size_t)(version >= 4));
{ unsigned int map[3]; xtc_automation_point point;
assert(xtc_project_get_control_map(p,0,map) == XTC_OK && map[0] == 7 && map[1] == 10 && map[2] == 64);
if (points) assert(xtc_project_get_automation(p,0,&point) == XTC_OK && !point.step); }
assert(xtc_project_save(p,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK);
}
for (version = 5; version <= 6; ++version) {
@ -85,6 +89,6 @@ int main(void)
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
xtc_project_destroy(p); unlink(file); rmdir(moved);
puts("v1-v8 migration, current folder relocation, cwd independence and missing-media preservation passed.");
puts("v1-v9 migration, current folder relocation, cwd independence and missing-media preservation passed.");
return 0;
}

View file

@ -18,10 +18,10 @@ int main(void)
for (i = 0; i < XTC_PROJECT_MAX_NOTES; ++i) assert(xtc_project_add_note(p,clips[i%1024],i/1024,1,60,100,0,&id) == XTC_OK);
assert(xtc_project_add_note(p,clips[0],0,1,60,100,0,&id) == XTC_ERR_CAPACITY);
for (i = 0; i < XTC_MAX_AUTOMATION_POINTS; ++i) {
xtc_automation_point a = {tracks[i%64],i/64,XTC_AUTOMATION_GAIN,.1f};
xtc_automation_point a = {tracks[i%64],i/64,XTC_AUTOMATION_GAIN,.1f,0};
assert(xtc_project_set_automation(p,&a) == XTC_OK);
}
{ xtc_automation_point a = {tracks[0],1000,XTC_AUTOMATION_GAIN,1}; assert(xtc_project_set_automation(p,&a) == XTC_ERR_CAPACITY); }
{ xtc_automation_point a = {tracks[0],1000,XTC_AUTOMATION_GAIN,1,0}; assert(xtc_project_set_automation(p,&a) == XTC_ERR_CAPACITY); }
assert(xtc_graph_create(256,&graph) == XTC_OK && xtc_graph_add_instrument(graph,&previous) == XTC_OK);
for (i = 1; i < 256; ++i) {
assert((i == 255 ? xtc_graph_add_output(graph,&node) : xtc_graph_add_meter(graph,&node)) == XTC_OK);

View file

@ -62,7 +62,7 @@ int main(void)
assert(xtc_project_set_mix(p,bus,.5f,1,1,1)==XTC_OK);
render(p,left,right,meters); assert(right[0]==0 && meters[0]==.5f && meters[7*64]==0);
assert(xtc_project_set_mix(p,bus,.5f,0,0,0)==XTC_OK);
point=(xtc_automation_point){bus,0,XTC_AUTOMATION_GAIN,.25f};
point=(xtc_automation_point){bus,0,XTC_AUTOMATION_GAIN,.25f,0};
assert(xtc_project_set_automation(p,&point)==XTC_OK);
render(p,left,right,meters); assert(left[0]==.5f && right[0]==.5f); /* .25 bus + .25 direct */
assert(xtc_project_save(p,song)==XTC_OK && xtc_project_load(q,song)==XTC_OK);

View file

@ -1,4 +1,5 @@
#include "xtc/xtc_midi.h"
#include "xtc/xtc_control.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
@ -29,10 +30,55 @@ int main(void)
for (i = 0; i < 400; ++i) assert(whole[i] == split[i]);
assert(whole[6] != 0); /* Sustain retains the released note. */
for (i = 250; i < 400; ++i) assert(whole[i] == 0); /* CC7 ramp reaches zero in 240 samples. */
{
xtc_midi_plan *mapped = NULL, *disabled = NULL;
xtc_parameter_state state;
xtc_parameter_key key = {XTC_PARAMETER_LIVE,2,2};
unsigned int bindings[3] = {1,74,11};
xtc_midi_event remapped[9];
float actual[400], right[400];
unsigned char message[3] = {0xb2,1,0};
assert(xtc_midi_decode(message,3,0,&decoded) == XTC_OK && decoded.type == XTC_MIDI_CONTROL);
message[1] = 120;
assert(xtc_midi_decode(message,3,0,&decoded) == XTC_OK && decoded.type == XTC_MIDI_ALL_OFF);
assert(xtc_project_parameter(project,key,48000,&state) == XTC_OK && !state.automated &&
(state.flags & XTC_PLUGIN_TOGGLED) && state.value == 0);
assert(xtc_project_automate(project,key,48000,0,1,1,0) == XTC_ERR_UNSUPPORTED);
assert(xtc_project_set_control_map(project,2,bindings) == XTC_OK);
assert(xtc_midi_plan_create(project,48000,&mapped) == XTC_OK);
for (i = 0; i < 8; ++i) {
remapped[i+(i ? 1 : 0)] = events[i];
if (events[i].type == XTC_MIDI_CONTROL)
remapped[i+1].pitch = events[i].pitch == 7 ? 1 : events[i].pitch == 10 ? 74 : 11;
}
// Former CC7 binding is ignored, not an additional volume binding.
remapped[1] = (xtc_midi_event){1,XTC_MIDI_CONTROL,2,7,0};
assert(xtc_midi_plan_render(mapped,0,remapped,9,actual,400) == XTC_OK);
for (i = 0; i < 400; ++i) assert(actual[i] == whole[i]);
{
const xtc_midi_event pan[] = {{0,XTC_MIDI_NOTE_ON,2,69,127},{0,XTC_MIDI_CONTROL,2,74,127}};
xtc_midi_plan_reset(mapped);
assert(xtc_midi_plan_render_stereo(mapped,0,pan,2,actual,right,400) == XTC_OK);
assert(actual[300] == 0 && right[300] != 0);
}
// Mutating the model does not change an already prepared mapping.
bindings[0] = bindings[1] = bindings[2] = XTC_CC_DISABLED;
assert(xtc_project_set_control_map(project,2,bindings) == XTC_OK);
xtc_midi_plan_reset(mapped);
assert(xtc_midi_plan_render(mapped,0,remapped,9,actual,400) == XTC_OK);
for (i = 0; i < 400; ++i) assert(actual[i] == whole[i]);
assert(xtc_midi_plan_create(project,48000,&disabled) == XTC_OK);
{
const xtc_midi_event unbound[] = {{0,XTC_MIDI_NOTE_ON,2,69,127},{3,XTC_MIDI_CONTROL,2,1,0}};
assert(xtc_midi_plan_render(disabled,0,unbound,2,actual,400) == XTC_OK);
assert(actual[300] != 0); // No mapped volume ramp reaches zero.
}
xtc_midi_plan_destroy(mapped); xtc_midi_plan_destroy(disabled);
}
xtc_midi_plan_reset(a);
assert(xtc_midi_plan_render(a,400,NULL,0,whole,400) == XTC_OK);
for (i = 0; i < 400; ++i) assert(whole[i] == 0);
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b); xtc_project_destroy(project);
puts("Sample-timed CC7/CC10/sustain, retrigger, note-off and block partition checks passed.");
puts("Sample-timed default/remapped/disabled controllers, immutable maps, sustain, retrigger, note-off and block partition checks passed.");
return 0;
}

View file

@ -71,12 +71,12 @@ int main(void)
assert(xtc_project_set_mix(p, a, NAN, 0, 0, 0) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_set_mix(p, a, 1, 2, 0, 0) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_set_mix(p, a, 1, 0, 2, 0) == XTC_ERR_INVALID_ARGUMENT);
point = (xtc_automation_point){a, 0, XTC_AUTOMATION_GAIN, 0};
point = (xtc_automation_point){a, 0, XTC_AUTOMATION_GAIN, 0,0};
assert(xtc_project_set_automation(p, &point) == XTC_OK);
point.tick = 20;
point.value = 1;
assert(xtc_project_set_automation(p, &point) == XTC_OK); /* sample 500 */
point = (xtc_automation_point){a, 0, XTC_AUTOMATION_PAN, -1};
point = (xtc_automation_point){a, 0, XTC_AUTOMATION_PAN, -1,0};
assert(xtc_project_set_automation(p, &point) == XTC_OK);
point.tick = 20;
point.value = 1;
@ -165,8 +165,8 @@ int main(void)
{
const char *bad[] = {"MIX 7fc00000 00000000 0 0", "MIX 40800001 00000000 0 0",
"MIX 3f800000 40000000 0 0", "MIX 3f800000 00000000 2 0",
"POINT 999 0 0 3f800000", "POINT 0 2 0 3f800000",
"POINT 0 0 0 7f800000", "POINT 0 0 20 3f800000"};
"POINT 999 0 0 3f800000 0", "POINT 0 2 0 3f800000 0",
"POINT 0 0 0 7f800000 0", "POINT 0 0 20 3f800000 0", "POINT 0 0 0 3f800000 2"};
for (i = 0; i < sizeof(bad) / sizeof(*bad); ++i) {
char *start = strstr(bytes, i < 4 ? "MIX " : "POINT "), *end;
assert(start);
@ -197,7 +197,7 @@ int main(void)
fputc('\n',legacy); continue;
}
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6))
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4))
assert(fputs(line, legacy) >= 0);
}
assert(fclose(f) == 0 && fclose(legacy) == 0);
@ -237,7 +237,7 @@ int main(void)
xtc_history_destroy(history);
assert(xtc_project_remove_track(p, a) == XTC_OK &&
xtc_project_automation_count(p, &count) == XTC_OK && count == 0);
point = (xtc_automation_point){b, 0, XTC_AUTOMATION_GAIN, 1};
point = (xtc_automation_point){b, 0, XTC_AUTOMATION_GAIN, 1,0};
for (i = 0; i < XTC_MAX_AUTOMATION_POINTS; ++i) {
point.tick = i;
assert(xtc_project_set_automation(p, &point) == XTC_OK);

View file

@ -0,0 +1,97 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_control.h"
#include "xtc/xtc_midi.h"
#include "../../src/mixer/mix_internal.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
xtc_project *p = NULL, *loaded = NULL;
xtc_midi_plan *reference = NULL, *whole = NULL, *split = NULL;
xtc_parameter_state state, sentinel;
uint64_t track, clip, note;
float base[80], left[80], right[80], sl[80], sr[80];
unsigned int map[3] = {1,2,3}, actual[3];
size_t i, cursor;
char path[] = "/tmp/xtc-parameters-XXXXXX";
int fd = mkstemp(path); assert(fd >= 0); close(fd);
assert(xtc_project_create(&p) == XTC_OK);
assert(xtc_project_add_track(p,"Parameters",&track) == XTC_OK);
assert(xtc_project_add_midi_clip(p,track,0,960,&clip) == XTC_OK);
assert(xtc_project_add_note(p,clip,0,960,69,127,0,&note) == XTC_OK);
assert(xtc_midi_plan_create(p,48000,&reference) == XTC_OK);
assert(xtc_midi_plan_render(reference,0,NULL,0,base,80) == XTC_OK);
{
xtc_parameter_key gain = {XTC_PARAMETER_TRACK,track,0}, pan = {XTC_PARAMETER_TRACK,track,1};
xtc_parameter_key missing = {XTC_PARAMETER_TRACK,UINT64_MAX,0};
xtc_mix_snapshot mix;
assert(xtc_project_parameter(p,gain,48000,&state) == XTC_OK && !state.automated && state.value == 1);
assert(!strcmp(state.info.unit,"linear") && state.info.minimum == 0 && state.info.maximum == 4);
sentinel = state;
assert(xtc_project_parameter(p,missing,48000,&state) == XTC_ERR_NOT_FOUND && !memcmp(&state,&sentinel,sizeof(state)));
assert(xtc_project_automate(p,missing,48000,0,0,1,0) == XTC_ERR_NOT_FOUND);
assert(xtc_project_automate(p,gain,48000,0,1,.25f,0) == XTC_OK);
assert(xtc_project_automate(p,gain,48000,1,0,1,0) == XTC_OK);
assert(xtc_project_automate(p,gain,48000,2,0,.5f,0) == XTC_OK);
assert(xtc_project_automate(p,pan,48000,0,1,-1,0) == XTC_OK);
assert(xtc_project_automate(p,pan,48000,2,1,1,0) == XTC_OK);
assert(xtc_project_automate(p,gain,48000,3,2,.5f,0) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_set_parameter(p,gain,48000,3) == XTC_OK);
assert(xtc_project_parameter(p,gain,48000,&state) == XTC_OK && state.automated && state.value == 3);
assert(xtc_project_set_parameter(p,gain,48000,NAN) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_mix_prepare(p,1,&mix) == XTC_OK && xtc_mix_level(&mix,0,0,2) == .5f); // Colliding ticks: latest wins.
}
assert(xtc_project_get_control_map(p,0,actual) == XTC_OK && actual[0] == 7 && actual[1] == 10 && actual[2] == 64);
assert(xtc_project_set_control_map(p,0,map) == XTC_OK);
map[1] = 1;
assert(xtc_project_set_control_map(p,0,map) == XTC_ERR_INVALID_ARGUMENT);
map[1] = 120;
assert(xtc_project_set_control_map(p,0,map) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_get_control_map(p,0,actual) == XTC_OK && actual[0] == 1 && actual[1] == 2 && actual[2] == 3);
assert(xtc_project_save(p,path) == XTC_OK);
assert(xtc_project_create(&loaded) == XTC_OK && xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_project_get_control_map(loaded,0,actual) == XTC_OK && actual[0] == 1 && actual[1] == 2 && actual[2] == 3);
{
xtc_automation_point point;
assert(xtc_project_get_automation(loaded,0,&point) == XTC_OK && point.step == 1);
}
assert(xtc_midi_plan_create(loaded,48000,&whole) == XTC_OK && xtc_midi_plan_create(loaded,48000,&split) == XTC_OK);
assert(xtc_midi_plan_render_stereo(whole,0,NULL,0,left,right,80) == XTC_OK);
for (cursor = 0; cursor < 80;) {
size_t frames = 13; if (frames > 80-cursor) frames = 80-cursor;
assert(xtc_midi_plan_render_stereo(split,cursor,NULL,0,sl+cursor,sr+cursor,frames) == XTC_OK);
cursor += frames;
}
for (i = 0; i < 80; ++i) {
float gain = i < 25 ? .25f : i < 50 ? 1-(float)(i-25)/50 : .5f;
assert(left[i] == sl[i] && right[i] == sr[i]);
assert(fabsf(left[i] - (i < 50 ? base[i]*gain : 0)) < .000001f);
assert(fabsf(right[i] - (i < 50 ? 0 : base[i]*gain)) < .000001f);
}
assert(xtc_midi_plan_render_stereo(split,29,NULL,0,sl,sr,13) == XTC_OK);
for (i = 0; i < 13; ++i) assert(sl[i] == left[i+29] && sr[i] == right[i+29]);
// Explicit backward transport wrap (no loop transport feature is implied).
assert(xtc_midi_plan_render_stereo(split,0,NULL,0,sl,sr,80) == XTC_OK);
for (i = 0; i < 80; ++i) assert(sl[i] == left[i] && sr[i] == right[i]);
{
FILE *source = fopen(path,"r"), *dest;
char bad[128], line[1024];
snprintf(bad,sizeof(bad),"%s-bad",path); dest = fopen(bad,"w"); assert(source && dest);
while (fgets(line,sizeof(line),source)) {
if (!strncmp(line,"MAP 0 ",6)) fputs("MAP 0 1 1 3\n",dest); else fputs(line,dest);
}
fclose(source); assert(fclose(dest) == 0);
assert(xtc_project_load(loaded,bad) == XTC_ERR_FORMAT);
assert(xtc_project_get_control_map(loaded,0,actual) == XTC_OK && actual[1] == 2);
unlink(bad);
}
xtc_midi_plan_destroy(reference); xtc_midi_plan_destroy(whole); xtc_midi_plan_destroy(split);
xtc_project_destroy(p); xtc_project_destroy(loaded); unlink(path);
puts("Common parameter identity, hold/linear lanes, static precedence, persistence, seeks and wrap passed.");
return 0;
}