1
0
Fork 0
forked from hanez/xtc
xtc/docs/AUDIO_QUALITY.md

168 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.