Phase 6: Add WAV clips, graph playback and bounded recording
This commit is contained in:
parent
65640c4aad
commit
8a37090b81
40 changed files with 1450 additions and 69 deletions
|
|
@ -85,3 +85,24 @@ a loaded document before stopping/submitting audio. File operations never run
|
|||
in the callback. See PROJECT_FORMAT.md for schema, bounds and durability.
|
||||
SequencerWidget owns selection, zoom and gestures only; musical range checks,
|
||||
source-preserving trims and ID allocation remain C responsibilities.
|
||||
|
||||
|
||||
## Phase 6 assets and recording
|
||||
|
||||
Audio paths are C clip metadata. A stopped preparation copies range/offset
|
||||
information, deduplicates paths, validates and preloads bounded WAV assets into
|
||||
an independent C audio plan. The backend owns that plan and a preallocated
|
||||
timeline buffer until callback quiescence. A graph audio-source node consumes
|
||||
the buffer, so effects/output routing remain graph operations. All tracks share
|
||||
the mono source in this phase; no audio summation bypasses the graph.
|
||||
|
||||
Arrangement edits stop transport; subsequent Play prepares the new snapshot.
|
||||
Graph-only edits retain live plan publication. Missing assets preserve metadata
|
||||
and render silence, with repair through a C path setter and GUI relinking.
|
||||
Version 2 persistence stores references and explicitly migrates version 1 loads.
|
||||
|
||||
Recording has one callback producer and one control-thread consumer. A bounded
|
||||
SPSC ring publishes complete payload blocks through lock-free indices. Only
|
||||
the consumer writes WAV bytes; finalization follows successful callback stop.
|
||||
No editable project pointer, GUI object or FILE is accessed by the producer.
|
||||
See decision 0005 and AUDIO_FILES.md for format, limits and recovery contracts.
|
||||
|
|
|
|||
|
|
@ -203,3 +203,38 @@ and delayed by polling. No external JACK transport synchronization is provided.
|
|||
Silent clip metadata is control-thread data, never traversed by the callback.
|
||||
Audio/MIDI scheduling will be added in later phases through compiled event/clip
|
||||
plans rather than direct callback access to the editable project.
|
||||
|
||||
|
||||
## Phase 6 audio source and recording
|
||||
|
||||
xtc_audio.h provides control-thread import and immutable preloaded clip plans.
|
||||
The backend prepares a project only while stopped: build audio candidate,
|
||||
compile/submit its graph, then replace the old audio plan without a later
|
||||
fallible step. Preparation failures retain both old plans. Missing/invalid
|
||||
assets produce explicit issue metadata and silence; OOM/capacity fails.
|
||||
Graph-only edits still use the existing pending/retired mechanism while running.
|
||||
|
||||
Each callback renders the plan into a preallocated mono timeline buffer using
|
||||
the authoritative sample position. xtc_realtime_process_audio passes that
|
||||
buffer into the compiled graph; XTC_NODE_AUDIO (output port only) reads it,
|
||||
independently from external JACK input. The old process APIs remain wrappers
|
||||
with a silent timeline. Import creates a source/output route if necessary.
|
||||
No samples or GUI/project arrays are shared with editable state. Clip mixing,
|
||||
rate conversion and graph processing are bounded; see AUDIO_FILES.md for limits.
|
||||
|
||||
A borrowed recorder can be attached/detached only while stopped. Raw JACK input
|
||||
is copied to its SPSC ring before graph processing. Producer and consumer index
|
||||
release/acquire operations protect payload ownership; all atomics are checked
|
||||
always lock-free. The control thread drains to a temporary float32 WAV, then
|
||||
finalizes only after successful stop/close has synchronized the callback.
|
||||
An overflowing ring or unavailable input creates a sticky error; later input is
|
||||
dropped, reported and never silently joined into a continuous-looking take.
|
||||
A partial overflow take is finalized for manual recovery. File errors retain
|
||||
the previous destination and remove unfinished temporaries.
|
||||
|
||||
GUI recording uses a two-second ring and 100 ms control-thread draining, with
|
||||
no worker thread. A GUI stall can overflow it. No callback allocation/free, file
|
||||
I/O, logging, locks or FLTK calls were added. Backend stop failure preserves the
|
||||
borrowed recorder; the caller must not free it before successful quiescence.
|
||||
Live software source/capture acceptance and concurrent ring tests pass; physical
|
||||
recording, latency compensation and disk endurance remain unverified.
|
||||
|
|
|
|||
109
docs/AUDIO_FILES.md
Normal file
109
docs/AUDIO_FILES.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Audio clips and recording (Phase 6)
|
||||
|
||||
## Import and playback
|
||||
|
||||
File/Import WAV (Ctrl+I) loads a WAV into the selected track at the playhead.
|
||||
With no selected track it creates an Audio track. Add region still creates a
|
||||
silent placeholder. Imported clips use the existing move/trim controls.
|
||||
Import stops playback, validates a candidate project, and commits only after
|
||||
successful backend preparation (or locally when disconnected).
|
||||
|
||||
Supported input: little-endian RIFF/WAVE, PCM16/24/32 and IEEE float32, one or
|
||||
two channels, 1..768000 Hz. Stereo is averaged to mono. Compressed WAV, RF64,
|
||||
WAVE_FORMAT_EXTENSIBLE, PCM8, other containers and multichannel files are
|
||||
unsupported. Nonfinite float data and malformed/truncated files are rejected.
|
||||
There is no new dependency: the bounded C decoder/writer is covered by known
|
||||
sample, truncation and invalid-codec tests. See decision 0005.
|
||||
|
||||
Audio is preloaded before playback: maximum 64 distinct paths per plan,
|
||||
16777216 mono frames per asset (64 MiB), 67108864 total frames (256 MiB).
|
||||
Import rejects files over 256 MiB or over 4096 chunks. No streaming decoder,
|
||||
waveform cache, peak drawing or background load worker exists yet. Preloading
|
||||
can briefly block the GUI. Candidate compilation retains the old plan until
|
||||
success; temporary build memory can include both old and candidate buffers.
|
||||
|
||||
The callback renders only overlapping clip ranges from memory. Clips on all
|
||||
tracks are summed at unity gain into the graph's **Audio clips** source node.
|
||||
Import creates that source and connects it to an existing/new output if no
|
||||
audio source exists. Subsequent routing is explicit: disconnect it or route it
|
||||
through gain/meter nodes in Modular Graph. Removing the output silences editor
|
||||
graphs. This source does not monitor JACK input; unconnected ordinary graph
|
||||
inputs retain their earlier external-input behavior. Track faders/pan/mute
|
||||
and individual track routing belong to the mixer phase.
|
||||
|
||||
Linear interpolation converts source sample rate to backend rate. There is no
|
||||
antialiasing filter, time stretching, looping or fades. Musical durations round
|
||||
up to a tick on import; playback boundaries and trims use floor tick-to-sample
|
||||
conversion. A rounded tail pads with silence past the source, and a recording's
|
||||
placement can differ by less than one tick from the capture start sample.
|
||||
Changing tempo changes the musical span, not the source playback rate.
|
||||
|
||||
Arrange edits stop playback. Play preloads and adopts the current arrangement;
|
||||
Stop pauses the sample clock. Graph-only edits can still be submitted during
|
||||
playback. Graph recompilation can reset oscillator phase as before. Playback
|
||||
does not stop automatically at the arrangement end.
|
||||
|
||||
## Assets and recovery
|
||||
|
||||
Version 2 projects store external absolute canonical paths, not sample data.
|
||||
Save the project AND keep its referenced WAV files. Save As changes only the
|
||||
metadata path; it does not copy assets. Moving/deleting a WAV makes its regions
|
||||
silent, preserves their IDs/positions and reports the path and unique issue
|
||||
count on Open/Play. An already loaded plan keeps playing even if its file is
|
||||
removed; the next preparation detects it. Unreadable, malformed and unsupported
|
||||
assets are also reported, rather than substituted with unrelated files.
|
||||
Capacity/OOM is a preparation failure and retains the old plans.
|
||||
|
||||
Select the affected clip, then File/Relink selected audio to choose a replacement.
|
||||
Relinking retains its original timeline/source bounds. The replacement is
|
||||
validated on the next Play; a shorter replacement pads beyond its end with
|
||||
silence. Saving retains unresolved references for later repair. Missing assets
|
||||
are also checked on Open without a working backend, using a temporary plan.
|
||||
Automatic asset search, content hashes and portable project bundles are deferred.
|
||||
|
||||
## Recording
|
||||
|
||||
Record (or Audio/Record WAV, Shift+F5) opens a destination chooser, stops playback,
|
||||
prepares the project, opens a temporary mono float32 WAV and starts JACK.
|
||||
Connect a source to xtc's JACK input using an external patchbay; no physical or
|
||||
software route is created automatically. Without a route the take contains
|
||||
silence. Recording captures raw mono JACK input, not the output/effect mix.
|
||||
|
||||
A preallocated two-second SPSC ring transfers complete callback blocks to the
|
||||
GUI/control thread, which drains it every 100 ms. The producer uses verified
|
||||
lock-free atomics and bounded copies only. It never allocates, logs, takes a
|
||||
mutex, opens/writes files or calls FLTK. A slow disk/blocked GUI can overflow
|
||||
the ring; capture then stops accepting input with a sticky, visible capacity
|
||||
error rather than hiding gaps or compressing time.
|
||||
|
||||
Stop first synchronizes the callback, detaches the recorder, drains remaining
|
||||
samples, writes the final WAV header, flushes/fsyncs/closes and renames the
|
||||
same-directory temporary over the destination. A successful nonempty take is
|
||||
imported at its capture position on the selected track (or a new Recording
|
||||
track), then the document becomes unsaved. Save the project after Stop. Seeking
|
||||
back to the take and pressing Play auditions it. Recording continues existing
|
||||
arrangement playback, but there is no automatic input monitoring, punch, loop,
|
||||
latency compensation, multitrack arming or overdub alignment compensation.
|
||||
|
||||
Graph/arrangement edits, project file actions, reconnect and close are blocked
|
||||
until Stop finishes the take. Deactivation failure retains the recorder for a
|
||||
safe retry. On overflow a finalized partial WAV is retained for manual recovery
|
||||
and is not automatically added. On import/preparation failure the completed
|
||||
take remains at its chosen path with an explicit recovery message. I/O errors
|
||||
are reported; an unfinished temporary is removed on destruction. Recordings
|
||||
are RIFF-limited to 1073741814 frames, but the smaller playback/import asset
|
||||
limit still applies: longer takes may need external splitting before import.
|
||||
There is no crash-recovery journal. The file is mode 0600; parent-directory
|
||||
fsync and preservation of previous permissions are not implemented.
|
||||
|
||||
## Verification
|
||||
|
||||
make test includes deterministic WAV/graph samples, resampling/overlap/trim,
|
||||
malformed inputs, missing/relinked assets, SPSC concurrency/overflow/cleanup,
|
||||
and record -> save/reopen -> exact playback through the backend callback.
|
||||
make test-gui covers the workflow and rollback with a fake backend.
|
||||
make test-gui-jack records/reopens a silent take using real JACK callbacks.
|
||||
make test-jack-audio routes only two software clients: known source -> recorded
|
||||
WAV -> saved/reopened project -> graph playback -> capture assertion.
|
||||
Physical input/output, device latency, slow-disk endurance and server loss
|
||||
during a real recording remain unverified.
|
||||
|
|
@ -128,3 +128,29 @@ and region, moved it one bar and zoomed the canvas. No dependencies were added.
|
|||
Phase 6 will add audio assets and recording. Silent arrangement regions, fixed
|
||||
tempo/4/4, no undo, no persisted GUI layout, and unverified physical-device
|
||||
playback/endurance remain explicit limits.
|
||||
|
||||
|
||||
## Phase 6 verification
|
||||
|
||||
```sh
|
||||
make all test gui debug test-gui test-gui-jack test-jack-audio
|
||||
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
|
||||
make BUILD_DIR=build/tsan CC=cc CFLAGS='-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=thread' LDFLAGS='-fsanitize=thread' build/tsan/test_audio build/tsan/test_realtime
|
||||
./build/tsan/test_audio
|
||||
./build/tsan/test_realtime
|
||||
```
|
||||
|
||||
No new dependency is required for WAV import, recording or playback. New audio
|
||||
sources remain C99; only GUI controls use C++/FLTK. test_audio covers decoder
|
||||
samples, truncations/nonfinite/unsupported input, resampling, trim/overlap,
|
||||
missing assets and concurrent recording. test_backend records known callback
|
||||
input, saves/reopens and checks exact graph output. Project tests cover v1
|
||||
migration and 1024 maximum-length asset paths in a valid v2 file.
|
||||
|
||||
The live test-jack-audio target connects only its software clients and checks
|
||||
record -> WAV -> saved/reopened project -> graph playback. GUI/JACK tests also
|
||||
record, finalize, save and reopen a take without hardware routing. Clang
|
||||
ASan/UBSan passes the core and GUI tests (GUI retains the existing fontconfig-only
|
||||
LSan suppression). ThreadSanitizer passes the concurrent recorder and existing
|
||||
realtime handoff tests. Hardware capture/playback, disk endurance and real server
|
||||
loss during recording remain unverified. See AUDIO_FILES.md.
|
||||
|
|
|
|||
31
docs/GUI.md
31
docs/GUI.md
|
|
@ -26,7 +26,7 @@ from an audio callback. No server is started and no audio ports are connected
|
|||
by the GUI. The initially empty graph produces silence.
|
||||
|
||||
- **Play / F5:** start the engine. **Stop / F8:** deactivate it. **Space:** toggle.
|
||||
These advance/pause the sample-counted playhead; regions are currently silent.
|
||||
These advance/pause the sample-counted playhead; imported audio clips play through their graph source.
|
||||
- **Reconnect / F6:** close the old client and open a fresh stopped client.
|
||||
Use this after server loss or sample-rate changes. A failed close preserves
|
||||
the handle and reports the failure instead of opening another client.
|
||||
|
|
@ -123,10 +123,35 @@ ranges from 24 to 384 pixels/quarter. Names, tempo and 4/4 meter are drawn.
|
|||
A ruler click seeks while stopped (or positions the cursor when disconnected).
|
||||
The 100 ms GUI refresh converts the backend's actual sample counter to ticks;
|
||||
it never integrates wall-clock time. Play resumes and Stop pauses. Reconnect,
|
||||
New and Open reset position. Regions contain no audio or MIDI and cannot
|
||||
produce sound yet; modular graph processing continues as before. The C model
|
||||
New and Open reset position. Add region creates silent metadata; imported WAV clips play through the
|
||||
audio source node (Phase 6 below). MIDI content is not implemented. The C model
|
||||
supports a constant tempo; a tempo map and scheduler are future work.
|
||||
|
||||
Tests cover C-model/persistence boundaries, GUI move/trim gestures and zoom,
|
||||
save/reopen, failed load/submission rollback, sample-derived cursor updates,
|
||||
and live JACK playhead advance/pause.
|
||||
|
||||
|
||||
## Phase 6 audio workflow
|
||||
|
||||
Use File/Import WAV (Ctrl+I) to add an audio clip at the playhead on the selected
|
||||
track, or on a new Audio track. Record (Shift+F5) chooses a WAV destination and
|
||||
captures JACK input until Stop finalizes and adds the take. No input routes are
|
||||
created automatically. Save after Stop; the project references the external WAV.
|
||||
|
||||
Import creates an Audio clips graph source and output route if the source is
|
||||
absent. Modular Graph offers Add audio source for explicit routing through gain
|
||||
or meter nodes. All tracks currently share one mono timeline mix. Arrangement
|
||||
edits stop playback; graph edits remain live outside recording. During recording,
|
||||
edits, file actions, reconnect and close require Stop first.
|
||||
|
||||
Open/Play reports missing, unreadable or unsupported assets, preserving their
|
||||
regions as silence. The backend status shows the unique issue count. Select a
|
||||
clip and use File/Relink selected audio to choose another path; Play validates
|
||||
the replacement. Playback uses preloaded immutable samples; imports and loads
|
||||
can block the GUI while reading, but never the audio callback.
|
||||
|
||||
See AUDIO_FILES.md for supported WAV formats, memory limits, musical placement,
|
||||
recording overflow/partial-take recovery, external asset portability and the
|
||||
software-only acceptance tests. Waveform drawing, input monitoring, recording
|
||||
latency compensation and per-track mixing are not implemented.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Project format v1
|
||||
# Project format v2 (reads v1)
|
||||
|
||||
Phase 5 uses a bounded text format implemented in C99 without a parser library.
|
||||
Phases 5/6 use a bounded text format implemented in C99 without a parser library.
|
||||
The extension is `.xtc`. File operations run exclusively on the control thread.
|
||||
The project owns its graph, tracks and silent arrangement regions; audio files,
|
||||
recording and MIDI content arrive in subsequent phases.
|
||||
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
|
||||
files; MIDI content is a subsequent phase.
|
||||
|
||||
## Schema
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
|
|||
Counts determine the number of subsequent records:
|
||||
|
||||
```text
|
||||
XTC_PROJECT 1
|
||||
XTC_PROJECT 2
|
||||
APP 0 1 0
|
||||
TEMPO 120000
|
||||
IDS 1 1
|
||||
|
|
@ -24,6 +24,7 @@ TRACKS 1
|
|||
TRACK 0 547261636b
|
||||
CLIPS 1
|
||||
CLIP 0 0 3840 2880 960 3840 526567696f6e
|
||||
ASSET -
|
||||
END
|
||||
```
|
||||
|
||||
|
|
@ -34,28 +35,34 @@ END
|
|||
- `GRAPH`: output-node policy (0/1), next node ID, node count, edge count.
|
||||
Nodes retain stable IDs (limited to the host's size_t range). No pointers or
|
||||
compiled state are serialized.
|
||||
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output), and exactly eight
|
||||
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source), and exactly eight
|
||||
hexadecimal digits encoding the IEEE binary32 parameter bits, independent
|
||||
of locale and host byte order. Nonfinite values are rejected.
|
||||
- `EDGE`: source and destination node IDs. Mono port directions are implied
|
||||
by node type; endpoints, duplicates and cycles are validated by the graph API.
|
||||
- `TRACK`: ID and hex-encoded name bytes.
|
||||
- `CLIP`: ID, track ID, start tick, length tick, source offset tick, source
|
||||
length tick, hex-encoded name bytes. Regions are silent metadata. Trimming
|
||||
length tick, hex-encoded name bytes. Trimming
|
||||
preserves source bounds, so a region can re-extend only within its original
|
||||
length. Overlaps are allowed; no audio mixing or scheduling is implied.
|
||||
length. Overlaps are allowed and audio clips mix through the audio source node.
|
||||
- `ASSET`: required after each CLIP in v2; "-" means silent, otherwise
|
||||
hex-encoded absolute path bytes, <=511 bytes after decoding, no embedded NUL
|
||||
or ASCII control characters. Each clip directly references a WAV; no sample
|
||||
data, pointers or DSP state are embedded. Paths can contain spaces.
|
||||
- Names decode to nonempty valid UTF-8 of at most 63 bytes; ASCII control
|
||||
characters and embedded NULs are forbidden. Hex avoids quoting ambiguity.
|
||||
|
||||
Ticks use 960 per quarter note, 3840 per bar. All region endpoints are bounded
|
||||
by 38400000 ticks (10000 bars). Limits: 64 tracks, 1024 clips, 64 graph nodes,
|
||||
4096 edges, 1 MiB input, 511 bytes per record, 12 tokens per record. The reader
|
||||
4096 edges, 2 MiB input, 1535 bytes per record, 12 tokens per record. The reader
|
||||
rejects invalid references, duplicate IDs, inconsistent next IDs, ranges,
|
||||
truncated records, raw control/non-ASCII bytes, and trailing data.
|
||||
|
||||
Unsupported format versions return UNSUPPORTED. Other malformed data returns
|
||||
FORMAT. No implicit migration is attempted; future revisions require explicit
|
||||
migration code and roundtrip tests.
|
||||
FORMAT. The reader explicitly accepts v1 records without ASSET lines, with
|
||||
node types 1..4, migrating their clips to empty asset paths. Saving always writes
|
||||
v2. Unknown versions are rejected without replacing the destination; older
|
||||
applications cannot read v2. Migration and maximum-path roundtrips are tested.
|
||||
|
||||
## Ownership and failures
|
||||
|
||||
|
|
@ -71,5 +78,8 @@ module; public headers contain no POSIX types.
|
|||
|
||||
Saved data reproduces the arrangement and graph, including ID allocation state.
|
||||
Graph node positions, timeline zoom/scroll/selection, transport position and DSP
|
||||
oscillator phase are GUI/session state and are not saved. There are no external
|
||||
asset references yet; missing-asset recovery belongs to Phase 6.
|
||||
oscillator phase are GUI/session state and are not saved. Asset references are
|
||||
external absolute paths, not copied on Save As. Metadata loading preserves
|
||||
missing references; audio-plan preparation reports unreadable/invalid assets
|
||||
and renders them silently. Relinking replaces a selected clip's path without
|
||||
changing its IDs or arrangement. See AUDIO_FILES.md for recovery and limitations.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,16 @@ no undo, asset playback, MIDI or persisted GUI layout are explicit limits.
|
|||
Audio import/playback, recording through bounded buffers, missing-asset
|
||||
handling, no file I/O in callbacks.
|
||||
|
||||
Implemented: bounded PCM16/24/32 and float32 WAV import (mono/stereo downmix),
|
||||
preloaded overlap-summed clip plans with linear rate conversion, and an audio
|
||||
source node using ordinary graph routing. A lock-free SPSC ring transfers JACK
|
||||
input to a control-thread WAV writer; Stop finalizes and adds successful takes.
|
||||
Version 2 metadata stores external asset references and reads v1. Missing assets
|
||||
remain silent with reporting and manual relinking. Core, callback, GUI and live
|
||||
software record/save/reopen/play acceptance tests pass. See AUDIO_FILES.md for
|
||||
memory/format limits, overflow recovery and deferred physical-device validation.
|
||||
No new dependency or callback I/O was introduced.
|
||||
|
||||
## Phase 7 — MIDI and instrument
|
||||
MIDI input/events/clips, simple internal instrument, note-off/panic handling,
|
||||
then a basic piano roll.
|
||||
|
|
|
|||
26
docs/decisions/0005-preloaded-audio-and-recording.md
Normal file
26
docs/decisions/0005-preloaded-audio-and-recording.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 0005: Preloaded WAV clips and control-thread recording consumer
|
||||
|
||||
Status: accepted for Phase 6.
|
||||
|
||||
Use a small bounded C99 WAV decoder/writer for PCM16/24/32 and IEEE float32,
|
||||
mono/stereo, instead of adding libsndfile in this milestone. This avoids a new
|
||||
build/runtime/license dependency, keeps the supported format surface explicit,
|
||||
and enables malformed-input tests. It also limits format coverage; a future
|
||||
codec library needs its own dependency and licensing review.
|
||||
|
||||
Compile musical clip ranges and decoded, shared-per-path mono buffers while
|
||||
stopped. The callback renders the timeline into a preallocated buffer consumed
|
||||
by an explicit graph source node, preserving graph routing rather than mixing
|
||||
outside the graph. The old snapshot remains until candidate preparation succeeds.
|
||||
All track clips currently share one source; per-track routing/mixing is deferred.
|
||||
|
||||
Use a bounded SPSC ring for callback input capture, with release/acquire index
|
||||
publication and a GUI/control-thread consumer. This adds no worker-thread
|
||||
dependency; a two-second ring tolerates normal 100 ms polling but exposes
|
||||
overflow if the GUI or disk stalls. Sticky errors prevent silent time compression.
|
||||
Finalization requires callback quiescence. Future long-session/background I/O
|
||||
work can reuse the ring contract with a dedicated consumer.
|
||||
|
||||
Store external absolute paths in version 2 metadata and retain version 1 loads.
|
||||
Missing/unreadable assets are reported silence, with explicit relinking; portable
|
||||
bundles and content-addressed assets are deferred. See AUDIO_FILES.md for limits.
|
||||
Loading…
Add table
Add a link
Reference in a new issue