Phase 12: Improve audio transitions and verify sample-accurate timing
This commit is contained in:
parent
461689dd44
commit
05140c2e93
29 changed files with 795 additions and 33 deletions
17
Makefile
17
Makefile
|
|
@ -15,7 +15,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
CORE_SOURCES := src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
|
||||
ifeq ($(JACK_AVAILABLE),yes)
|
||||
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
|
||||
|
|
@ -272,3 +272,18 @@ test-workflow:
|
|||
endif
|
||||
|
||||
-include $(BUILD_DIR)/tests/integration/test_workflow.d
|
||||
|
||||
$(BUILD_DIR)/test_quality: $(BUILD_DIR)/tests/unit/test_quality.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm
|
||||
.PHONY: test-quality
|
||||
test-quality: $(BUILD_DIR)/test_quality
|
||||
./$(BUILD_DIR)/test_quality
|
||||
test: test-quality
|
||||
-include $(BUILD_DIR)/tests/unit/test_quality.d
|
||||
|
||||
$(BUILD_DIR)/quality-preview: $(BUILD_DIR)/examples/quality_preview.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm
|
||||
.PHONY: quality-preview
|
||||
quality-preview: $(BUILD_DIR)/quality-preview
|
||||
./$(BUILD_DIR)/quality-preview "$(abspath $(BUILD_DIR))/quality"
|
||||
-include $(BUILD_DIR)/examples/quality_preview.d
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ This repository is a starter scaffold, not a production DAW. It currently provid
|
|||
- 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;
|
||||
- configurable boundary fades, gain ramps and graceful GUI playback transitions;
|
||||
- native C processors, deterministic source packaging and staged install/uninstall;
|
||||
- architecture, format, GUI, roadmap, and decision documents.
|
||||
|
||||
|
|
@ -109,3 +110,7 @@ Read `AGENT.md` and `docs/ARCHITECTURE.md`. Inspect the existing implementation,
|
|||
## License
|
||||
|
||||
The project is scaffolded under the MIT License; see `LICENSE`. Review the licenses of any future dependencies before distribution.
|
||||
|
||||
Audio timing, transitions, latency limits and listening previews are described
|
||||
in [docs/AUDIO_QUALITY.md](docs/AUDIO_QUALITY.md). Human listening acceptance
|
||||
for the Phase 12 transitions remains pending.
|
||||
|
|
|
|||
|
|
@ -280,3 +280,13 @@ and serialization limits and PERFORMANCE.md for measured workload and budgets.
|
|||
reclamation ordering, callback work bounds and repeatable diagnostic tests.
|
||||
Graph stereo rendering completes both channels before writing caller outputs;
|
||||
either output may equal a source, but the output buffers must not overlap.
|
||||
|
||||
## Phase 12 quality policy
|
||||
|
||||
[AUDIO_QUALITY.md](AUDIO_QUALITY.md) defines absolute musical timing, tempo snapshot
|
||||
changes, scheduled source envelopes and configurable master transitions. The GUI
|
||||
enables 5 ms fades; low-level defaults remain raw. `xtc_backend_request_stop` is
|
||||
asynchronous and does not establish quiescence; control-thread collection after
|
||||
its callback acknowledgement deactivates JACK. Immediate stop/close remain
|
||||
available. Sample rate and buffer frames are reported, but end-to-end latency
|
||||
and native-node delay compensation are not implemented.
|
||||
|
|
|
|||
168
docs/AUDIO_QUALITY.md
Normal file
168
docs/AUDIO_QUALITY.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Timing, transitions and audio quality
|
||||
|
||||
Phase 12 adds deterministic transition processing and tests. Musical time still
|
||||
uses a single constant tempo, 960 ticks per quarter note and fixed 4/4 meter;
|
||||
it does not introduce a tempo map, meter editor or JACK transport following.
|
||||
Project format remains v4 (reads v1–v3).
|
||||
|
||||
## Timing policy
|
||||
|
||||
`xtc_project_set_tempo` accepts 20000–300000 milli-BPM and changes the editable
|
||||
model only. Build new audio/MIDI/mixer plans while stopped to adopt it. Existing
|
||||
plans keep their prepared sample positions and mix snapshots. The backend retains
|
||||
its absolute sample counter through preparation; explicitly seek if the caller
|
||||
wants to preserve a particular musical tick after changing tempo. The GUI has no
|
||||
tempo-change command yet. Meter is always 4/4, has no setter and does not vary
|
||||
within a project.
|
||||
|
||||
Every musical boundary is converted from its **absolute** tick:
|
||||
|
||||
```
|
||||
sample = floor(tick * rate * 60000 / (tempo_milli_bpm * 960))
|
||||
```
|
||||
|
||||
Conversions use bounded unsigned integer arithmetic, not accumulated floating
|
||||
beat durations. The inverse also floors; a sample-to-tick roundtrip can lose a
|
||||
tick because of quantization. At very low supported rates several ticks can map
|
||||
to one sample. Collapsed note spans are silent. Automation uses the last point
|
||||
in tick order at a shared sample. Clip/note intervals are half-open `[start,end)`;
|
||||
a boundary exactly at the next block belongs to that next block. The MIDI input
|
||||
array preserves caller order for equal offsets (for example on/off versus off/on).
|
||||
A discontinuous MIDI render position resets live voices before rendering.
|
||||
|
||||
Changing tempo shifts musical boundaries, including source offsets, on newly
|
||||
compiled plans. WAV samples keep their original playback speed with linear rate
|
||||
conversion; this is not audio time stretching. Scheduled note phase comes from
|
||||
absolute note age, so seeks and block partitions reproduce the same waveform.
|
||||
The graph oscillator/native graph clock is plan-local, rather than project time.
|
||||
|
||||
## Boundary fades and smoothing
|
||||
|
||||
The GUI configures `xtc_backend_set_transition_frames(rate/200)`: approximately
|
||||
5 ms at the current rate (integer frames, floor). Configuration is control-thread
|
||||
only while stopped, before project preparation. The low-level backend and
|
||||
source-plan APIs default to zero transition frames for source compatibility.
|
||||
Call `xtc_audio_plan_set_fade_frames` and `xtc_midi_plan_set_fade_frames` before
|
||||
publication to enable the same behavior in an offline/custom host.
|
||||
|
||||
Each audio region and scheduled note gets an independent linear boundary
|
||||
envelope. Its first and last audible samples are zero. At distance `d` from the
|
||||
nearer boundary the factor is `min(1, d/fade_frames)`. For audio, the end is the
|
||||
earlier of the clip end and the actual loaded media end after source offset/rate
|
||||
conversion; rounded-up imported duration does not expose an unfaded last sample.
|
||||
The envelope is computed from absolute sample position: render partitioning,
|
||||
seeks, overlapping regions and mono/stereo calls do not advance shared envelope
|
||||
state. Very short regions are attenuated throughout; a one-sample region becomes
|
||||
silent. Adjacent regions each fade independently, so this is not a crossfade or
|
||||
seamless-loop implementation. Fade configuration is a host policy, not persisted
|
||||
per-clip editing metadata.
|
||||
|
||||
`xtc_gain_ramp` in `xtc_dsp.h` is a single-owner C primitive for smooth gain and
|
||||
parameter changes. Initialize with reset, set a finite target and length, then
|
||||
process preallocated mono/stereo buffers. It reaches the exact target on the Nth
|
||||
sample, with the first sample one step toward the target. Retargeting starts from
|
||||
the current gain; zero frames changes it immediately. Stereo shares one gain and
|
||||
advances once per frame. Invalid calls leave buffers/state intact. It allocates
|
||||
nothing. Mixer automation already interpolates gain/pan per sample; the ramp
|
||||
primitive does not add a second smoothing delay to that timeline. Static mixer
|
||||
edits stop playback and use the master ramp on restart. Live native parameter
|
||||
updates and live graph replacements have no automatic crossfade.
|
||||
|
||||
## Graceful and immediate stopping
|
||||
|
||||
GUI Play fades the master from zero to unity. Ordinary GUI Pause/Stop request a
|
||||
fade to zero using `xtc_backend_request_stop`; repeated requests do not restart
|
||||
the fade. The callback publishes completion with a release atomic, then outputs
|
||||
silence and holds the sample position. `xtc_backend_collect` on the control thread
|
||||
acquires completion and calls JACK deactivate, which establishes quiescence.
|
||||
Until then, status reports `running` and `stopping`, and the GUI displays that it
|
||||
is fading. The 100 ms GUI timer completes deactivation; it does not control fade
|
||||
timing. If deactivation fails, context/ownership remains intact, silence
|
||||
continues, the error is reported and later collection retries.
|
||||
|
||||
The ramp length is sample-counted, but the last callback processes a whole block
|
||||
and advances the sample position by that block, including its zero-valued tail.
|
||||
A stop request starts at the next observed block boundary. Therefore Pause/Stop
|
||||
can consume up to approximately `fade_frames + block_frames` after the request,
|
||||
plus server scheduling; this is not an immediate sample-exact stop command.
|
||||
Play resumes at the retained block-end sample position with a fresh start ramp.
|
||||
No callback waits for control or performs deactivation/allocation/I/O.
|
||||
|
||||
`xtc_backend_stop`/close remain synchronous immediate operations. Recording Stop
|
||||
uses immediate deactivation before draining/finalizing the input take; recording
|
||||
samples are never faded by the output master. Graceful requests refuse attached
|
||||
recorders. Home/seek, project installation, recording start, panic, errors and
|
||||
server loss retain immediate semantics. Those transitions can click; safety and
|
||||
material preservation take priority. If callbacks cease, use immediate stop or
|
||||
close rather than waiting for fade acknowledgement. Live note-off/panic remains
|
||||
immediate with no release tail; scheduled notes use the boundary envelope.
|
||||
|
||||
## Numerical and channel policy
|
||||
|
||||
Core graph, native output, audio and MIDI rendering canonicalize nonfinite values
|
||||
and float subnormals to zero at output boundaries; graph node buffers also use
|
||||
this policy. Native callbacks remain trusted and may have their own internal
|
||||
denormal behavior. No process-wide FTZ/DAZ flags are modified. Normal values as
|
||||
small as `FLT_MIN` are retained. The raw constant-gain helper is unchanged;
|
||||
`xtc_gain_ramp_process` applies the canonical output policy.
|
||||
|
||||
Finite values above unity are preserved for mixing headroom. There is no limiter,
|
||||
soft clipping or dither. The physical device/downstream system may clip; lower
|
||||
track gains if meters exceed unity. Ports remain mono with paired stereo passes;
|
||||
external mono input is duplicated. Audio assets are downmixed on import, so
|
||||
stereo assets do not retain stereo image. Output buffers must not overlap each
|
||||
other; graph sources may alias outputs because both source passes finish first.
|
||||
Supported render sizes are 0–65536 frames within the prepared graph maximum.
|
||||
|
||||
## Latency and synchronization limits
|
||||
|
||||
Backend status reports sample rate and current buffer frames, not measured
|
||||
end-to-end device latency. `buffer_frames/sample_rate` is one process quantum,
|
||||
not capture-to-speaker latency. JACK owns hardware/graph scheduling and external
|
||||
port latencies; xtc currently does not query/display JACK latency ranges or
|
||||
publish an explicit per-node latency model. Fades shape amplitude without a
|
||||
lookahead/delay buffer, so they add no timeline delay; they do attenuate attacks.
|
||||
|
||||
Built-in gain/meter/output, preloaded sources and the sine instrument have no
|
||||
algorithmic delay to compensate between tracks. The graph has no delay inserts,
|
||||
external plugins or parallel compensated processing paths. Native extensions can
|
||||
introduce unknown delay, which is neither reported nor compensated. A latency/
|
||||
tail contract and compensation are required before claiming synchronized delayed
|
||||
inserts or external hosting. JACK transport sync, external MIDI clock, tempo
|
||||
maps and physical recording-offset calibration remain unsupported.
|
||||
|
||||
## Verification and listening
|
||||
|
||||
```
|
||||
make all test gui test-reliability test-workflow test-gui test-gui-jack
|
||||
make test-jack test-jack-audio test-jack-midi test-performance
|
||||
make test-sanitize analyze
|
||||
make quality-preview
|
||||
```
|
||||
|
||||
`test_quality` is part of `make test`. It covers a sample-rate/tempo matrix up to
|
||||
768 kHz and the maximum project tick, conversion errors, old/new tempo snapshots
|
||||
for both audio and MIDI, exact offset/on/off order, sample boundary behavior,
|
||||
raw versus faded endpoints, media ends shorter than rounded clip ends, split/
|
||||
whole equivalence, seeks, ramps, stereo parity, overload, NaN/infinity/subnormals
|
||||
and block sizes 1, 2, 3, 17, 257, 4097, 65535 and 65536. Backend fault tests check
|
||||
exact master ramps, repeated stop requests, held position/silence after completion,
|
||||
deactivation failure/retry, immediate escape and recording guards. The workflow
|
||||
render comparison now enables the GUI source fade policy before/after reopening.
|
||||
|
||||
Phase 12 automated checks pass warning-enabled core/GUI and no-JACK builds,
|
||||
ASan/UBSan/leak checks, static analysis, pthread TSan tests, mock and live GUI,
|
||||
software JACK audio/MIDI tests and staged installation. GUI leak checks retain
|
||||
the existing fontconfig-only suppression; core leak checks are unsuppressed.
|
||||
Physical-device listening and human listening acceptance are **not yet verified**.
|
||||
|
||||
`make quality-preview` writes ignored build artifacts `quality-raw.wav` and
|
||||
`quality-smooth.wav` in BUILD_DIR using `examples/quality_preview.c`. Compare at
|
||||
moderate playback level: first listen to the three-note phrase endings, then the
|
||||
constant clip at two seconds, then a mid-period tone stop near three seconds.
|
||||
Check that fades reduce edge clicks, do not add noise and do not unexpectedly
|
||||
change pitch/timing. Also listen to the saved MIDI/audio workflow project with
|
||||
repeated Play/Pause/Stop, Home/seek and representative gain/pan automation on the
|
||||
actual target device. Record device, rate, block size and observations before
|
||||
marking human listening acceptance complete. Generated previews are reproducible
|
||||
test material, not a claim that anyone has listened to them.
|
||||
|
|
@ -239,3 +239,10 @@ full verification commands and remaining limits.
|
|||
JACK-compatible server. It does not start/configure the server or connect
|
||||
physical devices. See [WORKFLOW.md](WORKFLOW.md) for the full GUI composition
|
||||
acceptance sequence and transport semantics.
|
||||
|
||||
## Phase 12 audio quality
|
||||
|
||||
`make test-quality` runs exact timing, source-boundary, gain-ramp, event-order
|
||||
and numeric/layout tests (also included in `make test`). `make quality-preview`
|
||||
builds the C example and writes raw/smoothed listening WAVs into BUILD_DIR without
|
||||
playing them or configuring devices. See [AUDIO_QUALITY.md](AUDIO_QUALITY.md).
|
||||
|
|
|
|||
|
|
@ -193,3 +193,12 @@ even when the previous document is retained.
|
|||
stereo render comparisons across destruction/recreation of the GUI and live
|
||||
playhead/meter/transport checks. `test-gui` covers stop/seek failure and recording
|
||||
guards. No project files need manual editing for this workflow.
|
||||
|
||||
## Phase 12 timing and transitions
|
||||
|
||||
The GUI enables 5 ms source boundaries and master start/stop ramps. Ordinary
|
||||
Pause/Stop now request a fade and show “stopping” until the timer collects its
|
||||
acknowledgement and deactivates JACK. Recording Stop, Home, fault handling and
|
||||
project installation retain immediate semantics. See
|
||||
[AUDIO_QUALITY.md](AUDIO_QUALITY.md) for timing, overload/denormal behavior, latency
|
||||
limits, test commands and the pending human-listening checklist.
|
||||
|
|
|
|||
|
|
@ -114,3 +114,14 @@ arbitrary third-party native callbacks remain unqualified. Sanitizers change
|
|||
timing and should not be used to measure production realtime performance. See
|
||||
PERFORMANCE.md for workload measurements, RECOVERY.md for process-crash limits,
|
||||
and NATIVE_API.md for the trusted extension boundary.
|
||||
|
||||
## Phase 12 transition ownership
|
||||
|
||||
Source fade configuration is written before publication and immutable during
|
||||
rendering. The master ramp and stop-observed flag belong to the audio consumer
|
||||
while active; start initializes them only while quiescent. Control publishes an
|
||||
atomic stop request; audio publishes completion after the last ramp sample.
|
||||
Collection acquires completion and deactivates the backend before resetting
|
||||
voices/flags. A completed callback continues silence/holds position until that
|
||||
deactivation. No callback waits or calls JACK control functions. Immediate
|
||||
stop/close handles faults or missing callbacks. See AUDIO_QUALITY.md.
|
||||
|
|
|
|||
|
|
@ -199,6 +199,18 @@ explicit; physical-device listening and endurance remain unqualified.
|
|||
- Listening tests cover transitions, start/stop behavior, and representative projects.
|
||||
- Known limitations involving latency and synchronization are documented.
|
||||
|
||||
Implemented: documented constant-tempo/fixed-4/4 snapshot policy; absolute timing
|
||||
and sample-offset regression matrix; configurable audio/scheduled-note boundary
|
||||
fades; single-owner gain ramps; GUI 5 ms start and asynchronous Pause/Stop fades
|
||||
with control-thread acknowledgement/deactivation; finite/subnormal output policy;
|
||||
exact endpoint, block-size, stereo, overload and failure/retry tests. Latency and
|
||||
synchronization limits are documented in AUDIO_QUALITY.md. Normal/no-JACK,
|
||||
sanitizer/static-analysis, TSan, GUI/workflow/live JACK and install checks pass.
|
||||
`make quality-preview` generates matched listening WAVs. Human listening and
|
||||
physical-device acceptance remain pending; immediate safety/recording/seek paths
|
||||
and live graph swaps may still click. No tempo/meter map, latency compensation,
|
||||
limiter or project-format change is claimed.
|
||||
|
||||
## Phase 13 — Recording and Recovery
|
||||
|
||||
**Goal:** Protect recorded material and handle recording failures predictably.
|
||||
|
|
|
|||
|
|
@ -34,13 +34,17 @@ velocity 100; no notes or test oscillators are inserted automatically.
|
|||
| Command | Behavior |
|
||||
| --- | --- |
|
||||
| Play / F5 | Prepare the current arrangement and resume at the retained sample position. Already playing is a no-op. |
|
||||
| Pause / F7 | Deactivate playback and hold the sample position. Play resumes from there. Disabled during recording. |
|
||||
| Stop / F8 | Hold the sample position, preserving the established Stop behavior. During recording, finalize and add the take. |
|
||||
| Pause / F7 | Fade out, then deactivate playback and hold the final block-end sample position. Play resumes from there. Disabled during recording. |
|
||||
| Stop / F8 | Fade out and hold the final block-end sample position. During recording, finalize and add the take. |
|
||||
| Space | Toggle Play/Stop; while recording it finishes the take. |
|
||||
| To start / Home | Stop and seek to sample zero; remain stopped until Play. Disabled during recording. Also resets an offline cursor. |
|
||||
| Click timeline ruler | Seek on the sixteenth-note grid while stopped; during playback the seek is refused. |
|
||||
| New / Open / Reconnect | Stop and reset the position. New/Open show Sequencer; Reconnect leaves the document intact. |
|
||||
|
||||
Pause/Stop completion is asynchronous: the GUI briefly shows “stopping”. Recording
|
||||
Stop and Home retain immediate deactivation. See [AUDIO_QUALITY.md](AUDIO_QUALITY.md)
|
||||
for the 5 ms fade policy and timing bounds.
|
||||
|
||||
The backend sample count drives the playhead; its display refreshes every 100 ms.
|
||||
Pause/Stop retain the exact sample position even though the ruler displays ticks.
|
||||
Arrangement, note and mixer edits stop playback; graph edits can be adopted live.
|
||||
|
|
|
|||
|
|
@ -3,3 +3,9 @@
|
|||
`native_gain.c` is a complete trusted native C effect compiled into an offline
|
||||
graph. Build/run with `make build/native-gain && ./build/native-gain`; it needs
|
||||
no GUI/server/plugin SDK. See ../docs/NATIVE_API.md for lifecycle/event contracts.
|
||||
|
||||
`quality_preview.c` uses public C MIDI, recorder and gain-ramp APIs to generate
|
||||
matched raw/smoothed listening fixtures. Run `make quality-preview`; generated
|
||||
WAVs remain in the ignored build directory. See docs/AUDIO_QUALITY.md for the
|
||||
listening sequence and the distinction between automated checks and human
|
||||
listening acceptance. It never starts a backend or plays audio.
|
||||
|
|
|
|||
70
examples/quality_preview.c
Normal file
70
examples/quality_preview.c
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* Generate matched raw/smoothed previews for human listening; never plays audio.
|
||||
* Usage: quality-preview /absolute/path/prefix (writes prefix-{raw,smooth}.wav). */
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static xtc_result preview(const char *prefix, int smooth)
|
||||
{
|
||||
xtc_project *project = NULL;
|
||||
xtc_midi_plan *midi = NULL;
|
||||
xtc_recorder *recorder = NULL;
|
||||
xtc_gain_ramp ramp;
|
||||
uint64_t track, clip, note, position;
|
||||
char path[1024];
|
||||
xtc_result result;
|
||||
int n = snprintf(path, sizeof(path), "%s-%s.wav", prefix, smooth ? "smooth" : "raw");
|
||||
if (n < 0 || (size_t)n >= sizeof(path)) return XTC_ERR_CAPACITY;
|
||||
result = xtc_project_create(&project);
|
||||
if (result == XTC_OK) result = xtc_project_add_track(project, "Listening phrase", &track);
|
||||
if (result == XTC_OK) result = xtc_project_add_midi_clip(project, track, 0, 7680, &clip);
|
||||
if (result == XTC_OK) result = xtc_project_add_note(project, clip, 0, 480, 60, 100, 0, ¬e);
|
||||
if (result == XTC_OK) result = xtc_project_add_note(project, clip, 720, 480, 64, 100, 0, ¬e);
|
||||
if (result == XTC_OK) result = xtc_project_add_note(project, clip, 1440, 1440, 67, 100, 0, ¬e);
|
||||
if (result == XTC_OK) result = xtc_midi_plan_create(project, 48000, &midi);
|
||||
if (result == XTC_OK) result = xtc_midi_plan_set_fade_frames(midi, smooth ? 240U : 0U);
|
||||
if (result == XTC_OK) result = xtc_recorder_create(path, 48000, 256, &recorder);
|
||||
(void)xtc_gain_ramp_reset(&ramp, smooth ? 0 : 1);
|
||||
(void)xtc_gain_ramp_target(&ramp, 1, smooth ? 240U : 0U);
|
||||
for (position = 0; result == XTC_OK && position < 192000; position += 256) {
|
||||
float samples[256];
|
||||
size_t i;
|
||||
result = xtc_midi_plan_render(midi, position, NULL, 0, samples, 256);
|
||||
if (result != XTC_OK) break;
|
||||
for (i = 0; i < 256; ++i) {
|
||||
uint64_t sample = position + i;
|
||||
/* Deliberately abrupt constant clips with 5 ms fades in the smooth
|
||||
* version, followed by a tone stopped mid-period at three seconds. */
|
||||
if (sample >= 96000 && sample < 108000) {
|
||||
uint64_t age = sample - 96000, tail = 107999 - sample;
|
||||
uint64_t distance = age < tail ? age : tail;
|
||||
float envelope = smooth && distance < 240 ? (float)distance / 240.0f : 1;
|
||||
samples[i] += 0.2f * envelope;
|
||||
}
|
||||
if (sample >= 120000 && sample < 144000)
|
||||
samples[i] += 0.2f * (float)sin(6.283185307179586 * 443.0 * (double)(sample - 120000) / 48000.0);
|
||||
if (sample == 143760)
|
||||
(void)xtc_gain_ramp_target(&ramp, 0, smooth ? 240U : 0U);
|
||||
(void)xtc_gain_ramp_process(&ramp, samples + i, NULL, 1);
|
||||
}
|
||||
xtc_recorder_push(recorder, samples, 256);
|
||||
result = xtc_recorder_drain(recorder);
|
||||
}
|
||||
if (result == XTC_OK) result = xtc_recorder_finish(recorder);
|
||||
xtc_recorder_destroy(recorder); xtc_midi_plan_destroy(midi); xtc_project_destroy(project);
|
||||
if (result == XTC_OK) printf("Wrote %s\n", path);
|
||||
return result;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
xtc_result result;
|
||||
if (argc != 2) { fprintf(stderr, "Usage: %s OUTPUT_PREFIX\n", argv[0]); return EXIT_FAILURE; }
|
||||
result = preview(argv[1], 0);
|
||||
if (result == XTC_OK) result = preview(argv[1], 1);
|
||||
if (result != XTC_OK) { fprintf(stderr, "%s\n", xtc_result_string(result)); return EXIT_FAILURE; }
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate,
|
|||
* @param plan Preloaded arrangement plan.
|
||||
*/
|
||||
void xtc_audio_plan_destroy(xtc_audio_plan *plan);
|
||||
/* Control thread before publication only. Zero (default) preserves raw samples;
|
||||
* 1..65536 applies linear fades to each audible region's actual media bounds.
|
||||
* Rendering remains stateless, including seeks and variable block partitions. */
|
||||
xtc_result xtc_audio_plan_set_fade_frames(xtc_audio_plan *plan, size_t frames);
|
||||
/**
|
||||
* @brief Returns the number of asset issues and optionally the first affected path.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ typedef struct {
|
|||
uint64_t sample_position; /* Successful callback frames; retained across stop/submit. */
|
||||
unsigned int submitted_revision, active_revision;
|
||||
int running;
|
||||
int stopping; /* Graceful stop requested; collect completes deactivation. */
|
||||
int server_lost;
|
||||
int sample_rate_changed; /* Sticky: close/reopen and resubmit to recover. */
|
||||
xtc_result process_result;
|
||||
|
|
@ -75,6 +76,16 @@ xtc_result xtc_backend_start(xtc_backend *backend);
|
|||
* @return XTC_OK on success, or an error code describing the failure.
|
||||
*/
|
||||
xtc_result xtc_backend_stop(xtc_backend *backend);
|
||||
/* Configure while stopped, before preparing a project. Default zero preserves
|
||||
* raw rendering/immediate stop. GUI uses 5 ms (rate/200). Frames <=65536.
|
||||
* Applies source-boundary fades and a master start/stop gain ramp. */
|
||||
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames);
|
||||
/* Non-blocking graceful stop request; not callback quiescence. Repeated calls
|
||||
* are idempotent. Process finishes the ramp, then emits silence/holds position.
|
||||
* Poll/collect on the control thread to deactivate. Recorder must be detached.
|
||||
* Immediate stop/close remain the escape path if callbacks cease or errors occur. */
|
||||
xtc_result xtc_backend_request_stop(xtc_backend *backend);
|
||||
|
||||
/* Stopped only; changes transport clock, not oscillator phase. */
|
||||
/**
|
||||
* @brief Changes the stopped backend's sample position and resets MIDI voices.
|
||||
|
|
@ -140,7 +151,7 @@ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **o
|
|||
*/
|
||||
xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder);
|
||||
/**
|
||||
* @brief Reclaims retired realtime plans on the control thread.
|
||||
* @brief Completes acknowledged fades and reclaims retired plans on the control thread.
|
||||
*
|
||||
* @param backend Backend handle.
|
||||
*/
|
||||
|
|
|
|||
20
include/xtc/xtc_dsp.h
Normal file
20
include/xtc/xtc_dsp.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef XTC_DSP_H
|
||||
#define XTC_DSP_H
|
||||
#include "xtc_types.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Single processing owner (or quiescent control thread). No allocation or I/O.
|
||||
* Initialize with reset before target/process.
|
||||
* Linear ramps reach the target on the Nth sample, independent of block size. */
|
||||
typedef struct { float current, target; size_t remaining; } xtc_gain_ramp;
|
||||
xtc_result xtc_gain_ramp_reset(xtc_gain_ramp *ramp, float value);
|
||||
xtc_result xtc_gain_ramp_target(xtc_gain_ramp *ramp, float target, size_t frames);
|
||||
xtc_result xtc_gain_ramp_process(xtc_gain_ramp *ramp, float *left, float *right, size_t frames);
|
||||
/* Canonical silence for NaN, infinity and float subnormals; finite overloads
|
||||
* are retained for mixing/headroom. This does not change CPU-wide FP flags. */
|
||||
float xtc_dsp_finite(float sample);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
@ -129,6 +129,10 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int sample_
|
|||
* @param plan Instrument plan and its single-owner runtime state.
|
||||
*/
|
||||
void xtc_midi_plan_destroy(xtc_midi_plan *plan);
|
||||
/* Set before publication, while quiescent. Zero preserves raw scheduled notes.
|
||||
* Linear boundary envelopes do not delay note scheduling; live note-offs and
|
||||
* panic remain immediate. Max 65536 frames. */
|
||||
xtc_result xtc_midi_plan_set_fade_frames(xtc_midi_plan *plan, size_t frames);
|
||||
/* Audio owner or stopped only. Reset clears live notes and panic suppression.
|
||||
* Panic silences live notes and scheduled notes already started at position;
|
||||
* later scheduled notes can start. MIDI CC120/123 affects its own channel. */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "wave_internal.h"
|
||||
#include "../mixer/mix_internal.h"
|
||||
#include "../graph/graph_internal.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -12,6 +13,7 @@ typedef struct {
|
|||
} region;
|
||||
|
||||
struct xtc_audio_plan {
|
||||
size_t fade_frames;
|
||||
xtc_mix_snapshot mix;
|
||||
xtc_wave waves[64];
|
||||
char paths[64][XTC_ASSET_PATH_BYTES];
|
||||
|
|
@ -209,6 +211,13 @@ size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **path)
|
|||
return plan ? plan->issues : 0;
|
||||
}
|
||||
|
||||
xtc_result xtc_audio_plan_set_fade_frames(xtc_audio_plan *plan, size_t frames)
|
||||
{
|
||||
if (!plan || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
|
||||
plan->fade_frames = frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Renders a block of the plan at unity gain with linear rate conversion.
|
||||
*
|
||||
|
|
@ -235,8 +244,16 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos
|
|||
for (i = 0; i < plan->count; ++i) {
|
||||
const region *r = plan->regions + i;
|
||||
const xtc_wave *wave = plan->waves + r->asset;
|
||||
uint64_t audible_end = r->end;
|
||||
uint64_t begin = position > r->start ? position : r->start;
|
||||
uint64_t end = position + frames < r->end ? position + frames : r->end;
|
||||
uint64_t end;
|
||||
if (plan->fade_frames && wave->samples) {
|
||||
uint64_t duration = ((uint64_t)wave->frames * plan->rate + wave->rate - 1U) / wave->rate;
|
||||
if (duration <= r->offset) continue;
|
||||
if (duration - r->offset < audible_end - r->start)
|
||||
audible_end = r->start + duration - r->offset;
|
||||
}
|
||||
end = position + frames < audible_end ? position + frames : audible_end;
|
||||
if (!wave->samples || end <= begin)
|
||||
continue;
|
||||
for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) {
|
||||
|
|
@ -250,16 +267,21 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos
|
|||
float sample =
|
||||
(float)(wave->samples[index] * (1.0 - fraction) +
|
||||
(index + 1 < wave->frames ? wave->samples[index + 1] : wave->samples[index]) * fraction);
|
||||
if (plan->fade_frames) {
|
||||
uint64_t age = position + j - r->start;
|
||||
uint64_t tail = audible_end - 1U - (position + j);
|
||||
uint64_t distance = age < tail ? age : tail;
|
||||
if (distance < plan->fade_frames)
|
||||
sample *= (float)distance / (float)plan->fade_frames;
|
||||
}
|
||||
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;
|
||||
if (right && !isfinite(right[j]))
|
||||
right[j] = 0;
|
||||
output[j] = xtc_dsp_finite(output[j]);
|
||||
if (right) right[j] = xtc_dsp_finite(right[j]);
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -19,6 +20,10 @@ struct xtc_backend {
|
|||
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
|
||||
float *timeline, *instrument;
|
||||
size_t max_frames;
|
||||
size_t transition_frames; /* Control owner while stopped. */
|
||||
xtc_gain_ramp ramp; /* Audio owner while active. */
|
||||
unsigned int stop_requested, stop_done;
|
||||
int stop_seen; /* Audio owner. */
|
||||
uint64_t sample_position;
|
||||
unsigned int expected_rate;
|
||||
unsigned int rate, frames, lost, rate_changed, xruns;
|
||||
|
|
@ -104,6 +109,13 @@ static int process(jack_nframes_t frames, void *context)
|
|||
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
|
||||
return 0;
|
||||
}
|
||||
if (__atomic_load_n(&backend->stop_done, __ATOMIC_ACQUIRE)) {
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
||||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) {
|
||||
result = XTC_ERR_INVALID_STATE;
|
||||
|
|
@ -128,6 +140,15 @@ static int process(jack_nframes_t frames, void *context)
|
|||
backend->midi && right ? backend->instrument + backend->max_frames : NULL,
|
||||
output, right, (size_t)frames);
|
||||
}
|
||||
if (result == XTC_OK && backend->transition_frames) {
|
||||
if (__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE) && !backend->stop_seen) {
|
||||
(void)xtc_gain_ramp_target(&backend->ramp, 0, backend->transition_frames);
|
||||
backend->stop_seen = 1;
|
||||
}
|
||||
(void)xtc_gain_ramp_process(&backend->ramp, output, right, (size_t)frames);
|
||||
if (backend->stop_seen && !backend->ramp.remaining)
|
||||
__atomic_store_n(&backend->stop_done, 1U, __ATOMIC_RELEASE);
|
||||
}
|
||||
if (result == XTC_OK) {
|
||||
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
||||
if (position > UINT64_MAX - frames)
|
||||
|
|
@ -361,6 +382,11 @@ xtc_result xtc_backend_start(xtc_backend *backend)
|
|||
return XTC_ERR_INVALID_STATE;
|
||||
xtc_midi_plan_reset(backend->midi);
|
||||
backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
|
||||
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
|
||||
backend->stop_seen = 0;
|
||||
(void)xtc_gain_ramp_reset(&backend->ramp, backend->transition_frames ? 0 : 1);
|
||||
(void)xtc_gain_ramp_target(&backend->ramp, 1, backend->transition_frames);
|
||||
if (jack_activate(backend->client) != 0)
|
||||
return XTC_ERR_IO;
|
||||
backend->running = 1;
|
||||
|
|
@ -386,6 +412,8 @@ xtc_result xtc_backend_stop(xtc_backend *backend)
|
|||
if (jack_deactivate(backend->client) != 0)
|
||||
return XTC_ERR_IO;
|
||||
backend->running = 0;
|
||||
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
|
||||
xtc_midi_plan_reset(backend->midi);
|
||||
xtc_realtime_collect(backend->engine);
|
||||
return XTC_OK;
|
||||
|
|
@ -462,8 +490,12 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
|
|||
if (!backend->ready || backend->running)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
result = xtc_audio_plan_create(project, backend->expected_rate, &candidate);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_audio_plan_set_fade_frames(candidate, backend->transition_frames);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_midi_plan_create(project, backend->expected_rate, &midi);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_midi_plan_set_fade_frames(midi, backend->transition_frames);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_backend_submit(backend, xtc_project_graph(project));
|
||||
if (result != XTC_OK) {
|
||||
|
|
@ -547,15 +579,38 @@ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **o
|
|||
return XTC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reclaims retired realtime plans on the control thread.
|
||||
*
|
||||
* @param backend Backend handle.
|
||||
*/
|
||||
/* Control-only configuration; applied to source plans on next preparation. */
|
||||
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
|
||||
{
|
||||
if (!backend || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE;
|
||||
backend->transition_frames = frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
||||
{
|
||||
if (!backend) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!backend->ready || backend->recorder) return XTC_ERR_INVALID_STATE;
|
||||
if (!backend->running || !backend->transition_frames ||
|
||||
__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
||||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE) ||
|
||||
__atomic_load_n(&backend->process_result, __ATOMIC_RELAXED) != (unsigned int)XTC_OK)
|
||||
return xtc_backend_stop(backend);
|
||||
__atomic_store_n(&backend->stop_requested, 1U, __ATOMIC_RELEASE);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
/* Control thread: acknowledgement is not quiescence until stop succeeds. */
|
||||
void xtc_backend_collect(xtc_backend *backend)
|
||||
{
|
||||
if (backend != NULL)
|
||||
if (backend != NULL) {
|
||||
if (__atomic_load_n(&backend->stop_done, __ATOMIC_ACQUIRE)) {
|
||||
if (xtc_backend_stop(backend) != XTC_OK)
|
||||
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
|
||||
}
|
||||
xtc_realtime_collect(backend->engine);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -577,6 +632,7 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status
|
|||
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->stopping = (int)__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE);
|
||||
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);
|
||||
|
|
@ -968,4 +1024,15 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc
|
|||
(void)remote;
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
}
|
||||
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
|
||||
{
|
||||
(void)backend; (void)frames;
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
}
|
||||
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
||||
{
|
||||
(void)backend;
|
||||
return XTC_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
38
src/dsp/ramp.c
Normal file
38
src/dsp/ramp.c
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include "xtc/xtc_dsp.h"
|
||||
#include <math.h>
|
||||
|
||||
float xtc_dsp_finite(float sample)
|
||||
{
|
||||
return !isfinite(sample) || fpclassify(sample) == FP_SUBNORMAL ? 0.0f : sample;
|
||||
}
|
||||
xtc_result xtc_gain_ramp_reset(xtc_gain_ramp *ramp, float value)
|
||||
{
|
||||
if (!ramp || !isfinite(value)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
ramp->current = ramp->target = value;
|
||||
ramp->remaining = 0;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_gain_ramp_target(xtc_gain_ramp *ramp, float target, size_t frames)
|
||||
{
|
||||
if (!ramp || !isfinite(target) || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
|
||||
ramp->target = target;
|
||||
ramp->remaining = frames;
|
||||
if (!frames) ramp->current = target;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_gain_ramp_process(xtc_gain_ramp *ramp, float *left, float *right, size_t frames)
|
||||
{
|
||||
size_t i;
|
||||
if (!ramp || !left || (right && left == right) || frames > 65536)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < frames; ++i) {
|
||||
if (ramp->remaining) {
|
||||
ramp->current = (float)((double)ramp->current +
|
||||
((double)ramp->target - ramp->current) / (double)ramp->remaining);
|
||||
if (!--ramp->remaining) ramp->current = ramp->target;
|
||||
}
|
||||
left[i] = xtc_dsp_finite(left[i] * ramp->current);
|
||||
if (right) right[i] = xtc_dsp_finite(right[i] * ramp->current);
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#include "xtc/xtc_dsp.h"
|
||||
#include "../graph/graph_internal.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -234,8 +235,7 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
|
|||
if (spec.type == XTC_NODE_GAIN)
|
||||
sample *= spec.value;
|
||||
}
|
||||
if (!isfinite(sample))
|
||||
sample = 0.0f;
|
||||
sample = xtc_dsp_finite(sample);
|
||||
buffer[frame] = sample;
|
||||
if (fabsf(sample) > peak)
|
||||
peak = fabsf(sample);
|
||||
|
|
@ -269,7 +269,7 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
|
|||
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;
|
||||
destination[frame] = xtc_dsp_finite(sample);
|
||||
}
|
||||
}
|
||||
plan->position += frame_count;
|
||||
|
|
|
|||
|
|
@ -366,7 +366,12 @@ void MainWindow::open_backend()
|
|||
if (result != XTC_OK)
|
||||
report_error("Open JACK failed", result);
|
||||
else {
|
||||
result = xtc_backend_submit(backend_, xtc_project_graph(project_.get()));
|
||||
xtc_backend_status format{};
|
||||
result = xtc_backend_get_status(backend_, &format);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_backend_set_transition_frames(backend_, format.sample_rate / 200U);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_backend_submit(backend_, xtc_project_graph(project_.get()));
|
||||
if (result != XTC_OK) {
|
||||
ready_ = false; // Handle is retained for close/reconnect; graph was not installed.
|
||||
report_error("Submit graph failed", result);
|
||||
|
|
@ -410,7 +415,7 @@ void MainWindow::start_engine()
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Stops the audio engine, finalizing any active recording.
|
||||
* @brief Requests a graceful playback stop, or immediately finalizes recording.
|
||||
*
|
||||
* Does nothing when disconnected. Failures are reported in the window.
|
||||
*/
|
||||
|
|
@ -418,7 +423,7 @@ void MainWindow::stop_engine()
|
|||
{
|
||||
if (!ready_)
|
||||
return;
|
||||
xtc_result result = xtc_backend_stop(backend_);
|
||||
xtc_result result = recorder_ ? xtc_backend_stop(backend_) : xtc_backend_request_stop(backend_);
|
||||
if (result != XTC_OK)
|
||||
report_error("Stop engine failed", result);
|
||||
else {
|
||||
|
|
@ -431,7 +436,7 @@ void MainWindow::stop_engine()
|
|||
|
||||
xtc_result MainWindow::pause_playback()
|
||||
{
|
||||
xtc_result result = recorder_ || !ready_ ? XTC_ERR_INVALID_STATE : xtc_backend_stop(backend_);
|
||||
xtc_result result = recorder_ || !ready_ ? XTC_ERR_INVALID_STATE : xtc_backend_request_stop(backend_);
|
||||
if (result != XTC_OK)
|
||||
report_error("Pause failed; use Stop to finish recording or Reconnect audio", result);
|
||||
else
|
||||
|
|
@ -504,6 +509,7 @@ void MainWindow::refresh_status()
|
|||
indicator_->labelcolor(fl_rgb_color(255, 155, 120));
|
||||
set_label(graph_state_, "Graph: offline — edits stored locally; Reconnect to play");
|
||||
} else {
|
||||
xtc_backend_collect(backend_); // Completes an acknowledged graceful stop before reading status.
|
||||
xtc_backend_status status = {};
|
||||
xtc_result result = xtc_backend_get_status(backend_, &status);
|
||||
if (result != XTC_OK) {
|
||||
|
|
@ -523,6 +529,7 @@ void MainWindow::refresh_status()
|
|||
const char *state = status.server_lost ? "Audio: server lost — Reconnect required"
|
||||
: status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required"
|
||||
: status.process_result != XTC_OK ? "Audio: processing error"
|
||||
: status.stopping ? "Audio: stopping — fading to silence"
|
||||
: recorder_ ? "Audio: recording JACK input — Stop to finish"
|
||||
: running_ ? "Audio: running — modular graph"
|
||||
: "Audio: ready / stopped";
|
||||
|
|
@ -546,7 +553,6 @@ void MainWindow::refresh_status()
|
|||
else
|
||||
set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped");
|
||||
}
|
||||
xtc_backend_collect(backend_);
|
||||
}
|
||||
if (can_play)
|
||||
play_->activate();
|
||||
|
|
@ -683,7 +689,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 11: create, arrange, save, reopen and play.",
|
||||
fl_message("%s %s\nModular audio workstation\nPhase 12: timing, transitions and audio quality.",
|
||||
XTC_PROJECT_NAME, XTC_VERSION_STRING);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,8 +148,9 @@ class MainWindow : public Fl_Double_Window {
|
|||
/** Creates a fresh untitled project; the File menu checks unsaved changes first.
|
||||
* On failure the current document remains owned by the window. */
|
||||
xtc_result new_project();
|
||||
/** Stops playback, retaining its exact sample position. Recording must be
|
||||
* finalized with Stop instead. Returns the backend failure, if any. */
|
||||
/** Requests a playback fade/stop; the timer completes deactivation after
|
||||
* callback acknowledgement. Recording must use immediate Stop instead.
|
||||
* Returns request acceptance or the backend failure. */
|
||||
xtc_result pause_playback();
|
||||
/** Stops playback and seeks to zero. Refuses during recording. A failed
|
||||
* stop/seek is reported without moving the displayed cursor to zero. */
|
||||
|
|
@ -332,7 +333,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
*/
|
||||
void start_engine();
|
||||
/**
|
||||
* @brief Stops the audio engine, finalizing any active recording.
|
||||
* @brief Requests a graceful playback stop, or immediately finalizes recording.
|
||||
*
|
||||
* Does nothing when disconnected. Failures are reported in the window.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include "../mixer/mix_internal.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -24,6 +25,7 @@ typedef struct {
|
|||
} voice;
|
||||
|
||||
struct xtc_midi_plan {
|
||||
size_t fade_frames;
|
||||
xtc_mix_snapshot mix;
|
||||
span notes[XTC_PROJECT_MAX_NOTES];
|
||||
size_t count;
|
||||
|
|
@ -281,6 +283,13 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position)
|
|||
* @param right Destination right-channel buffer; NULL selects mono rendering.
|
||||
* @param frames Number of audio frames.
|
||||
*/
|
||||
xtc_result xtc_midi_plan_set_fade_frames(xtc_midi_plan *plan, size_t frames)
|
||||
{
|
||||
if (!plan || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
|
||||
plan->fade_frames = frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *right, size_t frames)
|
||||
{
|
||||
size_t i, j;
|
||||
|
|
@ -294,6 +303,13 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
|
|||
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);
|
||||
float sample = (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0);
|
||||
if (p->fade_frames) {
|
||||
uint64_t age = position + j - s->start;
|
||||
uint64_t tail = s->end - 1U - (position + j);
|
||||
uint64_t distance = age < tail ? age : tail;
|
||||
if (distance < p->fade_frames)
|
||||
sample *= (float)distance / (float)p->fade_frames;
|
||||
}
|
||||
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);
|
||||
|
|
@ -356,6 +372,10 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons
|
|||
cursor = next;
|
||||
}
|
||||
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor);
|
||||
for (i = 0; i < frames; ++i) {
|
||||
out[i] = xtc_dsp_finite(out[i]);
|
||||
if (right) right[i] = xtc_dsp_finite(right[i]);
|
||||
}
|
||||
p->next_position = position + frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include "xtc/xtc_dsp.h"
|
||||
#include "xtc/xtc_native.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -139,8 +140,7 @@ xtc_result xtc_native_process(xtc_native *p, const xtc_native_block *b)
|
|||
return r;
|
||||
}
|
||||
for (i = 0; i < b->frames; ++i)
|
||||
if (!isfinite(b->output[i]))
|
||||
b->output[i] = 0;
|
||||
b->output[i] = xtc_dsp_finite(b->output[i]);
|
||||
p->next_position = b->position + b->frames;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,3 +202,13 @@ window, reopens in another window, compares stereo rendering exactly, verifies
|
|||
playhead/meters and pause/resume/seek/home, and continues editing/saving.
|
||||
Mock GUI tests inject failed stop/seek/project preparation and recording guards.
|
||||
See [WORKFLOW.md](../../docs/WORKFLOW.md) for acceptance details and limitations.
|
||||
|
||||
## Phase 12 transitions
|
||||
|
||||
Backend fault tests verify start/stop ramp samples, repeated requests, callback
|
||||
completion, silent/held blocks before collection and deactivation error/retry.
|
||||
GUI tests keep immediate mocked stop semantics; real workflow/live GUI tests
|
||||
wait for asynchronous fade acknowledgement. `test_quality` in the unit suite
|
||||
checks exact source envelopes/timing and variable blocks, including tempo
|
||||
snapshot changes. See [AUDIO_QUALITY.md](../../docs/AUDIO_QUALITY.md) for commands,
|
||||
latency limitations and the pending listening checklist.
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@ xtc_result xtc_backend_close(xtc_backend *backend)
|
|||
}
|
||||
xtc_result xtc_backend_start(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) { handle.running = 1; engine_status.active_revision = engine_status.submitted_revision; } return start_result; }
|
||||
xtc_result xtc_backend_set_transition_frames(xtc_backend *, size_t frames)
|
||||
{ CHECK(frames == 240); return XTC_OK; }
|
||||
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
||||
{ return xtc_backend_stop(backend); }
|
||||
xtc_result xtc_backend_stop(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle); ++stops; if (stop_result == XTC_OK) handle.running = 0; return stop_result; }
|
||||
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *status)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ int main(int argc, char **argv)
|
|||
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
||||
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();
|
||||
stop->do_callback(); pump();
|
||||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
uint64_t paused = timeline->position(); pump();
|
||||
CHECK(timeline->position() == paused);
|
||||
|
|
@ -93,7 +93,7 @@ int main(int argc, char **argv)
|
|||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running — modular graph") != nullptr);
|
||||
stop->do_callback();
|
||||
stop->do_callback(); pump();
|
||||
char directory[] = "/tmp/xtc-live-record-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
|
||||
std::string take = std::string(directory) + "/take.wav";
|
||||
std::string song = std::string(directory) + "/song.xtc";
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ static std::vector<float> render(const xtc_project *project)
|
|||
CHECK(xtc_graph_compile(xtc_project_graph(project), 48000, 256, &graph) == XTC_OK);
|
||||
CHECK(xtc_audio_plan_create(project, 48000, &audio) == XTC_OK);
|
||||
CHECK(xtc_midi_plan_create(project, 48000, &midi) == XTC_OK);
|
||||
CHECK(xtc_audio_plan_set_fade_frames(audio, 240) == XTC_OK);
|
||||
CHECK(xtc_midi_plan_set_fade_frames(midi, 240) == XTC_OK);
|
||||
std::vector<float> result;
|
||||
for (uint64_t pos = 0; pos < 96000; pos += 256) {
|
||||
float al[256], ar[256], ml[256], mr[256], left[256], right[256];
|
||||
|
|
@ -113,7 +115,7 @@ int main(int argc, char **argv)
|
|||
CHECK(arranged.start_tick == 1920 && arranged.length_tick == 3840);
|
||||
CHECK(window.import_audio(wave.c_str()) == XTC_OK);
|
||||
expected = render(timeline->project());
|
||||
CHECK(expected[0] > 0); // Imported audio is routed without graph-file edits.
|
||||
CHECK(expected[0] == 0 && expected[2] > 0); // Imported audio is routed without graph-file edits.
|
||||
CHECK(expected[2 * 48001] != 0); // Arranged notes also reach the output.
|
||||
CHECK(xtc_graph_get_counts(xtc_project_graph(timeline->project()), &nodes, &edges) == XTC_OK);
|
||||
CHECK(nodes == 3 && edges == 2); // Audio and instrument -> output.
|
||||
|
|
@ -144,12 +146,13 @@ int main(int argc, char **argv)
|
|||
auto *right_meter = dynamic_cast<Fl_Progress *>(button(window, "Output R"));
|
||||
CHECK(left_meter && right_meter && left_meter->value() > 0 && right_meter->value() > 0);
|
||||
CHECK(window.pause_playback() == XTC_OK);
|
||||
pump(); // Fade completion is acknowledged by the GUI timer.
|
||||
CHECK(left_meter->value() == 0 && right_meter->value() == 0);
|
||||
uint64_t paused = timeline->position(); pump();
|
||||
CHECK(timeline->position() == paused);
|
||||
press(window, "Play"); pump();
|
||||
CHECK(timeline->position() > paused);
|
||||
press(window, "Stop");
|
||||
press(window, "Stop"); pump();
|
||||
paused = timeline->position(); pump(); CHECK(timeline->position() == paused);
|
||||
press(window, "To start"); CHECK(timeline->position() == 0);
|
||||
press(window, "Add MIDI clip");
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ int main(void)
|
|||
CHECK(xtc_backend_set_recorder(backend, recorder) == XTC_OK);
|
||||
CHECK(xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_set_recorder(backend, NULL) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(xtc_backend_request_stop(backend) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(xtc_backend_prepare_project(backend, p) == XTC_ERR_INVALID_STATE);
|
||||
for (i = 0; i < 32; ++i) input[i] = i % 2 ? -0.5f : 0.25f;
|
||||
CHECK(process_cb(32, context) == 0);
|
||||
|
|
@ -318,6 +319,39 @@ int main(void)
|
|||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_right == 0);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK); xtc_project_destroy(p);
|
||||
}
|
||||
{
|
||||
uint64_t held;
|
||||
config.channel_count = 2;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
|
||||
CHECK(xtc_backend_set_transition_frames(backend, 65537) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(xtc_backend_set_transition_frames(backend, 4) == XTC_OK);
|
||||
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
|
||||
for (i = 0; i < 128; ++i) input[i] = 1;
|
||||
CHECK(xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_set_transition_frames(backend, 4) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(process_cb(4, context) == 0);
|
||||
CHECK(output[0] == 0.125f && output[3] == 0.5f && output_right[3] == output[3]);
|
||||
CHECK(xtc_backend_request_stop(backend) == XTC_OK);
|
||||
CHECK(process_cb(2, context) == 0 && output[0] == 0.375f && output[1] == 0.25f);
|
||||
CHECK(xtc_backend_request_stop(backend) == XTC_OK); /* Must not restart ramp. */
|
||||
CHECK(process_cb(2, context) == 0 && output[0] == 0.125f && output[1] == 0);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.running && status.stopping);
|
||||
held = status.sample_position;
|
||||
CHECK(process_cb(32, context) == 0 && output[0] == 0 && output[31] == 0);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == held);
|
||||
deactivate_fail = 1;
|
||||
xtc_backend_collect(backend);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.running && status.process_result == XTC_ERR_IO);
|
||||
deactivate_fail = 0;
|
||||
xtc_backend_collect(backend);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running && !status.stopping);
|
||||
CHECK(xtc_backend_start(backend) == XTC_OK && process_cb(4, context) == 0 && output[3] == 0.5f);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK); /* Immediate escape stays available. */
|
||||
CHECK(xtc_backend_set_transition_frames(backend, 0) == XTC_OK);
|
||||
CHECK(xtc_backend_start(backend) == XTC_OK && xtc_backend_request_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK);
|
||||
}
|
||||
CHECK(closed >= 104);
|
||||
CHECK(xtc_backend_close(NULL) == XTC_OK);
|
||||
xtc_graph_destroy(invalid_graph);
|
||||
|
|
|
|||
201
tests/unit/test_quality.c
Normal file
201
tests/unit/test_quality.c
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <assert.h>
|
||||
#include <float.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void timing(void)
|
||||
{
|
||||
unsigned int rates[] = {1, 8000, 44100, 48000, 96000, 192000, 768000};
|
||||
unsigned int tempos[] = {20000, 120000, 123457, 300000};
|
||||
xtc_project *p = NULL;
|
||||
size_t r, t;
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
for (r = 0; r < sizeof(rates)/sizeof(*rates); ++r)
|
||||
for (t = 0; t < sizeof(tempos)/sizeof(*tempos); ++t) {
|
||||
uint64_t tick, sample, back, expected, previous = 0;
|
||||
assert(xtc_project_set_tempo(p, tempos[t]) == XTC_OK);
|
||||
for (tick = 0; tick <= XTC_PROJECT_MAX_TICK; tick += 9973) {
|
||||
expected = tick * rates[r] * UINT64_C(60000) / ((uint64_t)tempos[t] * 960);
|
||||
assert(xtc_project_tick_to_sample(p, tick, rates[r], &sample) == XTC_OK);
|
||||
assert(sample == expected && sample >= previous);
|
||||
previous = sample;
|
||||
assert(xtc_project_sample_to_tick(p, sample, rates[r], &back) == XTC_OK);
|
||||
assert(back == sample * tempos[t] * 960 / ((uint64_t)rates[r] * 60000));
|
||||
}
|
||||
assert(xtc_project_tick_to_sample(p, XTC_PROJECT_MAX_TICK, rates[r], &sample) == XTC_OK);
|
||||
}
|
||||
{
|
||||
uint64_t unchanged = 123;
|
||||
assert(xtc_project_sample_to_tick(p, UINT64_MAX, 1, &unchanged) == XTC_ERR_CAPACITY && unchanged == 123);
|
||||
assert(xtc_project_set_tempo(p, 0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
}
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
static void ramps(void)
|
||||
{
|
||||
xtc_gain_ramp a, b;
|
||||
float whole[257], split[257], right[257];
|
||||
size_t i;
|
||||
assert(xtc_gain_ramp_reset(&a, 0) == XTC_OK);
|
||||
b = a;
|
||||
assert(xtc_gain_ramp_target(&a, 2, 17) == XTC_OK);
|
||||
assert(xtc_gain_ramp_target(&b, 2, 17) == XTC_OK);
|
||||
for (i = 0; i < 257; ++i) whole[i] = split[i] = right[i] = 1;
|
||||
assert(xtc_gain_ramp_process(&a, whole, right, 257) == XTC_OK);
|
||||
for (i = 0; i < 257; ++i)
|
||||
assert(xtc_gain_ramp_process(&b, split + i, NULL, 1) == XTC_OK);
|
||||
assert(memcmp(whole, split, sizeof(whole)) == 0 && memcmp(whole, right, sizeof(whole)) == 0);
|
||||
assert(whole[16] == 2 && whole[256] == 2); /* Headroom, no implicit clipping. */
|
||||
assert(xtc_gain_ramp_target(&a, 0, 3) == XTC_OK);
|
||||
whole[0] = whole[1] = whole[2] = 1;
|
||||
assert(xtc_gain_ramp_process(&a, whole, NULL, 3) == XTC_OK && whole[2] == 0);
|
||||
assert(xtc_gain_ramp_target(&a, NAN, 1) == XTC_ERR_INVALID_ARGUMENT && a.target == 0);
|
||||
assert(xtc_dsp_finite(NAN) == 0 && xtc_dsp_finite(INFINITY) == 0);
|
||||
assert(xtc_dsp_finite(nextafterf(0, 1)) == 0 && xtc_dsp_finite(FLT_MIN) == FLT_MIN);
|
||||
assert(xtc_dsp_finite(4) == 4);
|
||||
assert(xtc_gain_ramp_reset(&a, 0) == XTC_OK);
|
||||
assert(xtc_gain_ramp_target(&a, 1, 4) == XTC_OK);
|
||||
whole[0] = 1;
|
||||
assert(xtc_gain_ramp_process(&a, whole, NULL, 1) == XTC_OK && whole[0] == 0.25f);
|
||||
assert(xtc_gain_ramp_target(&a, -0.25f, 2) == XTC_OK);
|
||||
whole[0] = whole[1] = 1;
|
||||
assert(xtc_gain_ramp_process(&a, whole, NULL, 2) == XTC_OK && whole[0] == 0 && whole[1] == -0.25f);
|
||||
assert(xtc_gain_ramp_target(&a, 1, 0) == XTC_OK && a.current == 1);
|
||||
whole[0] = 9;
|
||||
assert(xtc_gain_ramp_process(&a, whole, whole, 1) == XTC_ERR_INVALID_ARGUMENT && whole[0] == 9);
|
||||
}
|
||||
static void events(void)
|
||||
{
|
||||
xtc_project *p = NULL;
|
||||
xtc_midi_plan *a = NULL, *b = NULL;
|
||||
xtc_midi_event events[] = {{7, XTC_MIDI_NOTE_ON, 0, 69, 127}, {33, XTC_MIDI_NOTE_OFF, 0, 69, 0}};
|
||||
float whole[257], split[257], right[257];
|
||||
size_t i;
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK && xtc_midi_plan_create(p, 48000, &b) == XTC_OK);
|
||||
assert(xtc_midi_plan_render_stereo(a, 0, events, 2, whole, right, 257) == XTC_OK);
|
||||
for (i = 0; i < 257; ++i) {
|
||||
xtc_midi_event event;
|
||||
const xtc_midi_event *e = NULL;
|
||||
if (i == 7 || i == 33) { event = events[i == 7 ? 0 : 1]; event.offset = 0; e = &event; }
|
||||
assert(xtc_midi_plan_render(b, i, e, e ? 1U : 0U, split + i, 1) == XTC_OK);
|
||||
}
|
||||
assert(memcmp(whole, split, sizeof(whole)) == 0 && memcmp(whole, right, sizeof(whole)) == 0);
|
||||
for (i = 0; i <= 7; ++i) assert(whole[i] == 0);
|
||||
assert(whole[8] > 0);
|
||||
for (i = 33; i < 257; ++i) assert(whole[i] == 0);
|
||||
/* Equal-offset input events retain caller order: off then on starts a voice. */
|
||||
events[0].offset = events[1].offset = 0;
|
||||
xtc_midi_plan_reset(a);
|
||||
assert(xtc_midi_plan_render(a, 0, events, 2, whole, 257) == XTC_OK && whole[1] == 0);
|
||||
{ xtc_midi_event temp = events[0]; events[0] = events[1]; events[1] = temp; }
|
||||
xtc_midi_plan_reset(a);
|
||||
assert(xtc_midi_plan_render(a, 0, events, 2, whole, 257) == XTC_OK && whole[1] > 0);
|
||||
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b); xtc_project_destroy(p);
|
||||
}
|
||||
static void clips(void)
|
||||
{
|
||||
xtc_project *p = NULL;
|
||||
xtc_audio_plan *a = NULL;
|
||||
xtc_midi_plan *old = NULL, *fresh = NULL;
|
||||
xtc_recorder *rec = NULL;
|
||||
uint64_t track, clip, note;
|
||||
float input[1003], whole[4097], split[4097], midi[4097];
|
||||
size_t i, offset;
|
||||
char dir[] = "/tmp/xtc-quality-XXXXXX", path[256];
|
||||
assert(mkdtemp(dir)); snprintf(path, sizeof(path), "%s/source.wav", dir);
|
||||
for (i = 0; i < 1003; ++i) input[i] = 1;
|
||||
assert(xtc_recorder_create(path, 48000, 1003, &rec) == XTC_OK);
|
||||
xtc_recorder_push(rec, input, 1003);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "Test", &track) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, path, 1, &clip) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(p, 48000, &a) == XTC_OK);
|
||||
assert(xtc_audio_plan_set_fade_frames(a, 8) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(a, 0, whole, 4097) == XTC_OK);
|
||||
for (offset = 0; offset < 4097;) {
|
||||
size_t n = offset % 131 + 1;
|
||||
if (n > 4097 - offset) n = 4097 - offset;
|
||||
assert(xtc_audio_plan_render(a, offset, split + offset, n) == XTC_OK);
|
||||
offset += n;
|
||||
}
|
||||
assert(memcmp(whole, split, sizeof(whole)) == 0);
|
||||
assert(whole[24] == 0 && whole[25] == 0 && whole[26] == 0.125f && whole[33] == 1);
|
||||
/* Fade at the true media end, before the imported clip's rounded-up tick end. */
|
||||
assert(whole[1026] == 0.125f && whole[1027] == 0 && whole[1028] == 0);
|
||||
assert(xtc_audio_plan_render(a, 26, split, 1) == XTC_OK && split[0] == whole[26]);
|
||||
{
|
||||
xtc_audio_plan *resampled = NULL;
|
||||
assert(xtc_audio_plan_create(p, 8000, &resampled) == XTC_OK);
|
||||
assert(xtc_audio_plan_set_fade_frames(resampled, 8) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(resampled, 0, split, 200) == XTC_OK);
|
||||
assert(split[4] == 0 && split[5] == 0.125f && split[170] == 0.125f && split[171] == 0 && split[172] == 0);
|
||||
xtc_audio_plan_destroy(resampled);
|
||||
}
|
||||
|
||||
assert(xtc_project_add_midi_clip(p, track, 1, 100, &clip) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 0, 80, 69, 127, 0, ¬e) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &old) == XTC_OK);
|
||||
assert(xtc_midi_plan_set_fade_frames(old, 8) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(old, 0, NULL, 0, whole, 4097) == XTC_OK);
|
||||
xtc_midi_plan_reset(old);
|
||||
for (offset = 0; offset < 4097;) {
|
||||
size_t n = offset % 131 + 1;
|
||||
if (n > 4097 - offset) n = 4097 - offset;
|
||||
assert(xtc_midi_plan_render(old, offset, NULL, 0, split + offset, n) == XTC_OK);
|
||||
offset += n;
|
||||
}
|
||||
assert(memcmp(whole, split, sizeof(whole)) == 0);
|
||||
assert(whole[25] == 0 && whole[26] > 0 && whole[2024] == 0 && whole[2025] == 0);
|
||||
assert(xtc_project_set_tempo(p, 60000) == XTC_OK);
|
||||
{
|
||||
xtc_audio_plan *new_audio = NULL;
|
||||
assert(xtc_audio_plan_create(p, 48000, &new_audio) == XTC_OK);
|
||||
assert(xtc_audio_plan_set_fade_frames(new_audio, 8) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(new_audio, 0, midi, 4097) == XTC_OK);
|
||||
assert(midi[26] == 0 && midi[50] == 0 && midi[51] == 0.125f);
|
||||
assert(xtc_audio_plan_render(a, 26, split, 1) == XTC_OK && split[0] == 0.125f);
|
||||
xtc_audio_plan_destroy(new_audio);
|
||||
}
|
||||
assert(xtc_midi_plan_create(p, 48000, &fresh) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(fresh, 0, NULL, 0, midi, 4097) == XTC_OK);
|
||||
assert(midi[26] == 0 && midi[51] > 0); /* Newly prepared plan follows new tempo. */
|
||||
xtc_midi_plan_reset(old);
|
||||
assert(xtc_midi_plan_render(old, 0, NULL, 0, split, 4097) == XTC_OK);
|
||||
assert(memcmp(whole, split, sizeof(whole)) == 0); /* Existing snapshot unchanged. */
|
||||
xtc_audio_plan_destroy(a); xtc_midi_plan_destroy(old); xtc_midi_plan_destroy(fresh); xtc_project_destroy(p);
|
||||
assert(unlink(path) == 0 && rmdir(dir) == 0);
|
||||
}
|
||||
static void layouts(void)
|
||||
{
|
||||
size_t sizes[] = {1, 2, 3, 17, 257, 4097, 65535, 65536}, i, j, node;
|
||||
xtc_graph *g = NULL;
|
||||
xtc_plan *plan = NULL;
|
||||
float *l = malloc(65536 * sizeof(float)), *r = malloc(65536 * sizeof(float));
|
||||
assert(l && r);
|
||||
assert(xtc_graph_create(1, &g) == XTC_OK && xtc_graph_add_gain(g, 2, &node) == XTC_OK);
|
||||
assert(xtc_graph_compile(g, 48000, 65536, &plan) == XTC_OK);
|
||||
for (i = 0; i < sizeof(sizes)/sizeof(*sizes); ++i) {
|
||||
for (j = 0; j < sizes[i]; ++j) l[j] = j % 3 == 0 ? nextafterf(0, 1) : j % 3 == 1 ? 2 : INFINITY;
|
||||
assert(xtc_plan_process_stereo(plan, l, NULL, NULL, NULL, NULL, l, r, sizes[i]) == XTC_OK);
|
||||
for (j = 0; j < sizes[i]; ++j) assert(l[j] == (j % 3 == 1 ? 4 : 0) && r[j] == l[j]);
|
||||
assert(xtc_plan_process_stereo(plan, NULL, NULL, NULL, NULL, NULL, l, r, sizes[i]) == XTC_OK);
|
||||
for (j = 0; j < sizes[i]; ++j) assert(l[j] == 0 && r[j] == 0);
|
||||
}
|
||||
assert(xtc_plan_process_stereo(plan, NULL, NULL, NULL, NULL, NULL, l, l, 1) == XTC_ERR_INVALID_ARGUMENT);
|
||||
free(l); free(r); xtc_plan_destroy(plan); xtc_graph_destroy(g);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
timing(); ramps(); events(); clips(); layouts();
|
||||
puts("Timing, sample-offset events, tempo snapshots, fades, ramps and block invariance passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue