186 lines
10 KiB
Markdown
186 lines
10 KiB
Markdown
# Timing, Transitions and Audio Quality
|
||
|
||
Deterministic transition processing and tests are included. 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
|
||
|
||
```
|
||
cmake -S . -B build -DXTC_ENABLE_LIVE_TESTS=ON && cmake --build build --parallel && cmake --build build --target check gui test-reliability test-workflow test-gui test-gui-jack
|
||
cmake -S . -B build -DXTC_ENABLE_LIVE_TESTS=ON && cmake --build build --target test-jack test-jack-audio test-jack-midi test-performance
|
||
cmake --build build --target test-sanitize analyze
|
||
cmake --build build --target quality-preview
|
||
```
|
||
|
||
`test_quality` is part of `cmake --build build --target check`. 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.
|
||
|
||
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**.
|
||
|
||
`cmake --build build --target quality-preview` writes ignored build artifacts `quality-raw.wav` and
|
||
`quality-smooth.wav` in the selected binary directory 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.
|