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

10 KiB
Raw Permalink Blame History

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.