Phase 26: add common parameter targets, step automation and MIDI control mapping
This commit is contained in:
parent
7bff7b535d
commit
901a536f10
38 changed files with 740 additions and 67 deletions
10
Makefile
10
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
106
docs/CONTROL.md
106
docs/CONTROL.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
40
include/xtc/xtc_control.h
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
130
src/control/model.c
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
2
tests/fixtures/README.md
vendored
2
tests/fixtures/README.md
vendored
|
|
@ -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
22
tests/fixtures/project-v9.xtc
vendored
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¬e) == 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;
|
||||
|
|
|
|||
|
|
@ -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,©) == 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);
|
||||
|
|
|
|||
|
|
@ -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,¬es) == 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
97
tests/unit/test_parameters.c
Normal file
97
tests/unit/test_parameters.c
Normal 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,¬e) == 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue