aax
無料Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.
日本語の概要は準備中です。原文の説明を表示しています。
Pulp musical/media time primitives, exact beat divisions, tempo and meter maps, transport-range grid projection, inline and order-preserving groove projection, coordinate randomness, streaming cursors, and quantization arithmetic.
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
Use this skill when changing core/timebase, tempo conversion, or the transport
quantizer's beat/frame arithmetic.
kTicksPerQuarter is 705,600. Musical positions are stored and accumulated as
integer TickPosition; samples use integer SamplePosition. Tick-position,
duration, and MonotonicBeat arithmetic saturates at the signed 64-bit
endpoints; it must not invoke signed-overflow UB.
TempoMap and MeterMap are editable document values built through
nonthrowing factories. Their first point is tick zero and points are strictly
ordered. Meter changes must compile on exact preceding bar boundaries.
CompiledTempoMap is immutable and sample-rate-specific. Construct it only
through CompiledTempoMap::compile() and handle TempoMapError; public
throwing construction is forbidden. Its first tempo point
is tick zero, points are strictly ordered, and BPM is finite in [1, 1000].
Tempo ramps are BPM-linear in tick position. Integrate them analytically; do not approximate ramps block-by-block or accumulate floating-point deltas.
Every segment begins at an integer sample anchor. samples_to_ticks() returns
the first canonical tick mapping to that sample when one exists. Exact sample
-> tick -> sample requires a tick grid at least as dense as samples. On a
sparser grid, use resolve_sample() and inspect represented_sample,
absolute_error_samples, and exact; the nearest tick is returned.
Arbitrary tick -> sample -> tick cannot be identity because many ticks share an integer sample. Test monotonicity and canonical-sample preservation instead.
Render-time phase mapping may use fractional_ticks_to_samples() and its
analytic inverse fractional_samples_to_ticks(). Both retain the compiled
integer segment anchors but avoid rounding their input/output domains; keep
ramp round-trip coverage at fractional interior positions.
TempoCursor is the allocation-free playback path. Monotonic sample advances
consume segment transitions once (amortized O(1)); seeks and loop wraps reset
it explicitly. Differential tests must match cold-map canonical results.
CompiledMeterMap uses zero-based bars and exact integer bar/tick conversion.
Tempo changes never affect bar conversion and meter changes never affect
tick/sample conversion. Conversion is total across INT64_MIN..INT64_MAX:
exact results are returned when representable and out-of-range results
saturate without signed-overflow UB.
Keep TransportQuantizer's public behavior stable. Generic beat/frame/grid
arithmetic belongs in <pulp/timebase/quantize.hpp> and the format wrapper
delegates to it.
BeatDivision is an append-only persisted ordinal vocabulary. Append new
values immediately before Count, assign every ordinal explicitly, and keep
beat_fraction() reduced. division_ticks() must fail if a future fraction
is not exactly representable on the 705,600-tick quarter-note lattice.
BeatDivision owns the canonical fraction table. The older
signal::units::Division vocabulary is a compatibility adapter: preserve its
lowercase public spellings and persisted ordinals, map it to BeatDivision,
and derive its beat values from beat_fraction_or(). Append both enums in the
same change and keep exhaustive compile-time and runtime parity coverage in
test_signal_units.cpp; never add a second division formula in signal.
Grid projection consumes explicit document and monotonic anchors in
GridProjectionRange; it does not infer transport state from an unwrapped
sample clock. This matches playback::MasterTransport: pre-loop material uses
its ordinary document interval, loop passes repeat the loop's document sample
interval and tempo image, and seeks change the document anchor without
resetting MonotonicBeat. Ranges and callbacks are half-open, so splitting a
callback cannot duplicate a boundary. Keep capacity and signed-domain failure
explicit and leave caller output untouched on insufficient capacity. Bound
candidate opportunities before entering either timeline- or bar-grid loops;
counting only emitted points leaves incoherent remote-sample ranges able to
burn unbounded callback time.
For document-clock projection, enumerate the rounded end tick as a candidate
and let [timeline_sample_start, timeline_sample_start + frame_count) decide
ownership. Sparse maps can give a valid one-frame transport range equal
rounded tick endpoints (at 1 BPM/48 kHz, tick 0 maps to sample 0 and tick 1 to
sample 4). Returning early drops tick 0 from a 1 + 3 split even though a
four-frame block emits it; excluding the end candidate merely moves the bug.
The next range's sample filter prevents duplication.
A host-beat-mapped transport range carries fractional host tick endpoints and
maps an exact document tick proportionally into output frames. Preserve that
metadata in the dependency-lower grid range and match playback's half-open,
floor-to-frame rule. Range-local proportions are not callback invariant when
a loop boundary's output count was rounded. Retain one HostGridAnchor
(normalized source tick, absolute frame, ticks per frame) across the continuous
session interval, give each range its absolute first frame and loop-pass
document-to-source offset, and floor on that stable clock before clamping to
the owning half-open range. That floor recovers a near-integer delta first,
and must. frame_delta is a cancellation between two tick values routinely
near 1e9, so it keeps the absolute error of the operands, not of the small
difference: an exactly-on-grid event arrives as its integer plus or minus
several ulp of the inputs. Flooring that directly fired 42.2% of on-grid
events one sample early and none late -- one-directional, because floor only
truncates. grid_integral_frame_delta() snaps to the nearest integer when the
delta is within grid_frame_error_budget(), which is computed from the
operands rather than fixed (a constant epsilon is too small at 1e9 ticks and
too loose at small ones) and is capped at kGridMaxSnapFrames. The cap is
the load-bearing half: the raw bound reaches 48 frames at ticks near 2^53 with
one tick per frame, which would round a genuinely early event forward. Past
the cap, keep flooring -- the delta no longer carries an integer's worth of
meaning, so snapping would be guessing. The snap is symmetric on purpose: one
measured range gives 565416.0000000024 in binary64 while exact rational
arithmetic on the same inputs gives 565415.9999999999, one intended integer
either side of the boundary.
Where an event lands between two samples is now a caller's choice.
GridProjectionRequest::placement selects GridPlacement::Floor (the
default, and what Pulp has always done) or Nearest. They are not
interchangeable: Floor never fires late but its error is [0, 1) samples and
always early, mean 0.5; Nearest is unbiased with error <= 0.5 either
way, mean 0.25 — strictly the more accurate placement against true event
time. Keep Floor as the default: it is the conventional slot assignment and
changing it would move every projected event in the product. Select Nearest
when matching a consumer clock that rounds — Forge's tempo_clock_advance
uses llround, and projecting it through Floor moves 27.8% of on-grid
events one sample earlier, versus 0.2% through Nearest.
Nearest is std::round, deliberately not std::nearbyint. nearbyint
honours the current FP rounding mode, a hidden global that defaults to
half-to-even, so it answers 56 for both 55.5 and 56.5 while llround
answers 56 and 57. Matching at the tie is the point of the policy. Measured,
it barely moves the count — so this is a semantics and mode-independence
choice, not an accuracy one.
The epsilon recovery runs before the placement under both policies. Do
not skip it under Nearest on the reasoning that rounding already absorbs a
few ulp: at a tie it does not, and representation error would decide which
way the tie breaks. Initialize the source tick from the first resolved
range in a normalization epoch, not from an absolute host beat that the
transport has already wrapped into document coordinates; reset the anchor on
an epoch or slope discontinuity. Never feed such a range through
CompiledTempoMap::ticks_to_samples(): session tempo is independent of the
document tempo, including on split loop ranges.
project_ratchet_interval() treats the hit count as including the onset and
excludes the later clock boundary. It distributes integer-tick remainders
from the original interval coordinates on every projection; do not advance a
floating-point phase or carry remainder state between callbacks. Half-open
windows must concatenate to the same schedule as one whole-window call. Reject
a hit count greater than the integer-tick span rather than emitting duplicate
positions.
LoopRegion (<pulp/timebase/loop_region.hpp>) is two document positions plus
whether they are in force, and it lives here rather than beside a consumer
because that is the whole of it. playback::LoopRegion is an alias of it and
timeline_editor::UiPlayhead::loop names it directly, so the rung that runs
the transport and the rung that draws the ruler cannot drift apart. enabled
gates wrapping, not existence: a disabled loop keeps its bounds so a view goes
on drawing the region and re-enabling returns the user to it.
A value type both the transport rung and the editor rung need belongs here,
and this module is the only place it can go. playback's floor and
timeline_editor's floor exclude each other; timebase is in both, so it is
their entire intersection apart from platform/runtime. Reaching for a
shared home anywhere else means widening a floor row, which is the thing the
ladder exists to prevent. Before adding one, confirm the intersection still
holds in MODULE_FLOORS (timeline_engine_dependency_floor_check.py) rather
than assuming it.
swing_position() moves the interior boundary of every pair of grid cells to an
exact rational fraction of the pair and rescales the material on either side
onto the new halves. Ticks are integers, so the two halves land on integer
ranges of different lengths and one of them compresses: distinct input ticks
can map to the same output tick. The map is therefore not a bijection, and
claiming a lossless round trip would be wrong. What holds, and what to assert:
kStraightSwing it is the identity on every tick, bit for bit — the
general path produces it, so there is no early-out hiding a bug;unswing_position() recovers a position only to within
grid / (2 * min(pivot, pair - pivot)) + 1/2 ticks. That bound is tight: an
exhaustive sweep of a pair hits its floor at every swung ratio.State the bound rather than exactness. SwingRatio is a {numerator, denominator} rational and not a double on purpose — the result is a
document-visible tick that two machines must agree on, and a float ratio makes
that agreement depend on how one division rounds.
The pivot is clamped inside the pair. A coarse grid does not have enough ticks to express an extreme ratio, and without the clamp the pivot would round onto a pair boundary and erase half the warp while still reporting a valid setting. An invalid grid or ratio makes both functions the identity: the caller validates, and a bad setting must not silently move music.
The canonical authored groove remains timeline::GrooveTemplate: it owns the
name, persistence, independent swing/table grids, strengths, and 0..4x accent
domain. Timebase carries two projections of it, and picking the wrong one is
the mistake this section exists to prevent.
timebase::InlineGrooveProjector is the general one, and the default choice
for a realtime consumer. It is trivially copyable, holds at most
kMaximumInlineGrooveSteps (16) entries inline, and applies no order
validation whatsoever, so it admits any table the canonical model admits --
including one whose adjacent entries lean in opposite directions, which is what
an ordinary groove is. Reach for it whenever a groove table has to live inside a
value a realtime consumer swaps wholesale, or whenever the consumer emits by
displaced position and therefore needs no ordering guarantee.
timebase::OrderPreservingGrooveKernel is the strict subset, and is narrow on
purpose. It refuses at create() any table whose scaled offsets fall by more
than one tick between adjacent entries, which is a far tighter bound than it
reads as: a 16-entry table's admissible offsets must fit a descending staircase
spanning at most 15 ticks, while one percent of a 1/16 entry is already 1,764
ticks. An ordinary authored groove is therefore refused outright, at every
non-zero strength. Reach for it only when a consumer walks events in authored
order and cannot re-sort them, so a reordering feel would be a correctness bug
rather than a feel. Its 1,024-entry inline table also costs 16,432 bytes, which
is what keeps it out of a spec struct; InlineGrooveProjector is 304.
Both keep their numeric domains aligned with timeline (strength 0..1000, velocity 0..4000, offsets smaller than one entry), and neither is a template or a second persistence model -- do not call them one. Timing strength scales both swing and table displacement; zero must be exact identity, including swing. The strict kernel additionally validates the joint swing/table period within the documented bound and rejects either reorder or an unbounded period; the inline projector deliberately has no such pass, and its absence is a stated contract rather than an omission to be repaired.
coordinate_random() is a pure hash of seed plus stable musical coordinates
(tick, lane, loop pass/cycle, stream). Never replace it with callback-local RNG
state. Golden vectors pin the hash, callback-partition tests pin coordinate
selection, and coordinate_chance() multiply-high reduction is checked against
an independent wide/shift-add oracle in tests.
Build and run pulp-test-timebase, pulp-test-timebase-groove-kernels, and the
existing pulp-test-transport-quantizer oracle. Keep at least 1,000,000
deterministic randomized constant/ramp cases, plus tempo-point boundary cases.
Grid tests must adapt real MasterTransport ranges and cover pre-loop playback,
variable-tempo repeated passes, callback partitions, and a seek whose monotonic
anchor is intentionally independent. Also adapt begin_tempo_synced_block()
ranges with fractional host endpoints, a session/document tempo mismatch, and a
loop split. Pin the 180 BPM/48 kHz regression at source beat 0.93748125: tick
29400 stays on absolute frame 1666 for one 4800-frame callback and a 1500 + 3300
split. Exercise signed ceiling at INT64_MIN and prove huge incoherent
tick ranges fail the candidate preflight before enumeration. Include a real
MasterTransport sparse-map whole-vs-partitioned oracle (4 frames versus
1 + 3 and finer splits at 1 BPM/48 kHz).
MonotonicBeat is the strong type for the transport's non-looping musical
clock; the transport owns how it advances while timeline positions seek or
wrap.
__int128 for wide intermediatesMSVC does not support __int128 on any architecture, so it is not a
portability question about 32-bit targets — it fails on windows-x64 and
windows-arm64 alike with error C4235, and a header that uses it cascades
into C4430 / C2059 / C2064 at every use site. Clang and GCC accept it,
so macOS and Linux build clean and the break surfaces on the Windows leg of
the release matrix, where a failure means the GitHub release object and its
assets are never produced.
Use the portable saturating helpers in pulp::timebase::detail
(tick.hpp): saturating_add, saturating_subtract, saturating_multiply.
They are constexpr and total over the full signed 64-bit range.
Two things to know when composing them:
saturating_add(a, saturating_multiply(b, c)) rails the product first and then the sum, where
a wide intermediate would clamp the exact value once. These differ only when
a railed product is pulled back into range by the addend. State the choice in
a comment where it could matter rather than leaving it implicit.kMin is the case that actually bites. It has no positive counterpart,
so negating it must saturate, and kMin / -1 overflows. Any new helper needs
that case handled explicitly before it can divide to test for overflow.When adding a helper here, cross-check it against __int128 as an independent
oracle in the test (guarded by #if defined(__SIZEOF_INT128__)). The test is
not shipped to MSVC, so it may use the wide type that the header may not — that
verifies the portable path against the maths it replaced rather than against
itself. test_timebase.cpp does this over an exhaustive small grid.
TriggerGrid is fixed-capacity authored track×step data, not another clock or
transform chain. Its caller supplies the cycle origin, half-open projection
window, and one stable random word per configured coordinate. Keep projection
allocation-free, step-major then track-major, and block-partition invariant.
Microtiming must remain within the per-step bounds that prevent adjacent steps
from reversing. Groove/swing, coordinate RNG generation, generative pattern
algorithms, transport advancement, and note lifetime remain separate owners.
compiled_tempo_map.hpp exports kMinimumCompiledTempoBpm and
kMaximumCompiledTempoBpm. They were originally file-private constants in
compiled_tempo_map.cpp, and were promoted to the header precisely so a second
consumer could not drift from them.
Any downstream kernel that accepts a BPM (tempo-synced delay, groove, transport
projection) must validate against these symbols rather than writing its own
1.0 and 1000.0. Two copies of a range look identical the day they are
written and diverge silently the first time one side is widened, which produces
a value one component accepts and another rejects with no visible error.
Promoting them is behaviour-neutral and must stay that way: the timebase suite reports 43,136,088 assertions across 27 cases, and that figure should not move when the constants are relocated. If it does, the promotion changed a comparison rather than just its spelling.
pulp-timebase links pulp::foundation, not pulp::runtimecore/timebase uses exactly one runtime header, pulp/runtime/result.hpp, and
that header is a template with no translation unit behind it. So the module
links pulp::foundation, an INTERFACE target over the header-only Result and
queue primitives, and acquires none of runtime's object code.
That is deliberate and load bearing. libpulp-runtime.a compiles an HTTP
client, a WebSocket channel and an HTTPS model downloader, so pulp-runtime
links mbedTLS PRIVATE — and CMake propagates a static library's PRIVATE link
items to every consumer as $<LINK_ONLY:...>. Naming pulp::runtime here puts
libmbedcrypto.a, libmbedtls.a, libmbedx509.a, libeverest.a and libp256m.a back
on the link command of anything that only wanted a tempo map.
The include path is unchanged: pulp::foundation carries runtime's include
directory, so #include <pulp/runtime/result.hpp> still resolves. Only the
link narrows. FORBIDDEN_LINKS in
tools/scripts/timeline_engine_dependency_floor_check.py rejects a restored
pulp::runtime link here, and the clean-consumer fixture measures the same
thing end to end against a staged SDK.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.
日本語の概要は準備中です。原文の説明を表示しています。
Configure, implement, and test Pulp's optional desktop Ableton Link tempo-sync adapter while preserving the developer-supplied SDK, licensing, realtime, latency-compensation, and no-install boundaries.
日本語の概要は準備中です。原文の説明を表示しています。
Maintain Pulp's installed design-time agent capability manifest and public-surface ledger. Use when adding, removing, renaming, or materially changing public audio, MIDI, signal, timebase, or sequence APIs; registering a new algorithm for generators; changing capability support or deprecation state; or repairing agent-capabilities freshness, schema, fingerprint, tombstone, or installed-SDK tests.
日本語の概要は準備中です。原文の説明を表示しています。
Android platform development for Pulp — NDK cross-compilation, Oboe audio, Dawn/Skia GPU rendering, JNI bridge, touch interaction, emulator workflows, and end-to-end smoke validation. Covers build, deploy, debug, and the gotchas discovered during bringup.
日本語の概要は準備中です。原文の説明を表示しています。
Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.
日本語の概要は準備中です。原文の説明を表示しています。
The measurement surface for ALL Pulp DSP and audio-pipeline work — read it BEFORE writing or gating DSP, not only when something already sounds wrong. Covers the C++ harness (signal generators, metrics, assertions, RenderScenario, contracts), the offline Audio Doctor (magnitude/frequency response, THD/THD+N, phase/group delay), and their Python sibling the Audio Quality Lab (tools/audio/quality-lab — null residual + alignment, LTAS log-spectral distance, spectral flux/centroid, HNR, Theil-Sen drift slope, Kaiser-sinc resampling, license-guarded corpus, regression-net ratchet). TRIGGER on AUTHORING work — "build/design an oscillator/filter/synth/effect", "add a DSP module", "what should the acceptance gate be", "how do I measure aliasing / anti-aliasing / alias floor", "null against a reference", "is this DSP correct", "choose a tolerance", "golden/regression corpus for audio", "measure drift or jitter", "A/B two renders" — AND on DEBUGGING work — "is there sound / no audio / I hear nothing", "does this filter/compressor/synth/delay produce the right signal", "prove the DSP / prove the contract", "measure the frequency response", "what's the THD / is it distorting", "what's the group delay / phase response / measured latency", "magnitude response curve", "render a test tone and assert", "audio regression", "64-frame works but 128 is silent", "sample-rate change pitch-shifted it", "describe what's in this buffer", "audio doctor", "compare before/after a DSP refactor". Reach for this BEFORE hand-rolling any FFT, null test, alias measurement, pitch tracker, or golden-render script — most of it already exists in one of the two lanes. Test/tool layer over HeadlessHost — deterministic, no audio device, no speakers. Off the realtime thread entirely.
日本語の概要は準備中です。原文の説明を表示しています。