Phase 8: Add stereo track mixer, automation and undo/redo
This commit is contained in:
parent
cacc086805
commit
7db5fb7738
45 changed files with 1611 additions and 130 deletions
18
Makefile
18
Makefile
|
|
@ -13,7 +13,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := 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/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)
|
||||
|
|
@ -80,8 +80,12 @@ $(BUILD_DIR)/test_audio: $(BUILD_DIR)/tests/unit/test_audio.o $(BUILD_DIR)/libxt
|
|||
$(BUILD_DIR)/test_midi: $(BUILD_DIR)/tests/unit/test_midi.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm
|
||||
|
||||
test: $(BUILD_DIR)/test_midi $(BUILD_DIR)/test_audio $(BUILD_DIR)/test_project $(BUILD_DIR)/test_graph_edits $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
|
||||
$(BUILD_DIR)/test_mixer: $(BUILD_DIR)/tests/unit/test_mixer.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm
|
||||
|
||||
test: $(BUILD_DIR)/test_mixer $(BUILD_DIR)/test_midi $(BUILD_DIR)/test_audio $(BUILD_DIR)/test_project $(BUILD_DIR)/test_graph_edits $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
|
||||
$(if $(BACKEND_TEST),./$(BACKEND_TEST),@echo "JACK fault tests skipped: JACK development files unavailable")
|
||||
./$(BUILD_DIR)/test_mixer
|
||||
./$(BUILD_DIR)/test_midi
|
||||
./$(BUILD_DIR)/test_audio
|
||||
./$(BUILD_DIR)/test_project
|
||||
|
|
@ -124,14 +128,14 @@ endif
|
|||
ifeq ($(GUI_AVAILABLE),yes)
|
||||
gui: $(BUILD_DIR)/xtc
|
||||
|
||||
$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/libxtc.a
|
||||
$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm
|
||||
|
||||
test-gui: $(BUILD_DIR)/test_gui
|
||||
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||
|
||||
ifeq ($(JACK_AVAILABLE),yes)
|
||||
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/libxtc.a
|
||||
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
|
||||
|
||||
test-gui-jack: $(BUILD_DIR)/test_gui_jack
|
||||
|
|
@ -142,7 +146,7 @@ test-gui-jack:
|
|||
@exit 1
|
||||
endif
|
||||
|
||||
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/libxtc.a
|
||||
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
|
||||
else
|
||||
test-gui-jack test-gui:
|
||||
|
|
@ -174,3 +178,7 @@ clean:
|
|||
-include $(BUILD_DIR)/tests/unit/test_midi.d
|
||||
|
||||
-include $(BUILD_DIR)/tests/integration/test_jack_midi.d
|
||||
|
||||
-include $(BUILD_DIR)/src/gui/mixer_widget.d
|
||||
|
||||
-include $(BUILD_DIR)/tests/unit/test_mixer.d
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -17,7 +17,7 @@ This repository is a starter scaffold, not a production DAW. It currently provid
|
|||
- a mono graph model with port validation, cycle rejection and topological ordering;
|
||||
- compiled offline plans with preallocated buffers, sine oscillator, audio source, gain, meter and output nodes;
|
||||
- C tests for deterministic rendering, routing, validation and lifecycle;
|
||||
- an optional JACK-compatible mono backend with start/stop, routing and status;
|
||||
- an optional JACK-compatible backend with mono input and mono/stereo output with start/stop, routing and status;
|
||||
- safe control/audio plan handoff and concurrent replacement tests;
|
||||
- an optional FLTK shell with engine controls, view switching and backend status;
|
||||
- a modular graph editor with transactional edits and live plan adoption status;
|
||||
|
|
@ -25,6 +25,8 @@ This repository is a starter scaffold, not a production DAW. It currently provid
|
|||
- sample-counted transport, selection, move/trim, timeline zoom and scrolling;
|
||||
- WAV import/graph playback, bounded recording and missing-asset relinking;
|
||||
- MIDI clips, a sine instrument, JACK MIDI input and a basic piano roll;
|
||||
- stereo track gain/pan/mute/solo, output meters and gain/pan automation lanes;
|
||||
- bounded undo/redo with keyboard shortcuts;
|
||||
- architecture, format, GUI, roadmap, and decision documents.
|
||||
|
||||
The JACK backend has passed software-loopback testing; physical-device playback
|
||||
|
|
@ -65,7 +67,11 @@ select a missing clip and use File/Relink selected audio to repair it.
|
|||
Add MIDI clip on an existing track, then open Piano roll (Ctrl+4) to draw notes.
|
||||
Connect a MIDI source to the JACK `midi_in` port for live playing; Panic/F9
|
||||
silences held notes. See [MIDI workflow and limits](docs/MIDI.md).
|
||||
See [audio workflow and limits](docs/AUDIO_FILES.md). The mixer remains a placeholder.
|
||||
See [audio workflow and limits](docs/AUDIO_FILES.md).
|
||||
Mixer (Ctrl+2) selects a track for gain/pan/mute/solo and automation editing.
|
||||
Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes up to 32 edits.
|
||||
The GUI now exposes left `output` and `output_right` JACK ports; route both
|
||||
explicitly for stereo. See [mixer workflow and limits](docs/MIXER.md).
|
||||
Use Add track/Add region, drag regions or their edges, and Zoom +/-; scrollbars pan.
|
||||
Ctrl+N/O/S creates, opens or saves a project; Save As chooses another path.
|
||||
Arrangement edits stop playback; graph edits remain live.
|
||||
|
|
|
|||
|
|
@ -118,3 +118,15 @@ prepared MIDI runtime state exclusively while active, with lock-free panic
|
|||
requests and no callback allocation/I/O. Stop/seek clear live voices; scheduled
|
||||
notes chase their absolute phase. See MIDI.md for event ordering, panic semantics,
|
||||
capacities and test coverage. No new dependency is required.
|
||||
|
||||
## Phase 8 mix snapshots and history
|
||||
|
||||
`src/mixer` owns validated per-track settings and bounded gain/pan automation.
|
||||
Audio/MIDI preparation copies and indexes these into source snapshots; paired
|
||||
source buffers feed the same graph independently per stereo side. Atomic backend
|
||||
output-peak snapshots cross to the GUI without exposing DSP state. Mixer edits
|
||||
stop transport before subsequent preparation. `src/project/history.c` provides
|
||||
32-edit C project snapshots; checkout clones a candidate and accept moves the
|
||||
cursor only after successful GUI/backend installation. No samples or FLTK state
|
||||
are stored in history. `mixer_widget.cpp` is only controls/drawing/gestures.
|
||||
See MIXER.md and PROJECT_FORMAT.md for ownership, limits and v4 migration.
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ without normalization. Failed additions leave IDs and graph contents unchanged.
|
|||
thread. Empty graphs compile to silence. Sample rate must be finite and positive,
|
||||
oscillators must be at or below Nyquist, and max_frames is 1..65536. Compilation
|
||||
allocates and copies topology/parameters, builds a topological schedule and
|
||||
input adjacency, and allocates one max_frames float buffer per node. The
|
||||
maximum buffer storage is 64 MiB per plan. Compilation may fail with an
|
||||
input adjacency, and allocates paired max_frames float buffers per node for stereo. The
|
||||
maximum buffer storage is 128 MiB per plan (32 MiB for the GUI 64-node limit). Compilation may fail with an
|
||||
allocation error; output plan is then NULL. Destroy a plan on the control thread.
|
||||
|
||||
`xtc_plan_process` uses only prepared state: no allocation/free, sorting,
|
||||
|
|
@ -86,7 +86,7 @@ publication and JACK processing; offline tests alone do not qualify hardware.
|
|||
|
||||
Use `xtc_backend.h` on one serialized control thread:
|
||||
|
||||
1. Open a config with `XTC_BACKEND_JACK`, a client name, channel_count 1, and
|
||||
1. Open a config with `XTC_BACKEND_JACK`, a client name, channel_count 1 (or 2 for stereo output), and
|
||||
max_frames 1..65536. It must accommodate the server's current buffer size.
|
||||
The actual rate and buffer size are returned through get_status. Open does
|
||||
not start the server or acquire devices directly. Invalid config returns
|
||||
|
|
@ -134,8 +134,7 @@ JACK notification callbacks were audited for prohibited operations; JACK's own
|
|||
internal realtime guarantees depend on the installed implementation/server.
|
||||
|
||||
Validation commands and results are recorded in BUILD.md and the integration
|
||||
test README. Current limitations: mono input/output, no parameter ramping or
|
||||
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
|
||||
test README. Current limitations: mono input, no crossfaded graph changes, no automatic reconnect,
|
||||
no physical-device or long-duration endurance qualification. Phase 3 connects
|
||||
the FLTK shell to backend controls and status; see GUI.md. Its editors remain
|
||||
placeholders except the Phase 4 modular editor described below.
|
||||
|
|
@ -250,3 +249,17 @@ prepared MIDI runtime state exclusively while active, with lock-free panic
|
|||
requests and no callback allocation/I/O. Stop/seek clear live voices; scheduled
|
||||
notes chase their absolute phase. See MIDI.md for event ordering, panic semantics,
|
||||
capacities and test coverage. No new dependency is required.
|
||||
|
||||
## Phase 8 stereo mixing and automation
|
||||
|
||||
Track settings and automation are copied into immutable source-plan snapshots.
|
||||
`xtc_audio_plan_render_stereo` and `xtc_midi_plan_render_stereo` pan track audio
|
||||
and scheduled notes before summing into paired source buffers. Mono wrappers
|
||||
retain gain/mute/solo/automation but ignore pan. `xtc_plan_process_stereo` and
|
||||
`xtc_realtime_process_stereo` process both buses through the same graph topology
|
||||
with one oscillator phase advance, preserving mono APIs and graph publication.
|
||||
The backend preallocates paired buffers, registers an optional output_right port
|
||||
and publishes independently atomic output peak snapshots. Callback work remains
|
||||
bounded and allocation/I/O/lock free. Meter UI reads no active plan memory.
|
||||
See MIXER.md for exact pan law, automation interpolation, event/source exceptions,
|
||||
memory/history bounds and verification. Input/recording remain mono.
|
||||
|
|
|
|||
|
|
@ -23,13 +23,15 @@ can briefly block the GUI. Candidate compilation retains the old plan until
|
|||
success; temporary build memory can include both old and candidate buffers.
|
||||
|
||||
The callback renders only overlapping clip ranges from memory. Clips on all
|
||||
tracks are summed at unity gain into the graph's **Audio clips** source node.
|
||||
tracks are summed through their mix/automation settings into the graph's
|
||||
**Audio clips** source node.
|
||||
Import creates that source and connects it to an existing/new output if no
|
||||
audio source exists. Subsequent routing is explicit: disconnect it or route it
|
||||
through gain/meter nodes in Modular Graph. Removing the output silences editor
|
||||
graphs. This source does not monitor JACK input; unconnected ordinary graph
|
||||
inputs retain their earlier external-input behavior. Track faders/pan/mute
|
||||
and individual track routing belong to the mixer phase.
|
||||
are implemented by the Phase 8 track mixer (see MIXER.md); individual
|
||||
track graph routing remains deferred.
|
||||
|
||||
Linear interpolation converts source sample rate to backend rate. There is no
|
||||
antialiasing filter, time stretching, looping or fades. Musical durations round
|
||||
|
|
|
|||
|
|
@ -175,3 +175,24 @@ A fresh `build/verify` build passed all targets above with no compiler warnings.
|
|||
The no-JACK core/GUI suite and Clang ASan/UBSan core/GUI suite also passed; GUI
|
||||
leak checking uses only the existing fontconfig suppression documented in the
|
||||
integration README. Physical MIDI and audio devices remain unverified.
|
||||
|
||||
## Phase 8 verification
|
||||
|
||||
```sh
|
||||
make all test gui debug test-gui test-gui-jack test-jack-audio test-jack-midi
|
||||
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
|
||||
```
|
||||
|
||||
`test_mixer` covers stereo gain/pan, mute/solo, sample automation, block
|
||||
independence, WAV and graph routing, v4 persistence failures and bounded history.
|
||||
GUI tests add automation mouse edits, mixer keys, history rollback/branching and
|
||||
save/reopen. The existing live MIDI test now uses two software capture ports and
|
||||
checks hard-right saved-note playback and output meter snapshots; mono audio
|
||||
record/play acceptance remains covered. A native Xvfb mouse smoke check confirms
|
||||
sliders and automation drawing. See MIXER.md for capacities and deferred features.
|
||||
|
||||
The fresh Phase 8 verification build, no-JACK core/GUI suite and Clang
|
||||
ASan/UBSan core/GUI suite passed without compiler warnings. GUI leak checking
|
||||
retains only the existing fontconfig suppression. ThreadSanitizer also passed
|
||||
the recorder concurrency and realtime publication tests. Physical devices and
|
||||
long-running stereo performance remain unverified.
|
||||
|
|
|
|||
15
docs/GUI.md
15
docs/GUI.md
|
|
@ -31,7 +31,7 @@ by the GUI. The initially empty graph produces silence.
|
|||
Use this after server loss or sample-rate changes. A failed close preserves
|
||||
the handle and reports the failure instead of opening another client.
|
||||
- **View menu / Ctrl+1, Ctrl+2, Ctrl+3:** switch between sequencer, mixer and
|
||||
modular graph. Tabs provide the same view switching. The mixer remains a placeholder; graph and sequencer editors are described below.
|
||||
modular graph. Tabs provide the same view switching. Mixer, graph and sequencer editors are described below.
|
||||
- **File / Quit / Ctrl+Q** or the window close button: close the backend and
|
||||
synchronize audio callbacks before hiding/destroying widgets. A failed close
|
||||
leaves the window open for a retry. New/Open/Save actions are described below. About identifies the development milestone and version.
|
||||
|
|
@ -154,7 +154,8 @@ can block the GUI while reading, but never the audio callback.
|
|||
See AUDIO_FILES.md for supported WAV formats, memory limits, musical placement,
|
||||
recording overflow/partial-take recovery, external asset portability and the
|
||||
software-only acceptance tests. Waveform drawing, input monitoring, recording
|
||||
latency compensation and per-track mixing are not implemented.
|
||||
latency compensation and per-track graph inserts are not implemented.
|
||||
Per-track gain/pan/mute/solo is available in Phase 8 below.
|
||||
|
||||
## Phase 7 piano roll
|
||||
|
||||
|
|
@ -166,3 +167,13 @@ content is retained. Edits stop transport and are blocked during audio recording
|
|||
Panic/F9 silences held notes; Record continues to mean WAV audio capture.
|
||||
Modular Graph offers Add instrument for explicit routing. Status includes MIDI
|
||||
drops. See MIDI.md for voice limits, unsupported messages and live input routing.
|
||||
|
||||
## Phase 8 mixer and editing
|
||||
|
||||
Mixer (Ctrl+2) now has a selectable track strip, gain/pan controls, mute/solo,
|
||||
Output L/R peaks and editable gain/pan automation lanes. Mixer edits stop
|
||||
playback. Click the lane to add/replace points, right-click near a point to
|
||||
remove, and change First tick to navigate. Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y undo
|
||||
and redo up to 32 project edits; New/Open resets history. M/S and arrow keys
|
||||
provide mixer shortcuts. See MIXER.md for exact controls, history rollback,
|
||||
stereo port routing, automation semantics and prototype limits.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ captures audio to WAV: MIDI capture and Standard MIDI File import are deferred.
|
|||
Add/select a track in Sequencer, choose Add MIDI clip, then Piano roll (Ctrl+4).
|
||||
Creation adds an instrument-to-output route if no instrument exists. Existing
|
||||
instruments retain their routes; use Modular Graph to connect them as needed.
|
||||
All MIDI clips and live input share one mono instrument mix. No hardware ports
|
||||
Scheduled MIDI clips now apply their track mix/automation before summing to
|
||||
paired stereo instrument buffers; live input stays centered and unassigned. No hardware ports
|
||||
are connected automatically. Connect a MIDI source to the backend's `midi_in`
|
||||
JACK port, and route audio output explicitly to hear it while Play is running.
|
||||
|
||||
|
|
@ -71,3 +72,5 @@ panic, disconnection and saved/reopened scheduled playback. JACK port discovery
|
|||
may lag activation under PipeWire; the test waits before connecting.
|
||||
Physical MIDI devices, bridge/device loss behind an unchanged port, latency and
|
||||
long-running performance remain unverified. Panic/Stop provides manual recovery.
|
||||
|
||||
Phase 8 adds track mixing, automation and project undo/redo; see MIXER.md.
|
||||
|
|
|
|||
98
docs/MIXER.md
Normal file
98
docs/MIXER.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Mixer, automation and undo
|
||||
|
||||
Phase 8 provides one selectable channel strip per project track. Open Mixer
|
||||
(Ctrl+2), select a track, and set Gain, Pan, Mute and Solo. Changes stop transport;
|
||||
Play prepares independent audio/MIDI snapshots with the accepted settings.
|
||||
No edit changes a plan that the callback is using. Editing is blocked while
|
||||
recording. Graph edits retain their existing live publication behavior.
|
||||
|
||||
Gain is a linear multiplier from 0 to 4. Pan ranges from -1 (left) to +1 (right):
|
||||
center has unity on both sides and moving away attenuates the opposite side
|
||||
linearly. This preserves existing centered mono levels; it is not an equal-power
|
||||
pan law. Mute wins over Solo. If any track is soloed, only soloed, unmuted tracks
|
||||
play. WAV clips and scheduled MIDI use their containing track's settings before
|
||||
summing into the audio/instrument graph sources. Live MIDI input and independent
|
||||
graph oscillators/external input are unassigned sources and bypass track mixing.
|
||||
Stereo WAV files are still downmixed at import; stereo output comes from panning
|
||||
those mono sources. Separate per-track graph inserts/sends are deferred.
|
||||
|
||||
## Automation lanes
|
||||
|
||||
Choose Gain or Pan under Automation. The lane shows four bars (15360 ticks);
|
||||
First tick moves its viewport across the project. Grid labels are absolute ticks.
|
||||
Click to add a point or replace the point at that snapped time (1/16 note, 240
|
||||
ticks). Vertical position maps to gain 0..4 or pan -1..1, top to bottom descending.
|
||||
Right-click near a point's time to remove it. Removing every point restores the
|
||||
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
|
||||
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
|
||||
arrays; it never sorts, allocates, accesses project data, or calls the GUI.
|
||||
Seeking and block partitioning give the same timeline values. Mute/solo are
|
||||
static; automation capture, modes, curves and tempo automation are deferred.
|
||||
|
||||
## Stereo output and meters
|
||||
|
||||
The GUI opens the JACK backend with two audio output ports: `output` (left) and
|
||||
`output_right`. Input and raw WAV recording remain mono. Connect both outputs
|
||||
explicitly using an external JACK routing tool. Nothing connects hardware
|
||||
automatically. Legacy configurations with channel_count=1 and the original
|
||||
port/route APIs remain supported; output_name(channel) queries either stereo
|
||||
port. Old mono audio/MIDI render functions apply gain/mute/solo and gain
|
||||
automation while ignoring pan; the stereo variants apply both lanes.
|
||||
|
||||
The graph executes the same topology separately on left and right buses;
|
||||
oscillators advance once per frame and are centered on both sides. Existing
|
||||
port metadata and validation describe the mono graph topology. Stereo processing
|
||||
uses explicit paired buffers, not new port IDs. Output buffers must be distinct;
|
||||
stereo callers must not alias them with source buffers. Graph-node meter reads
|
||||
report the maximum of both sides on the processing owner only.
|
||||
|
||||
Mixer Output L/R bars show last-block post-graph absolute peaks, polled at 100 ms.
|
||||
Peaks above unity turn red. Independent lock-free integer snapshots avoid sharing
|
||||
DSP pointers or float state between callback and GUI; stopped meters show zero.
|
||||
They are output meters, without per-track RMS, peak hold or loudness analysis.
|
||||
There is no limiter: finite summed output can exceed unity. GUI polling may miss
|
||||
brief peaks between refreshes; these bars are not overload history.
|
||||
|
||||
## Undo and keyboard workflow
|
||||
|
||||
Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes. The C history keeps 32 project edits
|
||||
plus a baseline, cloning metadata and graph, never audio samples. Common graph,
|
||||
track, clip, note, mixer, import/relink and automation edits are included. One
|
||||
accepted gesture is one step; selection, scrolling and graph-node placement are
|
||||
GUI state and are not undoable. New/Open resets history; Save retains it.
|
||||
Undo/redo stops and resets transport and resets editor selections/layout. The
|
||||
candidate is installed before moving the history cursor: validation, asset-plan
|
||||
or backend failure retains the current document and history position. Missing
|
||||
assets retain the existing silent/reporting behavior. A new edit after Undo
|
||||
removes the redo branch; the oldest state is evicted at capacity. Allocation
|
||||
ID high-water marks survive Undo/branching so abandoned IDs are not reused. History is
|
||||
session-only and Undo marks the document dirty even if it matches a saved state.
|
||||
External WAV files are never deleted or restored by Undo; recording itself cannot
|
||||
be undone while active. An allocation failure retaining a new history snapshot
|
||||
reports an error; the accepted edit remains in the document.
|
||||
|
||||
With mixer focus, M/S toggles mute/solo, Left/Right changes gain by 0.05,
|
||||
Shift+Left/Right changes pan, and Up/Down selects a track. Native sliders also
|
||||
support keyboard navigation; Ctrl+1/2/3 switches views, Ctrl+4 opens Piano roll,
|
||||
F5/F8/Space controls transport and F9 panics, as before. There is no multiselection,
|
||||
clipboard editing, track rename or automation-point dragging yet.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
`test_mixer` verifies exact stereo gain/pan, mute/solo precedence, automation
|
||||
samples and block equivalence, stereo graph phase/routing, WAV mixing, v4 exact
|
||||
roundtrip/truncations/malformed records, lane capacities and history eviction and
|
||||
branching. Backend fault injection verifies stereo registration failure, output
|
||||
names, output separation and atomic meters. GUI tests edit lanes with mouse
|
||||
events, use mixer keys and exercise undo/redo, failed installation rollback,
|
||||
branching, save/reopen and history reset. Live JACK MIDI acceptance verifies
|
||||
saved/reopened hard-right playback and stereo peaks using only software clients.
|
||||
A native Xvfb mouse smoke check covers sliders and automation drawing.
|
||||
Physical stereo devices, listening quality, realtime endurance and high-density
|
||||
project performance remain unverified; no new dependency was introduced.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Project format v3 (reads v1/v2)
|
||||
# Project format v4 (reads v1/v2/v3)
|
||||
|
||||
Phases 5–7 use a bounded text format implemented in C99 without a parser library.
|
||||
Phases 5–8 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 3
|
||||
XTC_PROJECT 4
|
||||
APP 0 1 0
|
||||
TEMPO 120000
|
||||
IDS 1 1
|
||||
|
|
@ -22,11 +22,13 @@ NODE 1 4 00000000
|
|||
EDGE 0 1
|
||||
TRACKS 1
|
||||
TRACK 0 547261636b
|
||||
MIX 3f800000 00000000 0 0
|
||||
CLIPS 1
|
||||
CLIP 0 0 3840 2880 960 3840 526567696f6e
|
||||
ASSET -
|
||||
MIDI 0
|
||||
NOTES 0 0
|
||||
AUTOMATION 0
|
||||
END
|
||||
```
|
||||
|
||||
|
|
@ -40,30 +42,37 @@ END
|
|||
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source, 6 instrument), and exactly eight
|
||||
hexadecimal digits encoding the IEEE binary32 parameter bits, independent
|
||||
of locale and host byte order. Nonfinite values are rejected.
|
||||
- `EDGE`: source and destination node IDs. Mono port directions are implied
|
||||
- `EDGE`: source and destination node IDs. Port directions are implied
|
||||
by node type; endpoints, duplicates and cycles are validated by the graph API.
|
||||
- `TRACK`: ID and hex-encoded name bytes.
|
||||
- `TRACK`: ID and hex-encoded name bytes. In v4 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.
|
||||
- `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
|
||||
length. Overlaps are allowed and audio clips mix through the audio source node.
|
||||
- `ASSET`: required after each CLIP in v2/v3; "-" means no audio asset, otherwise
|
||||
- `ASSET`: required after each CLIP in v2/v3/v4; "-" means no audio asset, otherwise
|
||||
hex-encoded absolute path bytes, <=511 bytes after decoding, no embedded NUL
|
||||
or ASCII control characters. Each clip directly references a WAV; no sample
|
||||
data, pointers or DSP state are embedded. Paths can contain spaces.
|
||||
- `MIDI`: required after each ASSET in v3; 0 for silent/audio regions, 1 for
|
||||
- `MIDI`: required after each ASSET in v3/v4; 0 for silent/audio regions, 1 for
|
||||
MIDI clips. MIDI clips must have no asset.
|
||||
- `NOTES`: next stable note ID and global note count, followed by that many
|
||||
`NOTE id clip_id start_tick length_tick pitch velocity channel` records.
|
||||
Notes reference MIDI clips; start/length are source-relative, positive length
|
||||
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.
|
||||
Follow with `POINT track_id parameter tick value_bits` for each point.
|
||||
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.
|
||||
- Names decode to nonempty valid UTF-8 of at most 63 bytes; ASCII control
|
||||
characters and embedded NULs are forbidden. Hex avoids quoting ambiguity.
|
||||
|
||||
Ticks use 960 per quarter note, 3840 per bar. All region endpoints are bounded
|
||||
by 38400000 ticks (10000 bars). Limits: 64 tracks, 1024 clips, 64 graph nodes,
|
||||
4096 edges, 4096 notes, 4 MiB input, 1535 bytes per record, 12 tokens per record. The reader
|
||||
4096 edges, 4096 notes, 1024 automation points, 4 MiB input, 1535 bytes per record, 12 tokens per record. The reader
|
||||
rejects invalid references, duplicate IDs, inconsistent next IDs, ranges,
|
||||
truncated records, raw control/non-ASCII bytes, and trailing data.
|
||||
|
||||
|
|
@ -71,8 +80,10 @@ Unsupported format versions return UNSUPPORTED. Other malformed data returns
|
|||
FORMAT. The reader explicitly accepts v1 records without ASSET lines, with
|
||||
node types 1..4, migrating their clips to empty asset paths. Version 2 accepts
|
||||
node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes.
|
||||
Saving always writes v3. Unknown versions are rejected without replacing the destination; older
|
||||
applications cannot read v3. Migration and maximum-path roundtrips are tested.
|
||||
Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate
|
||||
all tracks to unity gain, centered pan, mute/solo off and no automation.
|
||||
Saving always writes v4. Unknown versions are rejected without replacing the destination; older
|
||||
applications cannot read v4. Migration and maximum-path roundtrips are tested.
|
||||
|
||||
## Ownership and failures
|
||||
|
||||
|
|
@ -86,7 +97,8 @@ loss is not guaranteed. Save replaces symlinks themselves and does not preserve
|
|||
existing file permissions. POSIX I/O is private to the Linux-first persistence
|
||||
module; public headers contain no POSIX types.
|
||||
|
||||
Saved data reproduces the arrangement and graph, including ID allocation state.
|
||||
Saved data reproduces the arrangement, graph, track mix and automation, including
|
||||
ID allocation state. Undo history is not saved. See MIXER.md for pan/interpolation.
|
||||
Graph node positions, timeline zoom/scroll/selection, transport position and DSP
|
||||
oscillator phase are GUI/session state and are not saved. Asset references are
|
||||
external absolute paths, not copied on Save As. Metadata loading preserves
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ See MIDI.md for capacities, immediate note-off, routing and hardware limits.
|
|||
## Phase 8 — Mixer and editing
|
||||
Gain/pan, meters, mute/solo, automation, undo/redo, keyboard workflow.
|
||||
|
||||
Implemented: selectable per-track channel strips with gain/pan and mute/solo;
|
||||
stereo graph/JACK output and output peak meters; editable gain/pan automation
|
||||
lanes with sample interpolation; bounded 32-edit C snapshot undo/redo and mixer
|
||||
keyboard controls. Format v4 persists mixing/automation and reads v1–v3.
|
||||
Core, GUI rollback/gesture/history and live stereo software playback tests pass.
|
||||
See MIXER.md for routing, mono compatibility, pan law, history behavior and limits.
|
||||
Track inserts/sends, live fader adoption, MIDI capture, per-track meters and
|
||||
advanced automation remain deferred. No project-file editing is needed for the
|
||||
implemented composition/mixing workflow.
|
||||
|
||||
## Phase 9 — Extensibility and release
|
||||
Native node API, evaluate plugin hosting, licensing review, packaging,
|
||||
installation tests, performance and recovery tests.
|
||||
|
|
|
|||
17
docs/decisions/0007-track-mixer-automation-and-history.md
Normal file
17
docs/decisions/0007-track-mixer-automation-and-history.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# 0007 — Track mixing, paired stereo buses and snapshot history
|
||||
|
||||
Status: accepted, Phase 8.
|
||||
|
||||
Apply gain/pan/mute/solo and gain/pan automation in prepared track sources, then
|
||||
run paired stereo buses through the existing modular topology. This preserves
|
||||
mono APIs and graph edits without inventing new graph-port IDs. Center-unity
|
||||
linear pan preserves previous centered levels. Add optional stereo JACK output
|
||||
and atomic output peak snapshots; keep input/recording mono.
|
||||
|
||||
Use 1024 global automation points, linear sample interpolation, and stopped
|
||||
preparation for mixer edits. Store settings/points in v4 and migrate v1–v3 to
|
||||
unity/center/no automation. Use 32 bounded C project snapshots for common undo
|
||||
edits; commit history movement only after candidate installation succeeds.
|
||||
Samples and GUI-only state stay outside history. Track graph inserts/sends,
|
||||
per-track meters, live fader publication, automation modes/capture, nonlinear
|
||||
curves and equal-power pan are future extensions. See ../MIXER.md.
|
||||
|
|
@ -24,6 +24,8 @@ void xtc_audio_plan_destroy(xtc_audio_plan *plan);
|
|||
size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **out_first_path);
|
||||
/* Single consumer; output replaced with overlap-summed mono at unity gain.
|
||||
* Linear rate conversion, no time stretching. Output must have frames floats. */
|
||||
/* Stereo outputs must be distinct; NULL right selects mono gain-only rendering. */
|
||||
xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *, uint64_t, float *left, float *right, size_t);
|
||||
xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position,
|
||||
float *output, size_t frames);
|
||||
/* Control-thread creation opens a same-directory temporary float32 WAV.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ typedef struct {
|
|||
xtc_backend_kind kind;
|
||||
const char *client_name; /* Borrowed for open only; nonempty. */
|
||||
size_t max_frames; /* 1..65536, at least the server's current block. */
|
||||
size_t channel_count; /* Currently must be 1. */
|
||||
size_t channel_count; /* 1 mono or 2 stereo output; input/recording remain mono. */
|
||||
} xtc_backend_config;
|
||||
typedef struct {
|
||||
float peak_left, peak_right; /* Last-block output peaks, zero while stopped. */
|
||||
unsigned int sample_rate;
|
||||
unsigned int buffer_frames;
|
||||
unsigned int xruns; /* Wraps modulo UINT_MAX + 1. */
|
||||
|
|
@ -63,6 +64,8 @@ void xtc_backend_collect(xtc_backend *backend);
|
|||
/* Snapshot fields are individually coherent, not a transactional snapshot. */
|
||||
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status);
|
||||
/* Borrowed names valid until close. Direction is the client's own port. */
|
||||
/* Output channel 0 is legacy output, channel 1 is output_right in stereo. */
|
||||
xtc_result xtc_backend_output_name(const xtc_backend *, size_t channel, const char **out_name);
|
||||
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction,
|
||||
const char **out_name);
|
||||
/* Enumerates available mono audio ports (devices and software routes). Visitor
|
||||
|
|
|
|||
|
|
@ -109,6 +109,12 @@ void xtc_plan_destroy(xtc_plan *plan);
|
|||
* unless require_output_nodes is set. Empty graphs produce silence.
|
||||
* Unconnected gain/meter/output inputs receive external input. */
|
||||
/* Separate mono source buffers; NULL is silence. Existing wrappers omit MIDI. */
|
||||
/* Stereo graph with paired source buffers and a mono external input duplicated
|
||||
* to both sides. Output buffers must be distinct and must not alias sources.
|
||||
* Stereo oscillator phase advances once per frame. NULL right selects mono. */
|
||||
xtc_result xtc_plan_process_stereo(xtc_plan *, const float *input,
|
||||
const float *audio_left, const float *audio_right, const float *midi_left, const float *midi_right,
|
||||
float *left, float *right, size_t frames);
|
||||
xtc_result xtc_plan_process_sources(xtc_plan *plan, const float *input, const float *timeline,
|
||||
const float *instrument, float *output, size_t frames);
|
||||
/* timeline is a separate mono input for XTC_NODE_AUDIO, NULL means silence. */
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ void xtc_midi_plan_panic(xtc_midi_plan *plan, uint64_t position);
|
|||
* caller order, count<=256, frames<=65536. Invalid input leaves state/output alone.
|
||||
* Renders [position,position+frames); discontinuity clears live voices, chases
|
||||
* scheduled notes at their correct phase. Note-off is immediate (no envelope). */
|
||||
/* Distinct stereo outputs; NULL right retains mono (gain only). */
|
||||
xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *, uint64_t, const xtc_midi_event *, size_t, float *left, float *right, size_t);
|
||||
xtc_result xtc_midi_plan_render(xtc_midi_plan *plan, uint64_t position,
|
||||
const xtc_midi_event *events, size_t count, float *output, size_t frames);
|
||||
#ifdef __cplusplus
|
||||
|
|
|
|||
40
include/xtc/xtc_mixer.h
Normal file
40
include/xtc/xtc_mixer.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#ifndef XTC_MIXER_H
|
||||
#define XTC_MIXER_H
|
||||
#include "xtc_project.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#define XTC_MAX_AUTOMATION_POINTS 1024U
|
||||
#define XTC_HISTORY_EDITS 32U
|
||||
typedef enum { XTC_AUTOMATION_GAIN = 0, XTC_AUTOMATION_PAN = 1 } xtc_automation_parameter;
|
||||
typedef struct {
|
||||
uint64_t track_id, tick;
|
||||
xtc_automation_parameter parameter;
|
||||
float value;
|
||||
} xtc_automation_point;
|
||||
/* 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,
|
||||
* holds first/last outside the lane. Duplicate track/parameter/tick replaces. */
|
||||
xtc_result xtc_project_set_mix(xtc_project *, uint64_t track, float gain, float pan, int mute,
|
||||
int solo);
|
||||
xtc_result xtc_project_set_automation(xtc_project *, const xtc_automation_point *);
|
||||
xtc_result xtc_project_remove_automation(xtc_project *, uint64_t track, xtc_automation_parameter,
|
||||
uint64_t tick);
|
||||
xtc_result xtc_project_automation_count(const xtc_project *, size_t *);
|
||||
xtc_result xtc_project_get_automation(const xtc_project *, size_t, xtc_automation_point *);
|
||||
/* Control-thread snapshot history; 32 edits plus baseline, no assets/DSP/GUI.
|
||||
* record clones before changing history; failure retains all history. checkout
|
||||
* clones adjacent state without moving cursor; accept only after installation.
|
||||
* Allocation ID high-water marks survive checkout/branching.
|
||||
* Reset by creating a new history for New/Open. */
|
||||
typedef struct xtc_history xtc_history;
|
||||
xtc_result xtc_history_create(const xtc_project *, xtc_history **);
|
||||
void xtc_history_destroy(xtc_history *);
|
||||
xtc_result xtc_history_record(xtc_history *, const xtc_project *);
|
||||
xtc_result xtc_history_checkout(const xtc_history *, int redo, xtc_project **);
|
||||
xtc_result xtc_history_accept(xtc_history *, int redo);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
@ -11,7 +11,9 @@ extern "C" {
|
|||
#define XTC_TICKS_PER_QUARTER 960U
|
||||
#define XTC_PROJECT_MAX_TICK UINT64_C(38400000)
|
||||
typedef struct xtc_project xtc_project;
|
||||
typedef struct { uint64_t id; char name[XTC_PROJECT_NAME_BYTES]; } xtc_track_info;
|
||||
typedef struct { uint64_t id; char name[XTC_PROJECT_NAME_BYTES];
|
||||
float gain, pan; int mute, solo;
|
||||
} xtc_track_info;
|
||||
/* Arrangement region: empty asset_path means silent metadata.
|
||||
* All musical positions are integer ticks, constant tempo and 4/4 meter. */
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ xtc_result xtc_realtime_get_status(const xtc_realtime *engine,
|
|||
* frame_count must be <= configured max_frames and output must hold it.
|
||||
* Oversized buffers are silenced too; callers must bound their own frame count.
|
||||
* NULL input means silence. Zero frames is a no-op. */
|
||||
xtc_result xtc_realtime_process_stereo(xtc_realtime *, const float *input,
|
||||
const float *audio_left, const float *audio_right, const float *midi_left, const float *midi_right,
|
||||
float *left, float *right, size_t frames);
|
||||
xtc_result xtc_realtime_process_sources(xtc_realtime *engine, const float *input, const float *timeline,
|
||||
const float *instrument, float *output, size_t frames);
|
||||
xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
C99/POSIX control code loads bounded WAV files and prepares immutable clip
|
||||
plans. Callback rendering reads only preloaded samples. The audio source node
|
||||
passes the mono timeline mix through the ordinary modular graph.
|
||||
passes the mono or paired stereo timeline mix through the ordinary modular graph.
|
||||
|
||||
The recorder uses a lock-free SPSC ring: callback push, control-thread drain,
|
||||
and finish/destroy only after callback quiescence. WAV headers, file writing
|
||||
and final rename occur on the control thread. See docs/AUDIO_FILES.md and
|
||||
include/xtc/xtc_audio.h for ownership, limits and recovery behavior.
|
||||
|
||||
Phase 8 track gain/pan/mute/solo and automation are prepared with each region;
|
||||
see ../../docs/MIXER.md. Raw recording remains mono and bypasses track settings.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
#define _XOPEN_SOURCE 700
|
||||
#include "wave_internal.h"
|
||||
#include "../project/project_internal.h"
|
||||
#include "../mixer/mix_internal.h"
|
||||
#include "../graph/graph_internal.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
typedef struct { uint64_t start, end, offset; size_t asset; } region;
|
||||
typedef struct { uint64_t start, end, offset; size_t asset, track; } region;
|
||||
struct xtc_audio_plan {
|
||||
xtc_mix_snapshot mix;
|
||||
xtc_wave waves[64];
|
||||
char paths[64][XTC_ASSET_PATH_BYTES];
|
||||
region regions[XTC_PROJECT_MAX_CLIPS];
|
||||
|
|
@ -73,6 +74,8 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate,
|
|||
plan = calloc(1, sizeof(*plan));
|
||||
if (!plan) return XTC_ERR_OUT_OF_MEMORY;
|
||||
plan->rate = rate;
|
||||
result = xtc_mix_prepare(project, rate, &plan->mix);
|
||||
if (result != XTC_OK) { free(plan); return result; }
|
||||
for (i = 0; i < project->clip_count; ++i) {
|
||||
const xtc_clip_info *clip = project->clips + i;
|
||||
size_t a; region *r;
|
||||
|
|
@ -92,7 +95,7 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate,
|
|||
if (total > 67108864U) { result = XTC_ERR_CAPACITY; break; }
|
||||
}
|
||||
r = plan->regions + plan->count++;
|
||||
r->asset = a;
|
||||
r->asset = a; r->track = xtc_mix_track(&plan->mix, clip->track_id);
|
||||
result = xtc_project_tick_to_sample(project, clip->start_tick, rate, &r->start);
|
||||
if (result == XTC_OK) result = xtc_project_tick_to_sample(project, clip->start_tick + clip->length_tick, rate, &r->end);
|
||||
if (result == XTC_OK) result = xtc_project_tick_to_sample(project, clip->offset_tick, rate, &r->offset);
|
||||
|
|
@ -106,11 +109,12 @@ size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **path)
|
|||
if (path) *path = plan ? plan->first_issue : "";
|
||||
return plan ? plan->issues : 0;
|
||||
}
|
||||
xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position, float *output, size_t frames)
|
||||
xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t position, float *output, float *right, size_t frames)
|
||||
{
|
||||
size_t i, j;
|
||||
if (!plan || !output || frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!plan || !output || (right && right == output) || frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
memset(output, 0, frames * sizeof(*output));
|
||||
if (right) memset(right, 0, frames * sizeof(*right));
|
||||
for (i = 0; i < plan->count; ++i) {
|
||||
const region *r = plan->regions + i;
|
||||
const xtc_wave *wave = plan->waves + r->asset;
|
||||
|
|
@ -123,10 +127,18 @@ xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position,
|
|||
double fraction;
|
||||
if (source >= (double)wave->frames) continue;
|
||||
index = (size_t)source; fraction = source - (double)index;
|
||||
output[j] += (float)(wave->samples[index] * (1.0 - fraction) +
|
||||
float sample = (float)(wave->samples[index] * (1.0 - fraction) +
|
||||
(index + 1 < wave->frames ? wave->samples[index + 1] : wave->samples[index]) * fraction);
|
||||
output[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, right ? 0U : 2U);
|
||||
if (right) right[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, 1);
|
||||
}
|
||||
}
|
||||
for (j = 0; j < frames; ++j) if (!isfinite(output[j])) output[j] = 0;
|
||||
for (j = 0; j < frames; ++j) {
|
||||
if (!isfinite(output[j])) output[j] = 0;
|
||||
if (right && !isfinite(right[j])) right[j] = 0;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_audio_plan_render(const xtc_audio_plan *p, uint64_t position, float *out, size_t frames)
|
||||
{ return xtc_audio_plan_render_stereo(p, position, out, NULL, frames); }
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@
|
|||
#include <jack/jack.h>
|
||||
#include <jack/midiport.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
|
||||
struct xtc_backend {
|
||||
jack_client_t *client;
|
||||
jack_port_t *input, *output, *midi_input;
|
||||
jack_port_t *input, *output, *midi_input, *output_right;
|
||||
xtc_realtime *engine;
|
||||
xtc_audio_plan *audio;
|
||||
xtc_midi_plan *midi;
|
||||
unsigned int panic_requested, panic_seen, midi_dropped;
|
||||
unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right;
|
||||
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
|
||||
float *timeline, *instrument;
|
||||
size_t max_frames;
|
||||
|
|
@ -56,14 +57,20 @@ static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nfra
|
|||
}
|
||||
++count;
|
||||
}
|
||||
return xtc_midi_plan_render(backend->midi, position, events, count, backend->instrument, (size_t)frames);
|
||||
return xtc_midi_plan_render_stereo(backend->midi, position, events, count, backend->instrument,
|
||||
backend->output_right ? backend->instrument + backend->max_frames : NULL, (size_t)frames);
|
||||
}
|
||||
static int process(jack_nframes_t frames, void *context)
|
||||
{
|
||||
xtc_backend *backend = context;
|
||||
float *output = jack_port_get_buffer(backend->output, frames);
|
||||
float *right = backend->output_right ? jack_port_get_buffer(backend->output_right, frames) : NULL;
|
||||
xtc_result result = XTC_OK;
|
||||
if (output == NULL) {
|
||||
if (output == NULL || (backend->output_right && !right)) {
|
||||
if (output) memset(output, 0, (size_t)frames * sizeof(*output));
|
||||
if (right) memset(right, 0, (size_t)frames * sizeof(*right));
|
||||
__atomic_store_n(&backend->peak_left, 0U, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&backend->peak_right, 0U, __ATOMIC_RELAXED);
|
||||
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
||||
if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U);
|
||||
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
|
||||
|
|
@ -79,12 +86,16 @@ static int process(jack_nframes_t frames, void *context)
|
|||
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
||||
if (backend->recorder) xtc_recorder_push(backend->recorder, input, (size_t)frames);
|
||||
if (backend->audio)
|
||||
result = xtc_audio_plan_render(backend->audio, position, backend->timeline, (size_t)frames);
|
||||
result = xtc_audio_plan_render_stereo(backend->audio, position, backend->timeline,
|
||||
right ? backend->timeline + backend->max_frames : NULL, (size_t)frames);
|
||||
if (result == XTC_OK) result = render_midi(backend, position, frames);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_realtime_process_sources(backend->engine, input,
|
||||
result = xtc_realtime_process_stereo(backend->engine, input,
|
||||
backend->audio ? backend->timeline : NULL,
|
||||
backend->midi ? backend->instrument : NULL, output, (size_t)frames);
|
||||
backend->audio && right ? backend->timeline + backend->max_frames : NULL,
|
||||
backend->midi ? backend->instrument : NULL,
|
||||
backend->midi && right ? backend->instrument + backend->max_frames : NULL,
|
||||
output, right, (size_t)frames);
|
||||
}
|
||||
if (result == XTC_OK) {
|
||||
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
||||
|
|
@ -95,6 +106,17 @@ static int process(jack_nframes_t frames, void *context)
|
|||
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
||||
if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U);
|
||||
memset(output, 0, (size_t)frames * sizeof(*output));
|
||||
if (right) memset(right, 0, (size_t)frames * sizeof(*right));
|
||||
}
|
||||
{
|
||||
size_t i; float left_peak = 0, right_peak = 0;
|
||||
for (i = 0; i < frames; ++i) {
|
||||
float l = fabsf(output[i]), r = right ? fabsf(right[i]) : l;
|
||||
if (l > left_peak) left_peak = l;
|
||||
if (r > right_peak) right_peak = r;
|
||||
}
|
||||
__atomic_store_n(&backend->peak_left, (unsigned int)(fminf(left_peak, 1000) * 1000000), __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&backend->peak_right, (unsigned int)(fminf(right_peak, 1000) * 1000000), __ATOMIC_RELAXED);
|
||||
}
|
||||
__atomic_store_n(&backend->process_result, (unsigned int)result, __ATOMIC_RELAXED);
|
||||
return 0; /* Keep connected and silent on recoverable processing errors. */
|
||||
|
|
@ -141,15 +163,15 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_
|
|||
if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out_backend = NULL;
|
||||
if (config == NULL || config->client_name == NULL || config->client_name[0] == '\0' ||
|
||||
config->max_frames == 0U || config->max_frames > 65536U || config->channel_count != 1U)
|
||||
config->max_frames == 0U || config->max_frames > 65536U || (config->channel_count != 1U && config->channel_count != 2U))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!__atomic_always_lock_free(sizeof(uint64_t), 0)) return XTC_ERR_UNSUPPORTED;
|
||||
if (config->kind != XTC_BACKEND_JACK) return XTC_ERR_UNSUPPORTED;
|
||||
if (strlen(config->client_name) >= (size_t)jack_client_name_size()) return XTC_ERR_INVALID_ARGUMENT;
|
||||
backend = calloc(1U, sizeof(*backend));
|
||||
if (backend == NULL) return XTC_ERR_OUT_OF_MEMORY;
|
||||
backend->timeline = calloc(config->max_frames, sizeof(float));
|
||||
backend->instrument = calloc(config->max_frames, sizeof(float));
|
||||
backend->timeline = calloc(config->max_frames * config->channel_count, sizeof(float));
|
||||
backend->instrument = calloc(config->max_frames * config->channel_count, sizeof(float));
|
||||
if (!backend->timeline || !backend->instrument) {
|
||||
free(backend->timeline); free(backend->instrument); free(backend); return XTC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
|
@ -165,7 +187,9 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_
|
|||
backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U);
|
||||
backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
|
||||
backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U);
|
||||
if (backend->input == NULL || backend->output == NULL || backend->midi_input == NULL) { result = XTC_ERR_IO; goto fail; }
|
||||
if (config->channel_count == 2U)
|
||||
backend->output_right = jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
|
||||
if ((config->channel_count == 2U && !backend->output_right) || backend->input == NULL || backend->output == NULL || backend->midi_input == NULL) { result = XTC_ERR_IO; goto fail; }
|
||||
if (jack_set_port_connect_callback(backend->client, port_connection, backend) != 0 ||
|
||||
jack_set_process_callback(backend->client, process, backend) != 0 ||
|
||||
jack_set_sample_rate_callback(backend->client, rate_callback, backend) != 0 ||
|
||||
|
|
@ -314,6 +338,8 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status
|
|||
if (backend == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
||||
(void)xtc_realtime_get_status(backend->engine, &engine_status);
|
||||
out_status->peak_left = backend->running ? (float)__atomic_load_n(&backend->peak_left, __ATOMIC_RELAXED) / 1000000.0f : 0;
|
||||
out_status->peak_right = backend->running ? (float)__atomic_load_n(&backend->peak_right, __ATOMIC_RELAXED) / 1000000.0f : 0;
|
||||
out_status->midi_dropped = __atomic_load_n(&backend->midi_dropped, __ATOMIC_RELAXED);
|
||||
out_status->asset_issues = (unsigned int)xtc_audio_plan_issues(backend->audio, NULL);
|
||||
out_status->sample_rate = __atomic_load_n(&backend->rate, __ATOMIC_RELAXED);
|
||||
|
|
@ -338,6 +364,14 @@ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction
|
|||
*out_name = jack_port_name(direction == XTC_PORT_INPUT ? backend->input : backend->output);
|
||||
return *out_name == NULL ? XTC_ERR_IO : XTC_OK;
|
||||
}
|
||||
xtc_result xtc_backend_output_name(const xtc_backend *backend, size_t channel, const char **out)
|
||||
{
|
||||
if (!backend || !out || channel > 1) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
||||
if (channel && !backend->output_right) return XTC_ERR_NOT_FOUND;
|
||||
*out = jack_port_name(channel ? backend->output_right : backend->output);
|
||||
return *out ? XTC_OK : XTC_ERR_IO;
|
||||
}
|
||||
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
|
||||
xtc_backend_port_visitor visitor, void *context)
|
||||
{
|
||||
|
|
@ -376,6 +410,8 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc
|
|||
{ return route(backend, direction, remote, 1); }
|
||||
|
||||
#else
|
||||
xtc_result xtc_backend_output_name(const xtc_backend *b, size_t c, const char **out)
|
||||
{ (void)b; (void)c; (void)out; return XTC_ERR_UNSUPPORTED; }
|
||||
xtc_result xtc_backend_panic(xtc_backend *backend)
|
||||
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
|
||||
xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate,
|
|||
plan->offsets = calloc(plan->node_count + 1U, sizeof(*plan->offsets));
|
||||
plan->sources = calloc(graph->edge_count + 1U, sizeof(*plan->sources));
|
||||
plan->render = calloc(storage_nodes, sizeof(*plan->render));
|
||||
plan->buffers = calloc(storage_nodes * max_frames, sizeof(*plan->buffers));
|
||||
plan->buffers = calloc(storage_nodes * max_frames * 2U, sizeof(*plan->buffers));
|
||||
plan->peaks = calloc(storage_nodes, sizeof(*plan->peaks));
|
||||
plan->phases = calloc(storage_nodes, sizeof(*plan->phases));
|
||||
if (plan->nodes == NULL || plan->order == NULL || plan->offsets == NULL ||
|
||||
|
|
@ -109,50 +109,56 @@ xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_
|
|||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_plan_process_sources(xtc_plan *plan, const float *input, const float *timeline, const float *instrument, float *output,
|
||||
size_t frame_count)
|
||||
xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input,
|
||||
const float *timeline, const float *timeline_right, const float *instrument, const float *instrument_right,
|
||||
float *output, float *right, size_t frame_count)
|
||||
{
|
||||
size_t index, frame, edge;
|
||||
if (plan == NULL || output == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
size_t index, frame, edge, channel;
|
||||
if (plan == NULL || output == NULL || (right && right == output)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (frame_count > plan->max_frames) return XTC_ERR_CAPACITY;
|
||||
if (frame_count == 0U) return XTC_OK;
|
||||
/* All external input reads finish before output is written, allowing aliasing.
|
||||
* Nonfinite input/arithmetic becomes silence per sample, never persistent DSP state. */
|
||||
for (index = 0U; index < plan->node_count; ++index) {
|
||||
size_t node = plan->order[index];
|
||||
xtc_node spec = plan->nodes[node];
|
||||
float *buffer = plan->buffers + node * plan->max_frames;
|
||||
float peak = 0.0f;
|
||||
double phase = plan->phases[node];
|
||||
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
|
||||
const float *audio_source = channel ? timeline_right : timeline;
|
||||
const float *midi_source = channel ? instrument_right : instrument;
|
||||
float *destination = channel ? right : output;
|
||||
for (index = 0U; index < plan->node_count; ++index) {
|
||||
size_t node = plan->order[index];
|
||||
xtc_node spec = plan->nodes[node];
|
||||
float *buffer = plan->buffers + (channel * plan->node_count + node) * plan->max_frames;
|
||||
float peak = 0.0f;
|
||||
double phase = plan->phases[node];
|
||||
for (frame = 0U; frame < frame_count; ++frame) {
|
||||
float sample = 0.0f;
|
||||
if (spec.type == XTC_NODE_INSTRUMENT) {
|
||||
sample = midi_source ? midi_source[frame] : 0.0f;
|
||||
} else if (spec.type == XTC_NODE_AUDIO) {
|
||||
sample = audio_source ? audio_source[frame] : 0.0f;
|
||||
} else if (spec.type == XTC_NODE_OSCILLATOR) {
|
||||
sample = (float)sin(XTC_TAU * phase);
|
||||
phase += (double)spec.value / plan->sample_rate;
|
||||
if (phase >= 1.0) phase -= 1.0;
|
||||
} else {
|
||||
if (plan->offsets[node] == plan->offsets[node + 1U])
|
||||
sample = input == NULL ? 0.0f : input[frame] * plan->input_gain;
|
||||
for (edge = plan->offsets[node]; edge < plan->offsets[node + 1U]; ++edge)
|
||||
sample += plan->buffers[(channel * plan->node_count + plan->sources[edge]) * plan->max_frames + frame];
|
||||
if (spec.type == XTC_NODE_GAIN) sample *= spec.value;
|
||||
}
|
||||
if (!isfinite(sample)) sample = 0.0f;
|
||||
buffer[frame] = sample;
|
||||
if (fabsf(sample) > peak) peak = fabsf(sample);
|
||||
}
|
||||
if (!right || channel == 1) plan->phases[node] = phase;
|
||||
if (spec.type == XTC_NODE_METER) plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak;
|
||||
}
|
||||
for (frame = 0U; frame < frame_count; ++frame) {
|
||||
float sample = 0.0f;
|
||||
if (spec.type == XTC_NODE_INSTRUMENT) {
|
||||
sample = instrument ? instrument[frame] : 0.0f;
|
||||
} else if (spec.type == XTC_NODE_AUDIO) {
|
||||
sample = timeline ? timeline[frame] : 0.0f;
|
||||
} else if (spec.type == XTC_NODE_OSCILLATOR) {
|
||||
sample = (float)sin(XTC_TAU * phase);
|
||||
phase += (double)spec.value / plan->sample_rate;
|
||||
if (phase >= 1.0) phase -= 1.0;
|
||||
} else {
|
||||
if (plan->offsets[node] == plan->offsets[node + 1U])
|
||||
sample = input == NULL ? 0.0f : input[frame] * plan->input_gain;
|
||||
for (edge = plan->offsets[node]; edge < plan->offsets[node + 1U]; ++edge)
|
||||
sample += plan->buffers[plan->sources[edge] * plan->max_frames + frame];
|
||||
if (spec.type == XTC_NODE_GAIN) sample *= spec.value;
|
||||
}
|
||||
if (!isfinite(sample)) sample = 0.0f;
|
||||
buffer[frame] = sample;
|
||||
if (fabsf(sample) > peak) peak = fabsf(sample);
|
||||
for (index = 0U; index < plan->node_count; ++index)
|
||||
if (plan->render[index]) sample += plan->buffers[(channel * plan->node_count + index) * plan->max_frames + frame];
|
||||
destination[frame] = isfinite(sample) ? sample : 0.0f;
|
||||
}
|
||||
plan->phases[node] = phase;
|
||||
if (spec.type == XTC_NODE_METER) plan->peaks[node] = peak;
|
||||
}
|
||||
for (frame = 0U; frame < frame_count; ++frame) {
|
||||
float sample = 0.0f;
|
||||
for (index = 0U; index < plan->node_count; ++index)
|
||||
if (plan->render[index]) sample += plan->buffers[index * plan->max_frames + frame];
|
||||
output[frame] = isfinite(sample) ? sample : 0.0f;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -177,3 +183,6 @@ xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output, s
|
|||
|
||||
xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output, size_t frames)
|
||||
{ return xtc_plan_process_sources(plan, input, timeline, NULL, output, frames); }
|
||||
|
||||
xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *audio, const float *midi, float *out, size_t frames)
|
||||
{ return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); }
|
||||
|
|
|
|||
|
|
@ -80,11 +80,12 @@ xtc_result xtc_realtime_get_status(const xtc_realtime *engine,
|
|||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_realtime_process_sources(xtc_realtime *engine, const float *input, const float *timeline, const float *instrument,
|
||||
float *output, size_t frame_count)
|
||||
xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input,
|
||||
const float *timeline, const float *timeline_right, const float *instrument, const float *instrument_right,
|
||||
float *output, float *right, size_t frame_count)
|
||||
{
|
||||
xtc_result result = XTC_OK;
|
||||
if (engine == NULL || output == NULL || frame_count > SIZE_MAX / sizeof(*output))
|
||||
if (engine == NULL || output == NULL || (right && right == output) || frame_count > SIZE_MAX / sizeof(*output))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (frame_count == 0U) return XTC_OK;
|
||||
if (frame_count > engine->max_frames) {
|
||||
|
|
@ -107,11 +108,16 @@ xtc_result xtc_realtime_process_sources(xtc_realtime *engine, const float *input
|
|||
}
|
||||
}
|
||||
if (engine->active != NULL)
|
||||
result = xtc_plan_process_sources(engine->active, input, timeline, instrument, output, frame_count);
|
||||
else
|
||||
result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument, instrument_right, output, right, frame_count);
|
||||
else {
|
||||
memset(output, 0, frame_count * sizeof(*output));
|
||||
if (right) memset(right, 0, frame_count * sizeof(*right));
|
||||
}
|
||||
}
|
||||
if (result != XTC_OK) {
|
||||
memset(output, 0, frame_count * sizeof(*output));
|
||||
if (right) memset(right, 0, frame_count * sizeof(*right));
|
||||
}
|
||||
if (result != XTC_OK) memset(output, 0, frame_count * sizeof(*output));
|
||||
__atomic_store_n(&engine->result, (unsigned int)result, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&engine->blocks,
|
||||
__atomic_load_n(&engine->blocks, __ATOMIC_RELAXED) + 1U, __ATOMIC_RELAXED);
|
||||
|
|
@ -123,3 +129,6 @@ xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, float
|
|||
|
||||
xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, float *output, size_t frames)
|
||||
{ return xtc_realtime_process_sources(engine, input, timeline, NULL, output, frames); }
|
||||
|
||||
xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const float *audio, const float *midi, float *out, size_t frames)
|
||||
{ return xtc_realtime_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "graph_widget.hpp"
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "piano_roll.hpp"
|
||||
#include "mixer_widget.hpp"
|
||||
#include <FL/Fl_Native_File_Chooser.H>
|
||||
#include "xtc/xtc_version.h"
|
||||
|
||||
|
|
@ -33,8 +34,15 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
xtc_result result = xtc_project_create(&initial);
|
||||
if (result != XTC_OK) throw std::runtime_error(xtc_result_string(result));
|
||||
project_.reset(initial);
|
||||
xtc_history *history = nullptr;
|
||||
result = xtc_history_create(initial, &history);
|
||||
if (result != XTC_OK) throw std::runtime_error(xtc_result_string(result));
|
||||
history_.reset(history);
|
||||
begin();
|
||||
auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
|
||||
menu->add("Edit/Undo", FL_CTRL + 'z', history_callback, this);
|
||||
menu->add("Edit/Redo", FL_CTRL + FL_SHIFT + 'z', history_callback, this);
|
||||
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
|
||||
menu->add("File/New", FL_CTRL + 'n', file_callback, this);
|
||||
menu->add("File/Open", FL_CTRL + 'o', file_callback, this);
|
||||
menu->add("File/Save", FL_CTRL + 's', file_callback, this);
|
||||
|
|
@ -102,7 +110,9 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
sequencer->end();
|
||||
auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer");
|
||||
mixer->begin();
|
||||
new Fl_Box(24, 164, 940, 40, "Mixer placeholder — channels and meters are not implemented yet.");
|
||||
mixer_ = new MixerWidget(20, 150, 956, 420, project_.get(), before_arrangement_edit, this);
|
||||
mixer_->callback([](Fl_Widget *, void *c) { static_cast<MainWindow *>(c)->mark_dirty(); }, this);
|
||||
mixer->resizable(mixer_);
|
||||
mixer->end();
|
||||
auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph");
|
||||
graph->begin();
|
||||
|
|
@ -216,7 +226,7 @@ xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, siz
|
|||
|
||||
void MainWindow::open_backend()
|
||||
{
|
||||
xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 1U};
|
||||
xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 2U};
|
||||
xtc_result result = xtc_backend_open(&config, &backend_);
|
||||
ready_ = result == XTC_OK;
|
||||
running_ = false;
|
||||
|
|
@ -280,6 +290,7 @@ void MainWindow::reconnect_engine()
|
|||
|
||||
void MainWindow::refresh_status()
|
||||
{
|
||||
if (mixer_) mixer_->peaks(0, 0);
|
||||
bool can_play = false, can_record = false;
|
||||
if (!ready_) {
|
||||
set_label(indicator_, "Audio: disconnected");
|
||||
|
|
@ -310,8 +321,9 @@ void MainWindow::refresh_status()
|
|||
set_label(indicator_, state);
|
||||
indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) :
|
||||
running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215));
|
||||
mixer_->peaks(status.peak_left, status.peak_right);
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | mono | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops",
|
||||
std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops",
|
||||
status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns,
|
||||
xtc_result_string(status.process_result), status.asset_issues, status.midi_dropped);
|
||||
set_label(status_, text);
|
||||
|
|
@ -372,7 +384,7 @@ void MainWindow::view_callback(Fl_Widget *widget, void *view)
|
|||
}
|
||||
void MainWindow::about_callback(Fl_Widget *, void *)
|
||||
{
|
||||
fl_message("%s %s\nModular audio workstation\nPhase 7: MIDI input, sine instrument, persisted notes and piano roll.",
|
||||
fl_message("%s %s\nModular audio workstation\nPhase 8: stereo track mixer, automation, meters and undo/redo.",
|
||||
XTC_PROJECT_NAME, XTC_VERSION_STRING);
|
||||
}
|
||||
void MainWindow::timer_callback(void *context)
|
||||
|
|
@ -392,6 +404,9 @@ void MainWindow::timer_callback(void *context)
|
|||
|
||||
void MainWindow::mark_dirty()
|
||||
{
|
||||
xtc_result history_result = xtc_history_record(history_.get(), project_.get());
|
||||
if (history_result != XTC_OK) report_error("Undo history could not retain edit", history_result);
|
||||
mixer_->set_project(project_.get());
|
||||
graph_widget_->set_graph(xtc_project_graph(project_.get()));
|
||||
if (piano_) piano_->set_project(project_.get(), piano_->clip());
|
||||
dirty_ = true; copy_label("xtc * — unsaved changes");
|
||||
|
|
@ -420,9 +435,16 @@ xtc_result MainWindow::save_project(const char *path)
|
|||
if (r == XTC_OK) { path_ = saved_path; dirty_ = false; copy_label(path_.c_str()); }
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::install_project(xtc_project *candidate)
|
||||
xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_history)
|
||||
{
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
std::unique_ptr<xtc_history, decltype(&xtc_history_destroy)> new_history(nullptr, xtc_history_destroy);
|
||||
if (reset_history) {
|
||||
xtc_history *h = nullptr;
|
||||
xtc_result result = xtc_history_create(candidate, &h);
|
||||
if (result != XTC_OK) return result;
|
||||
new_history.reset(h);
|
||||
}
|
||||
xtc_result r = XTC_OK;
|
||||
if (ready_) {
|
||||
r = xtc_backend_stop(backend_);
|
||||
|
|
@ -444,6 +466,8 @@ xtc_result MainWindow::install_project(xtc_project *candidate)
|
|||
if (piano_window_) piano_window_->hide();
|
||||
if (piano_) piano_->set_project(candidate, UINT64_MAX);
|
||||
project_.reset(candidate);
|
||||
mixer_->set_project(candidate);
|
||||
if (reset_history) history_.swap(new_history);
|
||||
graph_widget_->reset_layout();
|
||||
graph_widget_->set_graph(xtc_project_graph(candidate));
|
||||
sequencer_->set_project(candidate);
|
||||
|
|
@ -634,3 +658,23 @@ void MainWindow::panic_callback(Fl_Widget *, void *context)
|
|||
xtc_result r = xtc_backend_panic(window->backend_);
|
||||
if (r != XTC_OK) window->report_error("Panic failed", r);
|
||||
}
|
||||
|
||||
xtc_result MainWindow::undo(bool redo)
|
||||
{
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
xtc_project *candidate = nullptr;
|
||||
xtc_result r = xtc_history_checkout(history_.get(), redo ? 1 : 0, &candidate);
|
||||
if (r == XTC_OK) r = install_project(candidate, false);
|
||||
if (r != XTC_OK) { xtc_project_destroy(candidate); return r; }
|
||||
r = xtc_history_accept(history_.get(), redo ? 1 : 0);
|
||||
dirty_ = true; copy_label("xtc * — unsaved changes");
|
||||
return r;
|
||||
}
|
||||
void MainWindow::history_callback(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *window = static_cast<MainWindow *>(context);
|
||||
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
|
||||
bool redo = std::strncmp(menu->mvalue()->label(), "Redo", 4) == 0;
|
||||
xtc_result r = window->undo(redo);
|
||||
if (r != XTC_OK && r != XTC_ERR_NOT_FOUND) window->report_error("Undo/redo failed", r);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
#include <string>
|
||||
#include "xtc/xtc_project.h"
|
||||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
|
||||
class GraphWidget;
|
||||
class MixerWidget;
|
||||
class PianoRollWidget;
|
||||
class SequencerWidget;
|
||||
class Fl_Box;
|
||||
|
|
@ -23,6 +25,8 @@ public:
|
|||
using Fl_Double_Window::show;
|
||||
int handle(int event) override;
|
||||
|
||||
xtc_result undo(bool redo = false);
|
||||
MixerWidget *mixer() const { return mixer_; }
|
||||
xtc_result open_piano_roll();
|
||||
PianoRollWidget *piano_roll() const { return piano_; }
|
||||
xtc_result import_audio(const char *path);
|
||||
|
|
@ -30,6 +34,9 @@ public:
|
|||
xtc_result save_project(const char *path);
|
||||
xtc_result open_project(const char *path);
|
||||
private:
|
||||
MixerWidget *mixer_ = nullptr;
|
||||
std::unique_ptr<xtc_history, decltype(&xtc_history_destroy)> history_{nullptr, xtc_history_destroy};
|
||||
static void history_callback(Fl_Widget *, void *);
|
||||
std::unique_ptr<xtc_project, decltype(&xtc_project_destroy)> project_{nullptr, xtc_project_destroy};
|
||||
std::unique_ptr<Fl_Double_Window> piano_window_;
|
||||
PianoRollWidget *piano_ = nullptr;
|
||||
|
|
@ -46,7 +53,7 @@ private:
|
|||
std::string path_;
|
||||
bool discard_changes();
|
||||
void mark_dirty();
|
||||
xtc_result install_project(xtc_project *candidate);
|
||||
xtc_result install_project(xtc_project *candidate, bool reset_history = true);
|
||||
static xtc_result seek(void *context, uint64_t tick);
|
||||
static void file_callback(Fl_Widget *, void *context);
|
||||
GraphWidget *graph_widget_ = nullptr;
|
||||
|
|
|
|||
314
src/gui/mixer_widget.cpp
Normal file
314
src/gui/mixer_widget.cpp
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
#include "mixer_widget.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Check_Button.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Progress.H>
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace
|
||||
{
|
||||
constexpr uint64_t lane_ticks = 15360;
|
||||
}
|
||||
class AutomationLane : public Fl_Widget
|
||||
{
|
||||
MixerWidget &mixer_;
|
||||
|
||||
public:
|
||||
AutomationLane(int x, int y, int w, int h, MixerWidget &m) : Fl_Widget(x, y, w, h), mixer_(m) {}
|
||||
void draw() override
|
||||
{
|
||||
fl_push_clip(x(), y(), w(), h());
|
||||
fl_color(fl_rgb_color(32, 35, 40));
|
||||
fl_rectf(x(), y(), w(), h());
|
||||
fl_font(FL_HELVETICA, 11);
|
||||
for (int i = 0; i <= 16; ++i) {
|
||||
int px = x() + i * (w() - 1) / 16;
|
||||
fl_color(fl_rgb_color(i % 4 ? 55 : 95, 60, 65));
|
||||
fl_line(px, y(), px, y() + h());
|
||||
if (i % 4 == 0) {
|
||||
char label[32];
|
||||
std::snprintf(label, sizeof(label), "%llu",
|
||||
static_cast<unsigned long long>(mixer_.lane_start() +
|
||||
static_cast<uint64_t>(i) * 960));
|
||||
fl_color(FL_WHITE);
|
||||
fl_draw(label, px + 3, y() + 14);
|
||||
}
|
||||
}
|
||||
size_t count = 0;
|
||||
xtc_project_automation_count(mixer_.project(), &count);
|
||||
std::vector<xtc_automation_point> points;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_automation_point a{};
|
||||
xtc_project_get_automation(mixer_.project(), i, &a);
|
||||
if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter())
|
||||
points.push_back(a);
|
||||
}
|
||||
std::sort(points.begin(), points.end(),
|
||||
[](const auto &a, const auto &b) { return a.tick < b.tick; });
|
||||
float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f;
|
||||
float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f;
|
||||
auto py = [&](float v) {
|
||||
return y() + h() - 6 - static_cast<int>((v - min) / range * (h() - 28));
|
||||
};
|
||||
auto value = [&](uint64_t tick) {
|
||||
if (points.empty())
|
||||
return min;
|
||||
auto it = std::upper_bound(points.begin(), points.end(), tick,
|
||||
[](uint64_t t, const auto &a) { return t < a.tick; });
|
||||
if (it == points.begin())
|
||||
return it->value;
|
||||
if (it == points.end())
|
||||
return points.back().value;
|
||||
const auto &a = *(it - 1);
|
||||
return a.value + static_cast<float>(static_cast<double>(tick - a.tick) /
|
||||
static_cast<double>(it->tick - a.tick)) *
|
||||
(it->value - a.value);
|
||||
};
|
||||
fl_color(fl_rgb_color(105, 200, 150));
|
||||
if (!points.empty())
|
||||
for (int px = 1; px < w(); ++px) {
|
||||
uint64_t a = mixer_.lane_start() + static_cast<uint64_t>(px - 1) * lane_ticks /
|
||||
static_cast<uint64_t>(w() - 1);
|
||||
uint64_t b = mixer_.lane_start() + static_cast<uint64_t>(px) * lane_ticks /
|
||||
static_cast<uint64_t>(w() - 1);
|
||||
fl_line(x() + px - 1, py(value(a)), x() + px, py(value(b)));
|
||||
}
|
||||
for (const auto &a : points)
|
||||
if (a.tick >= mixer_.lane_start() && a.tick <= mixer_.lane_start() + lane_ticks) {
|
||||
int px = x() + static_cast<int>((a.tick - mixer_.lane_start()) *
|
||||
static_cast<uint64_t>(w() - 1) / lane_ticks);
|
||||
fl_rectf(px - 4, py(a.value) - 4, 8, 8);
|
||||
}
|
||||
if (points.empty()) {
|
||||
fl_color(FL_WHITE);
|
||||
fl_draw("Click to add automation; right-click a point to remove. Values replace the "
|
||||
"static control.",
|
||||
x() + 12, y() + h() / 2);
|
||||
}
|
||||
fl_pop_clip();
|
||||
}
|
||||
int handle(int event) override
|
||||
{
|
||||
if (event != FL_PUSH)
|
||||
return Fl_Widget::handle(event);
|
||||
if (Fl::event_button() != FL_LEFT_MOUSE && Fl::event_button() != FL_RIGHT_MOUSE)
|
||||
return 0;
|
||||
uint64_t tick = mixer_.lane_start() +
|
||||
static_cast<uint64_t>(std::clamp(Fl::event_x() - x(), 0, w() - 1)) *
|
||||
lane_ticks / static_cast<uint64_t>(w() - 1);
|
||||
tick = (tick + 120) / 240 * 240;
|
||||
tick = std::min(tick, XTC_PROJECT_MAX_TICK);
|
||||
float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f;
|
||||
float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f;
|
||||
float value =
|
||||
min + static_cast<float>(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) /
|
||||
static_cast<float>(h() - 28) * range;
|
||||
if (Fl::event_button() == FL_RIGHT_MOUSE) {
|
||||
size_t count = 0;
|
||||
xtc_project_automation_count(mixer_.project(), &count);
|
||||
uint64_t distance =
|
||||
static_cast<uint64_t>(8) * lane_ticks / static_cast<uint64_t>(w() - 1) + 1,
|
||||
closest = UINT64_MAX;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_automation_point a{};
|
||||
xtc_project_get_automation(mixer_.project(), i, &a);
|
||||
uint64_t d = a.tick > tick ? a.tick - tick : tick - a.tick;
|
||||
if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() &&
|
||||
d < distance) {
|
||||
distance = d;
|
||||
closest = a.tick;
|
||||
}
|
||||
}
|
||||
if (closest != UINT64_MAX)
|
||||
mixer_.point(closest, 0, true);
|
||||
} else
|
||||
mixer_.point(tick, value);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before,
|
||||
void *context)
|
||||
: Fl_Group(x, y, w, h), project_(p), before_(before), context_(context)
|
||||
{
|
||||
begin();
|
||||
tracks_ = new Fl_Choice(x + 70, y + 8, 240, 26, "Track");
|
||||
tracks_->callback(select, this);
|
||||
gain_ = new Fl_Value_Slider(x + 70, y + 48, 320, 28, "Gain");
|
||||
gain_->type(FL_HOR_NICE_SLIDER);
|
||||
gain_->bounds(0, 4);
|
||||
gain_->step(0.01);
|
||||
gain_->when(FL_WHEN_RELEASE);
|
||||
gain_->callback(control, this);
|
||||
pan_ = new Fl_Value_Slider(x + 480, y + 48, 320, 28, "Pan");
|
||||
pan_->type(FL_HOR_NICE_SLIDER);
|
||||
pan_->bounds(-1, 1);
|
||||
pan_->step(0.01);
|
||||
pan_->when(FL_WHEN_RELEASE);
|
||||
pan_->callback(control, this);
|
||||
mute_ = new Fl_Check_Button(x + 340, y + 8, 90, 26, "Mute");
|
||||
solo_ = new Fl_Check_Button(x + 440, y + 8, 90, 26, "Solo");
|
||||
mute_->callback(control, this);
|
||||
solo_->callback(control, this);
|
||||
left_ = new Fl_Progress(x + 70, y + 90, 320, 20, "Output L");
|
||||
right_ = new Fl_Progress(x + 480, y + 90, 320, 20, "Output R");
|
||||
for (auto *meter : {left_, right_}) {
|
||||
meter->minimum(0);
|
||||
meter->maximum(1);
|
||||
meter->selection_color(fl_rgb_color(80, 180, 120));
|
||||
}
|
||||
parameter_ = new Fl_Choice(x + 110, y + 126, 150, 26, "Automation");
|
||||
parameter_->add("Gain|Pan");
|
||||
parameter_->value(0);
|
||||
parameter_->callback(select, this);
|
||||
start_ = new Fl_Value_Input(x + 420, y + 126, 140, 26, "First tick");
|
||||
start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks);
|
||||
start_->step(3840);
|
||||
start_->callback(select, this);
|
||||
auto *hint = new Fl_Box(x + 10, y + 160, w - 20, 24,
|
||||
"Gain 0..4; pan -1 left / +1 right. M/S toggles; arrows change gain, "
|
||||
"Shift+arrows pan; Up/Down selects track.");
|
||||
hint->labelsize(12);
|
||||
lane_ = new AutomationLane(x + 10, y + 192, w - 20, h - 222, *this);
|
||||
end();
|
||||
set_project(p);
|
||||
}
|
||||
uint64_t MixerWidget::selected_track() const
|
||||
{
|
||||
xtc_track_info t{};
|
||||
return xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &t) == XTC_OK
|
||||
? t.id
|
||||
: UINT64_MAX;
|
||||
}
|
||||
xtc_automation_parameter MixerWidget::parameter() const
|
||||
{
|
||||
return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN;
|
||||
}
|
||||
uint64_t MixerWidget::lane_start() const
|
||||
{
|
||||
if (!std::isfinite(start_->value())) return 0;
|
||||
return static_cast<uint64_t>(
|
||||
std::clamp(start_->value(), 0.0, static_cast<double>(XTC_PROJECT_MAX_TICK - lane_ticks)));
|
||||
}
|
||||
void MixerWidget::set_project(xtc_project *p)
|
||||
{
|
||||
uint64_t selected = project_ == p ? selected_track() : UINT64_MAX;
|
||||
project_ = p;
|
||||
tracks_->clear();
|
||||
size_t n = 0, c = 0;
|
||||
xtc_project_get_counts(p, &n, &c);
|
||||
int index = 0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
xtc_track_info t{};
|
||||
xtc_project_get_track(p, i, &t);
|
||||
std::string label;
|
||||
for (char ch : std::string(t.name)) {
|
||||
if (ch == '/' || ch == '\\' || ch == '_' || ch == '&')
|
||||
label += '\\';
|
||||
label += ch;
|
||||
}
|
||||
tracks_->add(label.c_str());
|
||||
if (t.id == selected)
|
||||
index = static_cast<int>(i);
|
||||
}
|
||||
tracks_->value(n ? index : -1);
|
||||
selection();
|
||||
redraw();
|
||||
}
|
||||
void MixerWidget::selection()
|
||||
{
|
||||
xtc_track_info t{};
|
||||
bool valid =
|
||||
xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &t) == XTC_OK;
|
||||
for (Fl_Widget *w : {static_cast<Fl_Widget *>(gain_), static_cast<Fl_Widget *>(pan_),
|
||||
static_cast<Fl_Widget *>(mute_), static_cast<Fl_Widget *>(solo_),
|
||||
static_cast<Fl_Widget *>(lane_)}) {
|
||||
if (valid)
|
||||
w->activate();
|
||||
else
|
||||
w->deactivate();
|
||||
}
|
||||
gain_->value(valid ? t.gain : 1);
|
||||
pan_->value(t.pan);
|
||||
mute_->value(t.mute);
|
||||
solo_->value(t.solo);
|
||||
lane_->redraw();
|
||||
}
|
||||
void MixerWidget::peaks(float left, float right)
|
||||
{
|
||||
left_->value(std::min(left, 1.0f));
|
||||
right_->value(std::min(right, 1.0f));
|
||||
left_->selection_color(left > 1 ? FL_RED : FL_GREEN);
|
||||
right_->selection_color(right > 1 ? FL_RED : FL_GREEN);
|
||||
}
|
||||
xtc_result MixerWidget::changed(xtc_result r)
|
||||
{
|
||||
if (r == XTC_OK)
|
||||
do_callback();
|
||||
else
|
||||
fl_alert("Mixer edit failed: %s", xtc_result_string(r));
|
||||
selection();
|
||||
return r;
|
||||
}
|
||||
xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo)
|
||||
{
|
||||
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||||
if (r == XTC_OK)
|
||||
r = xtc_project_set_mix(project_, selected_track(), gain, pan, mute, solo);
|
||||
return changed(r);
|
||||
}
|
||||
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};
|
||||
if (r == XTC_OK)
|
||||
r = remove ? xtc_project_remove_automation(project_, a.track_id, a.parameter, tick)
|
||||
: xtc_project_set_automation(project_, &a);
|
||||
return changed(r);
|
||||
}
|
||||
void MixerWidget::control(Fl_Widget *, void *c)
|
||||
{
|
||||
auto *m = static_cast<MixerWidget *>(c);
|
||||
m->mix(static_cast<float>(m->gain_->value()), static_cast<float>(m->pan_->value()),
|
||||
m->mute_->value(), m->solo_->value());
|
||||
}
|
||||
void MixerWidget::select(Fl_Widget *, void *c)
|
||||
{
|
||||
static_cast<MixerWidget *>(c)->selection();
|
||||
}
|
||||
int MixerWidget::handle(int e)
|
||||
{
|
||||
if (e == FL_KEYDOWN && !(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META))) {
|
||||
int key = Fl::event_key();
|
||||
xtc_track_info t{};
|
||||
if (xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &t) == XTC_OK) {
|
||||
if (key == 'm' || key == 's') {
|
||||
mix(t.gain, t.pan, key == 'm' ? !t.mute : t.mute, key == 's' ? !t.solo : t.solo);
|
||||
return 1;
|
||||
}
|
||||
if (key == FL_Left || key == FL_Right) {
|
||||
float d = key == FL_Left ? -0.05f : 0.05f;
|
||||
bool pan = (Fl::event_state() & FL_SHIFT) != 0;
|
||||
mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f),
|
||||
pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, t.mute, t.solo);
|
||||
return 1;
|
||||
}
|
||||
if (key == FL_Up || key == FL_Down) {
|
||||
size_t n = 0, c = 0;
|
||||
xtc_project_get_counts(project_, &n, &c);
|
||||
tracks_->value(std::clamp(tracks_->value() + (key == FL_Up ? -1 : 1), 0,
|
||||
static_cast<int>(n) - 1));
|
||||
selection();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Fl_Group::handle(e);
|
||||
}
|
||||
44
src/gui/mixer_widget.hpp
Normal file
44
src/gui/mixer_widget.hpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#ifndef XTC_MIXER_WIDGET_HPP
|
||||
#define XTC_MIXER_WIDGET_HPP
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <FL/Fl_Group.H>
|
||||
class Fl_Choice;
|
||||
class Fl_Value_Slider;
|
||||
class Fl_Check_Button;
|
||||
class Fl_Progress;
|
||||
class Fl_Value_Input;
|
||||
class AutomationLane;
|
||||
class MixerWidget : public Fl_Group
|
||||
{
|
||||
public:
|
||||
using BeforeEdit = xtc_result (*)(void *);
|
||||
MixerWidget(int, int, int, int, xtc_project *, BeforeEdit, void *);
|
||||
void set_project(xtc_project *);
|
||||
void peaks(float left, float right);
|
||||
uint64_t selected_track() const;
|
||||
xtc_project *project() const
|
||||
{
|
||||
return project_;
|
||||
}
|
||||
xtc_automation_parameter parameter() const;
|
||||
uint64_t lane_start() const;
|
||||
xtc_result point(uint64_t tick, float value, bool remove = false);
|
||||
xtc_result mix(float gain, float pan, int mute, int solo);
|
||||
int handle(int) override;
|
||||
|
||||
private:
|
||||
xtc_project *project_;
|
||||
BeforeEdit before_;
|
||||
void *context_;
|
||||
Fl_Choice *tracks_, *parameter_;
|
||||
Fl_Value_Slider *gain_, *pan_;
|
||||
Fl_Check_Button *mute_, *solo_;
|
||||
Fl_Progress *left_, *right_;
|
||||
Fl_Value_Input *start_;
|
||||
AutomationLane *lane_;
|
||||
xtc_result changed(xtc_result);
|
||||
void selection();
|
||||
static void control(Fl_Widget *, void *);
|
||||
static void select(Fl_Widget *, void *);
|
||||
};
|
||||
#endif
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
#include "xtc/xtc_midi.h"
|
||||
#include "../project/project_internal.h"
|
||||
#include "../mixer/mix_internal.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define TAU 6.28318530717958647692
|
||||
typedef struct { uint64_t start, end, age; unsigned int pitch, velocity, channel; } span;
|
||||
typedef struct { uint64_t start, end, age; unsigned int pitch, velocity, channel; size_t track; } span;
|
||||
typedef struct { uint64_t time; int change; } endpoint;
|
||||
typedef struct { unsigned int active, channel, pitch, velocity; uint64_t age; double phase; } voice;
|
||||
struct xtc_midi_plan {
|
||||
xtc_mix_snapshot mix;
|
||||
span notes[XTC_PROJECT_MAX_NOTES];
|
||||
size_t count;
|
||||
voice voices[XTC_MIDI_VOICES];
|
||||
|
|
@ -51,6 +52,10 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
|
|||
if (!project || !rate || rate > 768000) return XTC_ERR_INVALID_ARGUMENT;
|
||||
p = calloc(1, sizeof(*p)); points = calloc(XTC_PROJECT_MAX_NOTES * 2U, sizeof(*points));
|
||||
if (!p || !points) { free(p); free(points); return XTC_ERR_OUT_OF_MEMORY; }
|
||||
{
|
||||
xtc_result r = xtc_mix_prepare(project, rate, &p->mix);
|
||||
if (r != XTC_OK) { free(points); free(p); return r; }
|
||||
}
|
||||
for (i = 0; i < 128; ++i) {
|
||||
double frequency = 440.0 * pow(2.0, ((double)i - 69.0) / 12.0);
|
||||
p->step[i] = frequency < rate * 0.5 ? frequency / rate : 0;
|
||||
|
|
@ -71,6 +76,7 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
|
|||
if (r == XTC_OK) r = xtc_project_tick_to_sample(project, begin - n->start_tick, rate, &s->age);
|
||||
if (r != XTC_OK) { free(points); free(p); return r; }
|
||||
if (s->end <= s->start) continue;
|
||||
s->track = xtc_mix_track(&p->mix, c.track_id);
|
||||
s->pitch = n->pitch; s->velocity = n->velocity; s->channel = n->channel;
|
||||
points[p->count * 2].time = s->start; points[p->count * 2].change = 1;
|
||||
points[p->count * 2 + 1].time = s->end; points[p->count * 2 + 1].change = -1;
|
||||
|
|
@ -122,7 +128,7 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
|
|||
p->voices[slot].pitch = e->pitch; p->voices[slot].velocity = e->velocity;
|
||||
p->voices[slot].phase = 0; p->voices[slot].age = p->age++;
|
||||
}
|
||||
static void segment(xtc_midi_plan *p, uint64_t position, float *out, size_t frames)
|
||||
static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *right, size_t frames)
|
||||
{
|
||||
size_t i, j;
|
||||
for (i = 0; i < p->count; ++i) {
|
||||
|
|
@ -130,27 +136,30 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, size_t fram
|
|||
uint64_t begin = position > s->start ? position : s->start;
|
||||
uint64_t end = position + frames < s->end ? position + frames : s->end;
|
||||
double step = p->step[s->pitch];
|
||||
if (!step || s->start < p->cutoff[s->channel] || end <= begin) continue;
|
||||
if (step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin) continue;
|
||||
for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) {
|
||||
double phase = fmod((double)(position + j - s->start + s->age) * step, 1.0);
|
||||
out[j] += (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0);
|
||||
float sample = (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0);
|
||||
out[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, right ? 0U : 2U);
|
||||
if (right) right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
|
||||
voice *v = p->voices + i;
|
||||
double step = p->step[v->pitch];
|
||||
if (!v->active || !step) continue;
|
||||
if (!v->active || step == 0.0) continue;
|
||||
for (j = 0; j < frames; ++j) {
|
||||
out[j] += (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0);
|
||||
float sample = (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0);
|
||||
out[j] += sample; if (right) right[j] += sample;
|
||||
v->phase += step; if (v->phase >= 1) v->phase -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position,
|
||||
const xtc_midi_event *events, size_t count, float *out, size_t frames)
|
||||
xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position,
|
||||
const xtc_midi_event *events, size_t count, float *out, float *right, size_t frames)
|
||||
{
|
||||
size_t i, cursor = 0;
|
||||
if (!p || !out || count > XTC_MIDI_MAX_EVENTS || (count && !events) ||
|
||||
if (!p || !out || (right && right == out) || count > XTC_MIDI_MAX_EVENTS || (count && !events) ||
|
||||
frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < count; ++i) {
|
||||
const xtc_midi_event *e = events + i;
|
||||
|
|
@ -162,12 +171,16 @@ xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position,
|
|||
if (!frames) return XTC_OK;
|
||||
if (p->next_position != position) xtc_midi_plan_reset(p);
|
||||
memset(out, 0, frames * sizeof(*out));
|
||||
if (right) memset(right, 0, frames * sizeof(*right));
|
||||
for (i = 0; i < count; ++i) {
|
||||
size_t next = events[i].offset;
|
||||
segment(p, position + cursor, out + cursor, next - cursor);
|
||||
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, next - cursor);
|
||||
event(p, events + i, position + next); cursor = next;
|
||||
}
|
||||
segment(p, position + cursor, out + cursor, frames - cursor);
|
||||
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor);
|
||||
p->next_position = position + frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events, size_t count, float *out, size_t frames)
|
||||
{ return xtc_midi_plan_render_stereo(p, position, events, count, out, NULL, frames); }
|
||||
|
|
|
|||
6
src/mixer/README.md
Normal file
6
src/mixer/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Mixer core
|
||||
|
||||
`model.c` validates track settings and stable track-relative automation references.
|
||||
`render.c` prepares sorted sample endpoints and evaluates linear gain/pan lanes
|
||||
without allocation. `mix_internal.h` is private snapshot storage for audio/MIDI
|
||||
source plans. See ../../docs/MIXER.md for the stereo pan law and ownership.
|
||||
20
src/mixer/mix_internal.h
Normal file
20
src/mixer/mix_internal.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef XTC_MIX_INTERNAL_H
|
||||
#define XTC_MIX_INTERNAL_H
|
||||
#include "../project/project_internal.h"
|
||||
typedef struct {
|
||||
uint64_t sample, tick;
|
||||
size_t track;
|
||||
unsigned int parameter;
|
||||
float value;
|
||||
} xtc_mix_point;
|
||||
typedef struct {
|
||||
xtc_track_info tracks[XTC_PROJECT_MAX_TRACKS];
|
||||
size_t count, offsets[XTC_PROJECT_MAX_TRACKS][2], lengths[XTC_PROJECT_MAX_TRACKS][2];
|
||||
xtc_mix_point points[XTC_MAX_AUTOMATION_POINTS];
|
||||
int any_solo;
|
||||
} xtc_mix_snapshot;
|
||||
xtc_result xtc_mix_prepare(const xtc_project *, unsigned int rate, xtc_mix_snapshot *);
|
||||
size_t xtc_mix_track(const xtc_mix_snapshot *, uint64_t id);
|
||||
/* side 0/1 stereo, 2 mono (gain only). */
|
||||
float xtc_mix_level(const xtc_mix_snapshot *, size_t track, uint64_t sample, unsigned int side);
|
||||
#endif
|
||||
81
src/mixer/model.c
Normal file
81
src/mixer/model.c
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#include "../project/project_internal.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
static size_t find_track(const xtc_project *p, uint64_t id)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < p->track_count; ++i)
|
||||
if (p->tracks[i].id == id)
|
||||
return i;
|
||||
return SIZE_MAX;
|
||||
}
|
||||
xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pan, int mute,
|
||||
int solo)
|
||||
{
|
||||
size_t i;
|
||||
if (!p || !isfinite(gain) || gain < 0 || gain > 4 || !isfinite(pan) || pan < -1 || pan > 1 ||
|
||||
(mute != 0 && mute != 1) || (solo != 0 && solo != 1))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = find_track(p, id);
|
||||
if (i == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
p->tracks[i].gain = gain;
|
||||
p->tracks[i].pan = pan;
|
||||
p->tracks[i].mute = mute;
|
||||
p->tracks[i].solo = solo;
|
||||
return XTC_OK;
|
||||
}
|
||||
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 ||
|
||||
(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))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (find_track(p, a->track_id) == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
for (i = 0; i < p->automation_count; ++i)
|
||||
if (p->automation[i].track_id == a->track_id &&
|
||||
p->automation[i].parameter == a->parameter && p->automation[i].tick == a->tick)
|
||||
break;
|
||||
if (i == XTC_MAX_AUTOMATION_POINTS)
|
||||
return XTC_ERR_CAPACITY;
|
||||
if (i == p->automation_count)
|
||||
++p->automation_count;
|
||||
p->automation[i] = *a;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_remove_automation(xtc_project *p, uint64_t id,
|
||||
xtc_automation_parameter param, uint64_t tick)
|
||||
{
|
||||
size_t i;
|
||||
if (!p || (param != XTC_AUTOMATION_GAIN && param != XTC_AUTOMATION_PAN) ||
|
||||
tick > XTC_PROJECT_MAX_TICK)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < p->automation_count; ++i)
|
||||
if (p->automation[i].track_id == id && p->automation[i].parameter == param &&
|
||||
p->automation[i].tick == tick)
|
||||
break;
|
||||
if (i == p->automation_count)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
memmove(p->automation + i, p->automation + i + 1,
|
||||
(--p->automation_count - i) * sizeof(*p->automation));
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_automation_count(const xtc_project *p, size_t *n)
|
||||
{
|
||||
if (!p || !n)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*n = p->automation_count;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_get_automation(const xtc_project *p, size_t i, xtc_automation_point *a)
|
||||
{
|
||||
if (!p || !a)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (i >= p->automation_count)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
*a = p->automation[i];
|
||||
return XTC_OK;
|
||||
}
|
||||
87
src/mixer/render.c
Normal file
87
src/mixer/render.c
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#include "mix_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
size_t xtc_mix_track(const xtc_mix_snapshot *m, uint64_t id)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < m->count; ++i)
|
||||
if (m->tracks[i].id == id)
|
||||
return i;
|
||||
return SIZE_MAX;
|
||||
}
|
||||
static int compare(const void *left, const void *right)
|
||||
{
|
||||
const xtc_mix_point *a = left, *b = right;
|
||||
if (a->track != b->track)
|
||||
return a->track < b->track ? -1 : 1;
|
||||
if (a->parameter != b->parameter)
|
||||
return a->parameter < b->parameter ? -1 : 1;
|
||||
return a->tick < b->tick ? -1 : a->tick != b->tick;
|
||||
}
|
||||
xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snapshot *m)
|
||||
{
|
||||
size_t i;
|
||||
xtc_result r;
|
||||
memset(m, 0, sizeof(*m));
|
||||
m->count = p->track_count;
|
||||
memcpy(m->tracks, p->tracks, p->track_count * sizeof(*m->tracks));
|
||||
for (i = 0; i < m->count; ++i)
|
||||
if (m->tracks[i].solo)
|
||||
m->any_solo = 1;
|
||||
for (i = 0; i < p->automation_count; ++i) {
|
||||
const xtc_automation_point *a = p->automation + i;
|
||||
xtc_mix_point *b = m->points + i;
|
||||
b->track = xtc_mix_track(m, a->track_id);
|
||||
b->parameter = (unsigned int)a->parameter;
|
||||
if (b->track == SIZE_MAX || b->parameter > 1)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
b->tick = a->tick;
|
||||
b->value = a->value;
|
||||
r = xtc_project_tick_to_sample(p, a->tick, rate, &b->sample);
|
||||
if (r != XTC_OK)
|
||||
return r;
|
||||
}
|
||||
qsort(m->points, p->automation_count, sizeof(*m->points), compare);
|
||||
for (i = 0; i < p->automation_count; ++i) {
|
||||
xtc_mix_point *a = m->points + i;
|
||||
size_t *n = &m->lengths[a->track][a->parameter];
|
||||
if (!*n)
|
||||
m->offsets[a->track][a->parameter] = i;
|
||||
++*n;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param, uint64_t sample)
|
||||
{
|
||||
size_t low = 0, high = m->lengths[track][param], count = high;
|
||||
const xtc_mix_point *p = m->points + m->offsets[track][param];
|
||||
if (!count)
|
||||
return param ? m->tracks[track].pan : m->tracks[track].gain;
|
||||
while (low < high) {
|
||||
size_t mid = low + (high - low) / 2;
|
||||
if (p[mid].sample <= sample)
|
||||
low = mid + 1;
|
||||
else
|
||||
high = mid;
|
||||
}
|
||||
if (!low)
|
||||
return p[0].value;
|
||||
if (low == count)
|
||||
return p[count - 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));
|
||||
}
|
||||
float xtc_mix_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side)
|
||||
{
|
||||
float gain, pan;
|
||||
if (track >= m->count)
|
||||
return 0;
|
||||
if (m->tracks[track].mute || (m->any_solo && !m->tracks[track].solo))
|
||||
return 0;
|
||||
gain = value(m, track, 0, sample);
|
||||
if (side == 2)
|
||||
return gain;
|
||||
pan = value(m, track, 1, sample);
|
||||
return gain * (side == 0 ? (pan > 0 ? 1 - pan : 1) : (pan < 0 ? 1 + pan : 1));
|
||||
}
|
||||
|
|
@ -4,3 +4,7 @@ project_file.c implements bounded version-2 project metadata loading (including
|
|||
temporary-file/fsync/rename saving in C. Loading validates a candidate before
|
||||
replacing the existing model. All I/O is control-thread only.
|
||||
See docs/PROJECT_FORMAT.md for the schema, limits and durability guarantees.
|
||||
|
||||
Phase 8 writes format v4 with per-track MIX records and bounded AUTOMATION/POINT
|
||||
records, reading v1–v3 with unity/center/no-automation defaults. History stays
|
||||
session-only. See ../../docs/PROJECT_FORMAT.md.
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ static xtc_result read_project(reader *input, xtc_project *project)
|
|||
size_t i, j;
|
||||
xtc_graph *graph = project->graph;
|
||||
READ("XTC_PROJECT", 2U); NUM(1U, version);
|
||||
if (version < 1U || version > 3U) return XTC_ERR_UNSUPPORTED;
|
||||
if (version < 1U || version > 4U) return XTC_ERR_UNSUPPORTED;
|
||||
format_version = version;
|
||||
READ("APP", 4U);
|
||||
for (i = 1U; i < 4U; ++i) { NUM(i, value); if (value > UINT_MAX) return XTC_ERR_FORMAT; }
|
||||
|
|
@ -124,6 +124,14 @@ static xtc_result read_project(reader *input, xtc_project *project)
|
|||
for (i = 0U; i < project->track_count; ++i) {
|
||||
READ("TRACK", 3U); NUM(1U, project->tracks[i].id);
|
||||
if (!decode_name(input->tokens[2], project->tracks[i].name)) return XTC_ERR_FORMAT;
|
||||
project->tracks[i].gain = 1;
|
||||
if (format_version >= 4U) {
|
||||
READ("MIX", 5U);
|
||||
if (!decode_float(input->tokens[1], &project->tracks[i].gain) ||
|
||||
!decode_float(input->tokens[2], &project->tracks[i].pan)) return XTC_ERR_FORMAT;
|
||||
NUM(3U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].mute = (int)value;
|
||||
NUM(4U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].solo = (int)value;
|
||||
}
|
||||
}
|
||||
READ("CLIPS", 2U); NUM(1U, value);
|
||||
if (value > XTC_PROJECT_MAX_CLIPS) return XTC_ERR_FORMAT;
|
||||
|
|
@ -167,6 +175,19 @@ static xtc_result read_project(reader *input, xtc_project *project)
|
|||
NUM(7U, value); if (value > 15) return XTC_ERR_FORMAT; n->channel = (unsigned int)value;
|
||||
}
|
||||
}
|
||||
if (format_version >= 4U) {
|
||||
READ("AUTOMATION", 2U); NUM(1U, value);
|
||||
if (value > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT;
|
||||
project->automation_count = (size_t)value;
|
||||
for (i = 0; i < project->automation_count; ++i) {
|
||||
xtc_automation_point *a = project->automation + i;
|
||||
READ("POINT", 5U); NUM(1U, a->track_id); NUM(2U, value);
|
||||
if (value > 1) return XTC_ERR_FORMAT;
|
||||
a->parameter = (xtc_automation_parameter)value;
|
||||
NUM(3U, a->tick);
|
||||
if (!decode_float(input->tokens[4], &a->value)) return XTC_ERR_FORMAT;
|
||||
}
|
||||
}
|
||||
READ("END", 1U);
|
||||
if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT;
|
||||
if (ferror(input->file)) return XTC_ERR_IO;
|
||||
|
|
@ -183,7 +204,7 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
|
|||
const xtc_graph *graph = project->graph;
|
||||
size_t i;
|
||||
if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED;
|
||||
if (fprintf(file, "XTC_PROJECT 3\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
||||
if (fprintf(file, "XTC_PROJECT 4\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
||||
XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo,
|
||||
project->next_track_id, project->next_clip_id, graph->require_output_nodes,
|
||||
graph->next_id, graph->node_count, graph->edge_count) < 0) return XTC_ERR_IO;
|
||||
|
|
@ -197,8 +218,13 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
|
|||
if (fprintf(file, "EDGE %zu %zu\n", graph->nodes[graph->edges[i].source].id,
|
||||
graph->nodes[graph->edges[i].destination].id) < 0) return XTC_ERR_IO;
|
||||
if (fprintf(file, "TRACKS %zu\n", project->track_count) < 0) return XTC_ERR_IO;
|
||||
for (i = 0U; i < project->track_count; ++i)
|
||||
if (fprintf(file, "TRACK %" PRIu64 " ", project->tracks[i].id) < 0 || !write_name(file, project->tracks[i].name)) return XTC_ERR_IO;
|
||||
for (i = 0U; i < project->track_count; ++i) {
|
||||
uint32_t gain, pan;
|
||||
const xtc_track_info *t = project->tracks + i;
|
||||
memcpy(&gain, &t->gain, sizeof(gain)); memcpy(&pan, &t->pan, sizeof(pan));
|
||||
if (fprintf(file, "TRACK %" PRIu64 " ", t->id) < 0 || !write_name(file, t->name) ||
|
||||
fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\n", gain, pan, t->mute, t->solo) < 0) return XTC_ERR_IO;
|
||||
}
|
||||
if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0) return XTC_ERR_IO;
|
||||
for (i = 0U; i < project->clip_count; ++i) {
|
||||
const xtc_clip_info *clip = project->clips + i;
|
||||
|
|
@ -217,6 +243,13 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
|
|||
if (fprintf(file, "NOTE %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %u %u %u\n",
|
||||
n->id, n->clip_id, n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel) < 0) return XTC_ERR_IO;
|
||||
}
|
||||
if (fprintf(file, "AUTOMATION %zu\n", project->automation_count) < 0) return XTC_ERR_IO;
|
||||
for (i = 0; i < project->automation_count; ++i) {
|
||||
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) return XTC_ERR_IO;
|
||||
}
|
||||
return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
|
||||
}
|
||||
static size_t path_length(const char *path)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,8 @@ project.c owns the opaque C project: graph, bounded tracks, silent/audio arrange
|
|||
regions and external asset references, tempo and stable ID allocation. The GUI borrows handles and edits through
|
||||
xtc_project.h. Tick/sample conversion and source-preserving trim validation live
|
||||
here; no FLTK, file I/O or callback scheduling belongs in this module.
|
||||
|
||||
Phase 8 adds track mix settings, bounded automation and 32-edit C snapshot
|
||||
history in history.c. Checkout/accept separates candidate installation from
|
||||
history cursor movement; allocation IDs stay monotonic across history branches.
|
||||
See ../../docs/MIXER.md.
|
||||
|
|
|
|||
111
src/project/history.c
Normal file
111
src/project/history.c
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#include "../graph/graph_internal.h"
|
||||
#include "project_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
struct xtc_history {
|
||||
xtc_project *states[XTC_HISTORY_EDITS + 1];
|
||||
size_t count, cursor;
|
||||
uint64_t next_track, next_clip, next_note;
|
||||
size_t next_node;
|
||||
};
|
||||
static void remember_ids(xtc_history *h, const xtc_project *p)
|
||||
{
|
||||
if (p->next_track_id > h->next_track)
|
||||
h->next_track = p->next_track_id;
|
||||
if (p->next_clip_id > h->next_clip)
|
||||
h->next_clip = p->next_clip_id;
|
||||
if (p->next_note_id > h->next_note)
|
||||
h->next_note = p->next_note_id;
|
||||
if (p->graph->next_id > h->next_node)
|
||||
h->next_node = p->graph->next_id;
|
||||
}
|
||||
void xtc_history_destroy(xtc_history *h)
|
||||
{
|
||||
size_t i;
|
||||
if (!h)
|
||||
return;
|
||||
for (i = 0; i < h->count; ++i)
|
||||
xtc_project_destroy(h->states[i]);
|
||||
free(h);
|
||||
}
|
||||
xtc_result xtc_history_create(const xtc_project *p, xtc_history **out)
|
||||
{
|
||||
xtc_history *h;
|
||||
xtc_result r;
|
||||
if (!out)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out = NULL;
|
||||
if (!p)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
h = calloc(1, sizeof(*h));
|
||||
if (!h)
|
||||
return XTC_ERR_OUT_OF_MEMORY;
|
||||
r = xtc_project_clone(p, h->states);
|
||||
if (r != XTC_OK) {
|
||||
free(h);
|
||||
return r;
|
||||
}
|
||||
remember_ids(h, p);
|
||||
h->count = 1;
|
||||
*out = h;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_history_record(xtc_history *h, const xtc_project *p)
|
||||
{
|
||||
xtc_project *copy;
|
||||
size_t i;
|
||||
xtc_result r;
|
||||
if (!h || !p)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = xtc_project_clone(p, ©);
|
||||
if (r != XTC_OK)
|
||||
return r;
|
||||
remember_ids(h, p);
|
||||
for (i = h->cursor + 1; i < h->count; ++i)
|
||||
xtc_project_destroy(h->states[i]);
|
||||
h->count = h->cursor + 1;
|
||||
if (h->count == XTC_HISTORY_EDITS + 1) {
|
||||
xtc_project_destroy(h->states[0]);
|
||||
memmove(h->states, h->states + 1, XTC_HISTORY_EDITS * sizeof(*h->states));
|
||||
--h->count;
|
||||
}
|
||||
h->states[h->count++] = copy;
|
||||
h->cursor = h->count - 1;
|
||||
return XTC_OK;
|
||||
}
|
||||
static int available(const xtc_history *h, int redo)
|
||||
{
|
||||
return redo ? h->cursor + 1 < h->count : h->cursor > 0;
|
||||
}
|
||||
xtc_result xtc_history_checkout(const xtc_history *h, int redo, xtc_project **out)
|
||||
{
|
||||
if (!out)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out = NULL;
|
||||
if (!h || (redo != 0 && redo != 1))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!available(h, redo))
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
{
|
||||
xtc_result r = xtc_project_clone(h->states[redo ? h->cursor + 1 : h->cursor - 1], out);
|
||||
if (r == XTC_OK) {
|
||||
(*out)->next_track_id = h->next_track;
|
||||
(*out)->next_clip_id = h->next_clip;
|
||||
(*out)->next_note_id = h->next_note;
|
||||
(*out)->graph->next_id = h->next_node;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
xtc_result xtc_history_accept(xtc_history *h, int redo)
|
||||
{
|
||||
if (!h || (redo != 0 && redo != 1))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!available(h, redo))
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (redo)
|
||||
++h->cursor;
|
||||
else
|
||||
--h->cursor;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
#include "xtc/xtc_audio.h"
|
||||
#include "../graph/graph_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
int xtc_project_valid_name(const char *name)
|
||||
|
|
@ -127,7 +128,7 @@ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_
|
|||
xtc_track_info track = {0};
|
||||
if (project == NULL || id == NULL || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX) return XTC_ERR_CAPACITY;
|
||||
track.id = project->next_track_id++;
|
||||
track.id = project->next_track_id++; track.gain = 1.0f;
|
||||
memcpy(track.name, name, strlen(name) + 1U);
|
||||
project->tracks[project->track_count++] = track; *id = track.id; return XTC_OK;
|
||||
}
|
||||
|
|
@ -146,7 +147,11 @@ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id)
|
|||
count = 0;
|
||||
for (i = 0; i < project->note_count; ++i)
|
||||
if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX) project->notes[count++] = project->notes[i];
|
||||
project->note_count = count; return XTC_OK;
|
||||
project->note_count = count;
|
||||
count = 0;
|
||||
for (i = 0; i < project->automation_count; ++i)
|
||||
if (project->automation[i].track_id != id) project->automation[count++] = project->automation[i];
|
||||
project->automation_count = count; return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name,
|
||||
uint64_t start, uint64_t length, uint64_t *id)
|
||||
|
|
@ -246,6 +251,21 @@ xtc_result xtc_project_validate(const xtc_project *project)
|
|||
n->length_tick > project->clips[c].source_length_tick - n->start_tick) return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j) if (project->notes[j].id == n->id) return XTC_ERR_FORMAT;
|
||||
}
|
||||
if (project->automation_count > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT;
|
||||
for (i = 0; i < project->track_count; ++i) {
|
||||
const xtc_track_info *t = project->tracks + i;
|
||||
if (!isfinite(t->gain) || t->gain < 0 || t->gain > 4 || !isfinite(t->pan) || t->pan < -1 || t->pan > 1 ||
|
||||
(t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1)) return XTC_ERR_FORMAT;
|
||||
}
|
||||
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) || (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)) return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j) if (project->automation[j].track_id == a->track_id &&
|
||||
project->automation[j].parameter == a->parameter && project->automation[j].tick == a->tick) return XTC_ERR_FORMAT;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define XTC_PROJECT_INTERNAL_H
|
||||
#include "xtc/xtc_project.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
struct xtc_project {
|
||||
xtc_graph *graph;
|
||||
uint64_t next_track_id, next_clip_id;
|
||||
|
|
@ -9,6 +10,8 @@ struct xtc_project {
|
|||
size_t track_count, clip_count;
|
||||
uint64_t next_note_id;
|
||||
size_t note_count;
|
||||
size_t automation_count;
|
||||
xtc_automation_point automation[XTC_MAX_AUTOMATION_POINTS];
|
||||
xtc_note_info notes[XTC_PROJECT_MAX_NOTES];
|
||||
xtc_track_info tracks[XTC_PROJECT_MAX_TRACKS];
|
||||
xtc_clip_info clips[XTC_PROJECT_MAX_CLIPS];
|
||||
|
|
|
|||
|
|
@ -135,3 +135,24 @@ draw/move/resize notes, save/reopen and render them. The live MIDI target connec
|
|||
only software clients and checks on/off, panic, disconnect, and saved-note
|
||||
playback/seek. It needs an existing JACK-compatible server. No hardware routes
|
||||
or system changes are made. MIDI recording/SMF and physical devices are deferred.
|
||||
|
||||
## Phase 8 verification
|
||||
|
||||
```sh
|
||||
make all test gui debug test-gui test-gui-jack test-jack-audio test-jack-midi
|
||||
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
|
||||
```
|
||||
|
||||
`test_mixer` covers stereo gain/pan, mute/solo, sample automation, block
|
||||
independence, WAV and graph routing, v4 persistence failures and bounded history.
|
||||
GUI tests add automation mouse edits, mixer keys, history rollback/branching and
|
||||
save/reopen. The existing live MIDI test now uses two software capture ports and
|
||||
checks hard-right saved-note playback and output meter snapshots; mono audio
|
||||
record/play acceptance remains covered. A native Xvfb mouse smoke check confirms
|
||||
sliders and automation drawing. See MIXER.md for capacities and deferred features.
|
||||
|
||||
The fresh Phase 8 verification build, no-JACK core/GUI suite and Clang
|
||||
ASan/UBSan core/GUI suite passed without compiler warnings. GUI leak checking
|
||||
retains only the existing fontconfig suppression. ThreadSanitizer also passed
|
||||
the recorder concurrency and realtime publication tests. Physical devices and
|
||||
long-running stereo performance remain unverified.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "../../src/gui/graph_widget.hpp"
|
||||
#include "../../src/gui/sequencer_widget.hpp"
|
||||
#include "../../src/gui/piano_roll.hpp"
|
||||
#include "../../src/gui/mixer_widget.hpp"
|
||||
#include <unistd.h>
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
|
|
@ -89,7 +90,7 @@ void reset()
|
|||
extern "C" {
|
||||
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
|
||||
{
|
||||
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 1U);
|
||||
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 2U);
|
||||
++opens;
|
||||
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
|
||||
if (*out != nullptr) { ++alive; handle.running = 0; CHECK(handle.plan == nullptr); engine_status.active_revision = engine_status.submitted_revision = 0U; }
|
||||
|
|
@ -328,6 +329,41 @@ int main(int argc, char **argv)
|
|||
press(find(&window, "Panic"));
|
||||
CHECK(xtc_midi_plan_render(handle.midi, 18032, nullptr, 0, instrument, 32) == XTC_OK && instrument[0] == 0);
|
||||
engine_status.sample_position = 0;
|
||||
// Mixer edits and automation are undoable; failed backend install retains cursor.
|
||||
auto *mixer_ui = window.mixer(); CHECK(mixer_ui != nullptr);
|
||||
xtc_track_info mixed{};
|
||||
CHECK(mixer_ui->mix(0.5f, -1, 0, 1) == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1 && mixed.solo);
|
||||
CHECK(mixer_ui->point(0, 0) == XTC_OK && mixer_ui->point(960, 1) == XTC_OK);
|
||||
size_t points = 0;
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
|
||||
submit_result = XTC_ERR_CAPACITY;
|
||||
CHECK(window.undo(true) == XTC_ERR_CAPACITY);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
|
||||
submit_result = XTC_OK;
|
||||
CHECK(window.undo(true) == XTC_OK);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
||||
CHECK(window.undo() == XTC_OK && mixer_ui->point(1920, 0.25f) == XTC_OK && window.undo(true) == XTC_ERR_NOT_FOUND);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_OK && window.open_project(filename.c_str()) == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
||||
CHECK(window.undo() == XTC_ERR_NOT_FOUND); // Open resets history.
|
||||
auto *lane = mixer_ui->child(mixer_ui->children() - 1);
|
||||
saved_x = Fl::e_x; saved_y = Fl::e_y; saved_key = Fl::e_keysym;
|
||||
Fl::e_x = lane->x() + (lane->w() - 1) / 4; Fl::e_y = lane->y() + lane->h() / 2;
|
||||
Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
|
||||
CHECK(lane->handle(FL_PUSH) == 1);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 3);
|
||||
Fl::e_keysym = FL_Button + FL_RIGHT_MOUSE;
|
||||
CHECK(lane->handle(FL_PUSH) == 1);
|
||||
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
||||
Fl::e_keysym = 'm'; CHECK(mixer_ui->handle(FL_KEYDOWN) == 1);
|
||||
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.mute);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && !mixed.mute);
|
||||
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
|
||||
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
|
||||
engine_status.xruns = 3U;
|
||||
engine_status.processed_blocks = 123U;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ int main(int argc, char **argv)
|
|||
auto *status = find(window, "JACK |");
|
||||
CHECK(status != nullptr);
|
||||
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
||||
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | mono | %u blocks", &rate, &frames, &blocks) == 3);
|
||||
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | stereo | %u blocks", &rate, &frames, &blocks) == 3);
|
||||
CHECK(rate > 0U && frames > 0U && blocks > 0U);
|
||||
stop->do_callback();
|
||||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <jack/jack.h>
|
||||
#include <jack/midiport.h>
|
||||
#include <errno.h>
|
||||
|
|
@ -9,17 +10,19 @@
|
|||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
typedef struct {
|
||||
jack_port_t *input, *midi;
|
||||
unsigned int command, seen, loud, heard, bad;
|
||||
jack_port_t *input, *right, *midi;
|
||||
unsigned int command, seen, loud, right_loud, heard, bad;
|
||||
} probe;
|
||||
static int process(jack_nframes_t frames, void *context)
|
||||
{
|
||||
probe *p = context;
|
||||
const float *input = jack_port_get_buffer(p->input, frames);
|
||||
const float *right = jack_port_get_buffer(p->right, frames);
|
||||
void *midi = jack_port_get_buffer(p->midi, frames);
|
||||
unsigned int right_loud = 0;
|
||||
unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0;
|
||||
jack_nframes_t i;
|
||||
if (!input || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
|
||||
if (!input || !right || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
|
||||
jack_midi_clear_buffer(midi);
|
||||
if (command && command != p->seen) {
|
||||
unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0};
|
||||
|
|
@ -30,8 +33,11 @@ static int process(jack_nframes_t frames, void *context)
|
|||
for (i = 0; i < frames; ++i) {
|
||||
if (!isfinite(input[i]) || fabsf(input[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
||||
if (fabsf(input[i]) > 0.01f) loud = 1;
|
||||
if (!isfinite(right[i]) || fabsf(right[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
||||
if (fabsf(right[i]) > 0.01f) right_loud = 1;
|
||||
}
|
||||
__atomic_store_n(&p->loud, loud, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&p->right_loud, right_loud, __ATOMIC_RELAXED);
|
||||
if (loud) __atomic_store_n(&p->heard, 1U, __ATOMIC_RELAXED);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -41,7 +47,7 @@ static void wait_blocks(void)
|
|||
int main(void)
|
||||
{
|
||||
char directory[] = "/tmp/xtc-live-midi-XXXXXX", song[256] = "", name[64];
|
||||
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 1};
|
||||
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 2};
|
||||
xtc_backend *backend = NULL;
|
||||
xtc_project *p = NULL, *q = NULL;
|
||||
jack_client_t *client = NULL;
|
||||
|
|
@ -60,8 +66,9 @@ int main(void)
|
|||
REQUIRE(xtc_backend_prepare_project(backend, p) == XTC_OK);
|
||||
client = jack_client_open("xtc-midi-probe", JackNoStartServer, &flags); REQUIRE(client);
|
||||
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||
state.right = jack_port_register(client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||
state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
|
||||
REQUIRE(state.input && state.midi);
|
||||
REQUIRE(state.input && state.right && state.midi);
|
||||
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
|
||||
REQUIRE(xtc_backend_start(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
|
||||
|
|
@ -72,6 +79,10 @@ int main(void)
|
|||
REQUIRE(connected == 0 || connected == EEXIST);
|
||||
}
|
||||
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
||||
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
||||
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
||||
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
||||
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
||||
__atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks();
|
||||
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
||||
__atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks();
|
||||
|
|
@ -86,17 +97,23 @@ int main(void)
|
|||
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
||||
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
|
||||
REQUIRE(xtc_project_add_note(p, clip, 0, 960, 69, 100, 0, ¬e) == XTC_OK);
|
||||
REQUIRE(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
|
||||
REQUIRE(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
|
||||
REQUIRE(xtc_backend_prepare_project(backend, q) == XTC_OK);
|
||||
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
||||
wait_blocks(); REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
||||
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
||||
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
||||
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED) && __atomic_load_n(&state.right_loud, __ATOMIC_RELAXED));
|
||||
{ xtc_backend_status status; REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0); }
|
||||
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_seek(backend, 7680000) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
||||
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
||||
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
||||
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
||||
REQUIRE(__atomic_load_n(&state.heard, __ATOMIC_RELAXED) && !__atomic_load_n(&state.bad, __ATOMIC_RELAXED));
|
||||
puts("Live JACK MIDI input/note-off/panic and saved/reopened scheduled instrument playback passed.");
|
||||
puts("Live JACK MIDI/panic/disconnect and saved stereo gain/pan playback/meters passed.");
|
||||
result = EXIT_SUCCESS;
|
||||
cleanup:
|
||||
if (backend && xtc_backend_close(backend) != XTC_OK) return EXIT_FAILURE;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* Processing is driven synchronously here; realtime concurrency is tested
|
||||
* separately by test_realtime. No fake header replaces installed declarations. */
|
||||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <jack/jack.h>
|
||||
#include <jack/midiport.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -18,8 +19,8 @@ static JackBufferSizeCallback size_cb;
|
|||
static JackXRunCallback xrun_cb;
|
||||
static JackShutdownCallback shutdown_cb;
|
||||
static void *context;
|
||||
static float input[128], output[128];
|
||||
static int client_token, input_token, output_token, midi_token;
|
||||
static float input[128], output[128], output_right[128];
|
||||
static int client_token, input_token, output_token, right_token, midi_token;
|
||||
static unsigned int midi_count;
|
||||
static jack_midi_event_t midi_events[256];
|
||||
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
|
||||
|
|
@ -44,6 +45,7 @@ jack_port_t *jack_port_register(jack_client_t *client, const char *name, const c
|
|||
(void)client; (void)name; (void)type; (void)buffer_size;
|
||||
++port_calls;
|
||||
if (register_fail == port_calls) return NULL;
|
||||
if (!strcmp(name, "output_right")) return (jack_port_t *)&right_token;
|
||||
if (!strcmp(type, JACK_DEFAULT_MIDI_TYPE)) return (jack_port_t *)&midi_token;
|
||||
return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token);
|
||||
}
|
||||
|
|
@ -66,12 +68,12 @@ int jack_activate(jack_client_t *client)
|
|||
int jack_deactivate(jack_client_t *client)
|
||||
{ (void)client; if (deactivate_fail) return -1; active = 0; return 0; }
|
||||
void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames)
|
||||
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; return port == (jack_port_t *)&input_token ? input : output; }
|
||||
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; }
|
||||
uint32_t jack_midi_get_event_count(void *buffer) { (void)buffer; return midi_count; }
|
||||
int jack_midi_event_get(jack_midi_event_t *event, void *buffer, uint32_t index)
|
||||
{ (void)buffer; if (index >= midi_count || index >= 256) return -1; *event = midi_events[index]; return 0; }
|
||||
const char *jack_port_name(const jack_port_t *port)
|
||||
{ if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
|
||||
{ if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
|
||||
const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags)
|
||||
{
|
||||
const char **list = malloc(2U * sizeof(*list));
|
||||
|
|
@ -97,7 +99,7 @@ int main(void)
|
|||
const char *name;
|
||||
int visits = 0, iteration;
|
||||
CHECK(xtc_backend_open(NULL, &backend) == XTC_ERR_INVALID_ARGUMENT && backend == NULL);
|
||||
config.channel_count = 2U;
|
||||
config.channel_count = 3U;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_INVALID_ARGUMENT);
|
||||
config.channel_count = 1U;
|
||||
config.kind = (xtc_backend_kind)999;
|
||||
|
|
@ -297,6 +299,25 @@ int main(void)
|
|||
CHECK(xtc_backend_close(backend) == XTC_OK);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
{
|
||||
xtc_project *p = NULL; uint64_t track, clip, note;
|
||||
config.channel_count = 2;
|
||||
register_fail = 4;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
|
||||
register_fail = 0;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
|
||||
CHECK(xtc_backend_output_name(backend, 1, &name) == XTC_OK && strcmp(name, "test:output_right") == 0);
|
||||
CHECK(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "Right", &track) == XTC_OK);
|
||||
CHECK(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
|
||||
CHECK(xtc_project_add_note(p, clip, 0, 960, 69, 127, 0, ¬e) == XTC_OK);
|
||||
CHECK(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
|
||||
CHECK(xtc_backend_prepare_project(backend, p) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(process_cb(32, context) == 0 && output[1] == 0 && output_right[1] > 0);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_right == 0);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK); xtc_project_destroy(p);
|
||||
}
|
||||
CHECK(closed >= 104);
|
||||
CHECK(xtc_backend_close(NULL) == XTC_OK);
|
||||
xtc_graph_destroy(invalid_graph);
|
||||
|
|
|
|||
254
tests/unit/test_mixer.c
Normal file
254
tests/unit/test_mixer.c
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
static void close_to(float a, float b)
|
||||
{
|
||||
assert(fabsf(a - b) < 0.000001f);
|
||||
}
|
||||
static void render(xtc_project *p, uint64_t position, float *l, float *r, size_t n)
|
||||
{
|
||||
xtc_midi_plan *plan = NULL;
|
||||
assert(xtc_midi_plan_create(p, 48000, &plan) == XTC_OK);
|
||||
assert(xtc_midi_plan_render_stereo(plan, position, NULL, 0, l, r, n) == XTC_OK);
|
||||
xtc_midi_plan_destroy(plan);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
xtc_project *p = NULL, *q = NULL, *candidate = NULL;
|
||||
xtc_history *history = NULL;
|
||||
uint64_t a, b, clip, note;
|
||||
float l[512], r[512], base[512], split[512], split_r[512];
|
||||
xtc_automation_point point;
|
||||
xtc_track_info track;
|
||||
xtc_midi_plan *plan = NULL;
|
||||
xtc_graph *g = NULL;
|
||||
xtc_plan *graph = NULL;
|
||||
xtc_realtime *engine = NULL;
|
||||
size_t source, gain, out, i, count;
|
||||
char dir[] = "/tmp/xtc-mix-XXXXXX", path[256], copy[256], wave[256];
|
||||
FILE *f;
|
||||
char bytes[8192] = {0}, roundtrip[8192];
|
||||
size_t length;
|
||||
assert(mkdtemp(dir));
|
||||
snprintf(path, sizeof(path), "%s/song.xtc", dir);
|
||||
snprintf(copy, sizeof(copy), "%s/copy.xtc", dir);
|
||||
snprintf(wave, sizeof(wave), "%s/take.wav", dir);
|
||||
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "One", &a) == XTC_OK &&
|
||||
xtc_project_add_track(p, "Two", &b) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p, a, 0, 3840, &clip) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 0, 3840, 69, 127, 0, ¬e) == XTC_OK);
|
||||
render(p, 0, base, r, 512);
|
||||
assert(memcmp(base, r, sizeof(base)) == 0);
|
||||
assert(xtc_project_set_mix(p, a, 0.5f, -1, 0, 0) == XTC_OK);
|
||||
render(p, 0, l, r, 512);
|
||||
for (i = 0; i < 512; ++i) {
|
||||
close_to(l[i], base[i] * 0.5f);
|
||||
assert(r[i] == 0);
|
||||
}
|
||||
assert(xtc_project_set_mix(p, a, 1, 1, 0, 0) == XTC_OK);
|
||||
render(p, 0, l, r, 512);
|
||||
assert(memcmp(base, r, sizeof(base)) == 0);
|
||||
for (i = 0; i < 512; ++i)
|
||||
assert(l[i] == 0);
|
||||
assert(xtc_project_set_mix(p, b, 1, 0, 0, 1) == XTC_OK);
|
||||
render(p, 0, l, r, 512);
|
||||
for (i = 0; i < 512; ++i)
|
||||
assert(l[i] == 0 && r[i] == 0);
|
||||
assert(xtc_project_set_mix(p, a, 1, 0, 1, 1) == XTC_OK);
|
||||
render(p, 0, l, r, 512);
|
||||
for (i = 0; i < 512; ++i)
|
||||
assert(l[i] == 0);
|
||||
assert(xtc_project_set_mix(p, a, 1, 0, 0, 1) == XTC_OK);
|
||||
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};
|
||||
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};
|
||||
assert(xtc_project_set_automation(p, &point) == XTC_OK);
|
||||
point.tick = 20;
|
||||
point.value = 1;
|
||||
assert(xtc_project_set_automation(p, &point) == XTC_OK);
|
||||
render(p, 0, l, r, 512);
|
||||
for (i = 0; i < 512; ++i) {
|
||||
float t = i < 500 ? (float)i / 500 : 1, pan = -1 + 2 * t;
|
||||
close_to(l[i], base[i] * t * (pan > 0 ? 1 - pan : 1));
|
||||
close_to(r[i], base[i] * t * (pan < 0 ? 1 + pan : 1));
|
||||
}
|
||||
assert(xtc_midi_plan_create(p, 48000, &plan) == XTC_OK);
|
||||
for (i = 0; i < 512; i += 64)
|
||||
assert(xtc_midi_plan_render_stereo(plan, i, NULL, 0, split + i, split_r + i, 64) == XTC_OK);
|
||||
assert(memcmp(l, split, sizeof(l)) == 0 && memcmp(r, split_r, sizeof(r)) == 0);
|
||||
xtc_midi_plan_destroy(plan);
|
||||
/* Real graph routing preserves stereo; oscillator phases advance only once. */
|
||||
assert(xtc_graph_create(4, &g) == XTC_OK);
|
||||
assert(xtc_graph_add_audio(g, &source) == XTC_OK);
|
||||
assert(xtc_graph_add_gain(g, 0.5f, &gain) == XTC_OK);
|
||||
assert(xtc_graph_add_output(g, &out) == XTC_OK);
|
||||
assert(xtc_graph_connect(g, source, gain) == XTC_OK &&
|
||||
xtc_graph_connect(g, gain, out) == XTC_OK);
|
||||
assert(xtc_graph_compile(g, 48000, 512, &graph) == XTC_OK);
|
||||
assert(xtc_plan_process_stereo(graph, NULL, l, r, NULL, NULL, split, split_r, 512) == XTC_OK);
|
||||
for (i = 0; i < 512; ++i) {
|
||||
close_to(split[i], l[i] * 0.5f);
|
||||
close_to(split_r[i], r[i] * 0.5f);
|
||||
}
|
||||
xtc_plan_destroy(graph);
|
||||
assert(xtc_realtime_create(48000, 512, &engine) == XTC_OK &&
|
||||
xtc_realtime_submit(engine, g) == XTC_OK);
|
||||
assert(xtc_realtime_process_stereo(engine, NULL, l, r, NULL, NULL, split, split_r, 512) ==
|
||||
XTC_OK);
|
||||
for (i = 0; i < 512; ++i)
|
||||
close_to(split_r[i], r[i] * 0.5f);
|
||||
xtc_realtime_destroy(engine);
|
||||
xtc_graph_destroy(g);
|
||||
assert(xtc_graph_create(1, &g) == XTC_OK &&
|
||||
xtc_graph_add_oscillator(g, 440, &source) == XTC_OK &&
|
||||
xtc_graph_compile(g, 48000, 512, &graph) == XTC_OK);
|
||||
assert(xtc_plan_process_stereo(graph, NULL, NULL, NULL, NULL, NULL, l, r, 64) == XTC_OK &&
|
||||
memcmp(l, r, 64 * sizeof(float)) == 0);
|
||||
assert(xtc_plan_process_stereo(graph, NULL, NULL, NULL, NULL, NULL, l + 64, r + 64, 64) ==
|
||||
XTC_OK &&
|
||||
memcmp(l, r, 128 * sizeof(float)) == 0);
|
||||
close_to(l[64], (float)sin(6.283185307179586 * 440 * 64 / 48000));
|
||||
xtc_plan_destroy(graph);
|
||||
xtc_graph_destroy(g);
|
||||
/* Audio track uses the same mix/automation evaluator. */
|
||||
{
|
||||
xtc_recorder *rec = NULL;
|
||||
xtc_audio_plan *audio = NULL;
|
||||
uint64_t frames;
|
||||
for (i = 0; i < 512; ++i)
|
||||
base[i] = 0.25f;
|
||||
assert(xtc_recorder_create(wave, 48000, 1024, &rec) == XTC_OK);
|
||||
xtc_recorder_push(rec, base, 512);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK);
|
||||
assert(xtc_recorder_status(rec, &frames) == XTC_OK && frames == 512);
|
||||
xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_import_audio(p, a, wave, 0, &clip) == XTC_OK &&
|
||||
xtc_audio_plan_create(p, 48000, &audio) == XTC_OK);
|
||||
assert(xtc_audio_plan_render_stereo(audio, 0, l, r, 512) == XTC_OK);
|
||||
close_to(l[250], 0.125f);
|
||||
close_to(r[250], 0.125f);
|
||||
assert(l[500] == 0 && r[500] == 0.25f);
|
||||
xtc_audio_plan_destroy(audio);
|
||||
}
|
||||
assert(xtc_project_save(p, path) == XTC_OK && xtc_project_load(q, path) == XTC_OK &&
|
||||
xtc_project_save(q, copy) == XTC_OK);
|
||||
f = fopen(path, "rb");
|
||||
assert(f);
|
||||
length = fread(bytes, 1, sizeof(bytes), f);
|
||||
assert(length < sizeof(bytes));
|
||||
fclose(f);
|
||||
f = fopen(copy, "rb");
|
||||
assert(f && fread(roundtrip, 1, sizeof(roundtrip), f) == length);
|
||||
fclose(f);
|
||||
assert(memcmp(bytes, roundtrip, length) == 0);
|
||||
for (i = 0; i < length; ++i) {
|
||||
f = fopen(copy, "wb");
|
||||
assert(f && fwrite(bytes, 1, i, f) == i && fclose(f) == 0);
|
||||
assert(xtc_project_load(q, copy) != XTC_OK);
|
||||
}
|
||||
/* Invalid mixer and automation records retain destination. */
|
||||
{
|
||||
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"};
|
||||
for (i = 0; i < sizeof(bad) / sizeof(*bad); ++i) {
|
||||
char *start = strstr(bytes, i < 4 ? "MIX " : "POINT "), *end;
|
||||
assert(start);
|
||||
end = strchr(start, '\n');
|
||||
assert(end);
|
||||
f = fopen(copy, "wb");
|
||||
assert(f);
|
||||
assert(fwrite(bytes, 1, (size_t)(start - bytes), f) == (size_t)(start - bytes));
|
||||
assert(fprintf(f, "%s%s", bad[i], end) > 0);
|
||||
fclose(f);
|
||||
assert(xtc_project_load(q, copy) == XTC_ERR_FORMAT);
|
||||
}
|
||||
}
|
||||
/* v3 retains notes but supplies default mix and no automation. */
|
||||
{
|
||||
char line[1536];
|
||||
FILE *legacy;
|
||||
f = fopen(path, "rb");
|
||||
legacy = fopen(copy, "wb");
|
||||
assert(f && legacy);
|
||||
assert(fgets(line, sizeof(line), f));
|
||||
assert(fputs("XTC_PROJECT 3\n", legacy) >= 0);
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
|
||||
strncmp(line, "POINT ", 6))
|
||||
assert(fputs(line, legacy) >= 0);
|
||||
}
|
||||
assert(fclose(f) == 0 && fclose(legacy) == 0);
|
||||
assert(xtc_project_load(q, copy) == XTC_OK);
|
||||
assert(xtc_project_get_track(q, 0, &track) == XTC_OK && track.gain == 1 && track.pan == 0 &&
|
||||
!track.mute && !track.solo);
|
||||
assert(xtc_project_automation_count(q, &count) == XTC_OK && count == 0);
|
||||
assert(xtc_project_note_count(q, &count) == XTC_OK && count == 1);
|
||||
}
|
||||
assert(xtc_history_create(p, &history) == XTC_OK);
|
||||
for (i = 0; i < 40; ++i) {
|
||||
assert(xtc_project_set_mix(p, a, (float)i / 10, 0, 0, 0) == XTC_OK &&
|
||||
xtc_history_record(history, p) == XTC_OK);
|
||||
}
|
||||
for (i = 0; i < 32; ++i) {
|
||||
assert(xtc_history_checkout(history, 0, &candidate) == XTC_OK);
|
||||
xtc_project_destroy(candidate);
|
||||
assert(xtc_history_accept(history, 0) == XTC_OK);
|
||||
}
|
||||
assert(xtc_history_checkout(history, 0, &candidate) == XTC_ERR_NOT_FOUND && candidate == NULL);
|
||||
assert(xtc_history_checkout(history, 1, &candidate) == XTC_OK);
|
||||
assert(xtc_project_get_track(candidate, 0, &track) == XTC_OK);
|
||||
close_to(track.gain, 0.8f);
|
||||
xtc_project_destroy(candidate);
|
||||
assert(xtc_history_accept(history, 1) == XTC_OK);
|
||||
assert(xtc_history_record(history, p) == XTC_OK &&
|
||||
xtc_history_checkout(history, 1, &candidate) == XTC_ERR_NOT_FOUND);
|
||||
{
|
||||
uint64_t discarded, replacement;
|
||||
assert(xtc_project_add_track(p, "Discarded", &discarded) == XTC_OK);
|
||||
assert(xtc_history_record(history, p) == XTC_OK);
|
||||
assert(xtc_history_checkout(history, 0, &candidate) == XTC_OK);
|
||||
assert(xtc_project_add_track(candidate, "New branch", &replacement) == XTC_OK &&
|
||||
replacement > discarded);
|
||||
xtc_project_destroy(candidate);
|
||||
}
|
||||
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};
|
||||
for (i = 0; i < XTC_MAX_AUTOMATION_POINTS; ++i) {
|
||||
point.tick = i;
|
||||
assert(xtc_project_set_automation(p, &point) == XTC_OK);
|
||||
}
|
||||
point.tick = i;
|
||||
assert(xtc_project_set_automation(p, &point) == XTC_ERR_CAPACITY);
|
||||
point.tick = 0;
|
||||
point.value = 2;
|
||||
assert(xtc_project_set_automation(p, &point) == XTC_OK);
|
||||
assert(xtc_project_remove_automation(p, b, XTC_AUTOMATION_GAIN, 0) == XTC_OK);
|
||||
assert(xtc_project_remove_automation(p, b, XTC_AUTOMATION_GAIN, 0) == XTC_ERR_NOT_FOUND);
|
||||
xtc_project_destroy(p);
|
||||
xtc_project_destroy(q);
|
||||
unlink(path);
|
||||
unlink(copy);
|
||||
unlink(wave);
|
||||
rmdir(dir);
|
||||
puts("Mixer stereo/gain/pan/mute/solo/automation/history/persistence passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue