168 lines
10 KiB
Markdown
168 lines
10 KiB
Markdown
|
|
# 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.
|