本文へ移動
cccskills
無料GitHub で公開

stretch

Offline time-stretch / pitch / varispeed — character modes, fine-tune presets, A/B toolkit, and the honest quality state, so an agent can pick a mode, dial it in, and ship a plugin with it.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md20.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Stretch (offline time-stretch / pitch / varispeed)

pulp::signal::OfflineStretch is the non-realtime, max-quality stretch/pitch engine. This skill is the agent-facing guide: which character mode to use, how to fine-tune and share a preset, how to A/B measure, and what's genuinely good vs. still cooking. Headers: core/signal/include/pulp/signal/{offline_stretch,stretch_preset}.hpp. Dev CLI: examples/offline-stretch/stretchcli. Eval toolkit: examples/offline-stretch/eval/.

Most DSP helpers now expose *64 aliases when the underlying class is templated, including OfflineStretch64 and RealtimePitchTimeProcessor64. The offline stretch public config remains control-plane double regardless of sample type; only the sample buffers and internal DSP storage widen for the f64 aliases. Keep quality comparisons explicit about which sample type is under test.

Finite streaming contract

For clip-ready streaming, prepare RealtimePitchTimeProcessor in PitchTimeMode::time_stretch and check its PitchTimePrepareStatus. Non-positive sample rates, channel counts outside the prepared ceiling, and non-positive max_block capacities are rejected before processor state changes. Time-stretch sizing also requires a finite max_time_ratio >= 1; pitch sizing requires a finite, non-negative semitone bound whose derived ratio is finite. Preparation rejects any otherwise-valid bound whose synthesis hop, power-of-two ring, or typed backing-buffer byte size is not representable in the target address space; never rely on a later float-to-int conversion or allocation failure to police capacity. Optional FFT overrides must satisfy the spectral engine's 256–16384 power-of-two window and analysis_hop <= fft_size/2 invariants or preparation rejects them without changing prior state. The realtime processor also stages its spectral-envelope analyzer before it publishes any new configuration or retained processing state. Keep that prepare transactional: an analyzer capacity failure must leave the previously prepared processor usable rather than half-adopting the new geometry. feed() is all-or-nothing: on backpressure, drain available_stretched() with read_stretched() and retry the identical input block. Never advance a decoder on a rejected feed.

At source EOF, call finalize() repeatedly, alternating with output reads, until it returns complete. Finalization seals input, advances at most one prepared block per call, preserves the overlap-add tail, and publishes the frame-map-derived duration (exactly round(input_frames * ratio) for a constant ratio). A later feed returns input_closed; reset starts a new stream.

input_priming_samples() is how much source must be queued before the first final output becomes readable. It is not leading silence: output_alignment_samples() is zero. output_free_space() is an advisory scheduling value; the typed feed/finalize result remains authoritative. These methods allocate nothing after prepare(). Timeline TimeConform::Stretch uses this finite path only during program compilation. The compiler first materializes the exact source slice at the compiled timeline sample rate, updates ratios only at analysis boundaries from the authored tempo map, and accepts only an exact target-frame result. It then publishes immutable audio that the renderer reads 1:1. This offline path uses the scalar double finite builder and a bounded final conversion to float so separate compiles remain bit-identical within one build/platform. Compiler work-block size is deliberately absent from cache identity and must not change output. This is offline compilation, not the separate live/realtime stretch lifecycle.

Higher engine layers use the public audio-domain boundaries FiniteTimeStretchJob and RealtimeTimeStretchProcessor; they must not expose pulp::signal types through playback headers. The realtime facade is prepared on the control thread, reports its complete retained-state charge and fixed causal delay, and is allocation-free after successful preparation. Timeline live playback uses it only through a prepared RealtimeStretchProgramRuntime, with one compensated latency shared by Stretch, parallel non-Stretch audio, MIDI 1, and UMP output. Keep both audio implementation translation units in the native no-exception mirror and the WAM/WebCLAP portable dependency inventories.

Source/filter analysis is a prepared control-thread facility. Reach it through pulp/signal/source_filter_analysis.hpp (also exported by signal.hpp) for a cepstral envelope, LPC coefficients, Schur stability, or an all-pole response; formant extraction is deliberately not part of this contract. Preparation is transactional and reports typed size, configuration, or allocation failures. The analysis calls themselves use only retained scratch after a successful prepare. SpectralEnvelopeShifterT likewise rejects a non-finite or negative max_gain_db without changing its prior prepared state. Its runtime warp must be finite and strictly positive: zero, negative, NaN, or infinity is a fail-closed no-op on the frame, never a value to clamp or pass to an index calculation. Preserve that guard before any scratch or frame mutation when changing the stretch spectral path.

For reproducible offline artifacts, prefer FiniteStretchBuilder64 from finite_stretch_builder.hpp over open-coding the stream loop. Keep the render double-precision through completion, then seal to float once. This uses the portable scalar FFT path; platform float FFTs are numerically equivalent but do not promise bit-identical output across fresh instances. The typed capacity checks charge sizeof(double) for every 64-bit caller-owned plane. FiniteStretchBuilder remains available for float/RT-oriented uses. Its caller-owned planar buffers must remain stable for the builder lifetime. One step() does at most one bounded feed, drain, or finalize unit; rejected feeds are retained and retried byte-for-byte after a drain. Success means the requested target frame count was produced exactly; short and long natural renders are typed failures. Output planes must be distinct and non-overlapping with every input plane, because incremental writes otherwise corrupt unread source frames. Shared read-only input planes are valid. Long streams keep absolute synthesis position in a checked signed integer and only the bounded fractional-hop residual in floating point. Do not restore an ever-growing double accumulator: sub-sample hop fractions disappear around large absolute positions even before integer spacing exceeds one.

The non-obvious ramp rule is that ratios belong to analysis-frame boundaries, not decoder or host blocks. The builder's ratio_at_input_frame callback is invoked at those boundaries even when max_block changes, and bounded finalization preserves the same rule through EOF padding while holding the endpoint ratio. Keep the callback noexcept and allocation-free. Do not replace this with block-start ratio updates: that makes the rendered ramp depend on the chosen work quantum.

Build + run

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DPULP_ENABLE_GPU=OFF
tools/ci/governed-build.sh cmake --build build --target stretchcli
./build/examples/offline-stretch/stretchcli in.wav out.wav --ratio 2.0 --quality 2

--ratio = out_dur/in_dur (2.0 = half speed). Output length is exactly round(in_frames*ratio) — loops stay bar-locked. --pitch S shifts S semitones (duration preserved). --bpm-to T picks the ratio from detected BPM.

The four character modes (--character, or StretchCharacter)

ModeWhatUse forState
clean (default)peak-lock phase vocoder + material-adaptive FFT; time ≠ pitchtonal / melodic / sustained (bass, vocals, pads, mixes)good, shipping
varispeedpitch+time LINKED (sinc resample) + speed-scaled tape head EQ + end-fadetape slow/speed character; sidesteps the drum weaknessgood, shipping
phase_vocoderreserved; renders as clean (the clean engine + --relocate is the punch path)percussion punchscaffold → renders clean; use --relocate for punch
granulargrain/stutter texturetexturescaffold → renders clean

varispeed ≠ time-stretch: it changes pitch AND tempo together (like tape/vinyl speed), so it has no phase-vocoder artifacts. Slowing down warms + dulls (head-gap HF loss + low-mid bump, scaled by log2(ratio)); speeding up brightens. Exact identity at ratio 1. Demo: --character varispeed vs --repitch (plain resample, no EQ) vs --character clean (time-stretch).

Honest quality state (don't oversell)

  • Bass: excellent — pitch-exact (adaptive 8192 FFT resolves close low partials).
  • Tonal (vocals/pads/mixes): very good — matches/beats Rubber Band R3 to the ear.
  • Drums/percussion (clean mode): a phase vocoder smears percussion ATTACKS (keeps only ~70-75% of each transient peak) — the "compressed/less dynamic" artifact. Verbatim transient relocation (below) now restores the attack peaks to ~97%+ of source; enable it for percussion. The PV still smears the decay TAIL (a paradigm limit), so for the most tape-like character varispeed is still an option; full R3-beating drums would need a true transient-separated path.

Engine internals worth knowing (validated, do not regress)

  • Peak-lock (Laroche-Dolson) is the universal phase mode. Vertical phase coherence was tried and RETIRED — it shifted partials ~5-6 Hz (audible "howl" + pitch drift). Do not reintroduce it.
  • Adaptive FFT (recommend_window): percussive→1024/128, bass/low→8192/512, else 4096/512. Override with --fft/--hop.
  • STN noise-morphing is OFF by default — it dulled every material ~400 centroid points (muddy + "wind"). --stn to opt in for noisy textures. Two correctness fixes landed on the OPT-IN path (default stays route_noise_stn=false, settled by the broader A/B): a CAUSAL StnConfig (the morph split applied the mask to the newest pushed frame, but the decomposer evaluated medians on the ring's CENTER frame — lagging ~(time_median-1)/2 frames, so a transient's broadband energy was misrouted into the random-phase noise path and decohered) and a √(8/3) Hann WOLA synthesis-gain (random-phase frames overlap-add incoherently while WOLA normalizes for coherent summation → ~4-5 dB loss). The morpher still dulls tonal/transient material, so it stays off by default; these only improve the opt-in path.
  • Mandatory end-fade on varispeed (tape doesn't hard-cut; a bare resample of a ringing tail ticks).
  • --transient-sens X raises the Röbel reset sensitivity (sharper attacks); off by default — a fine-tune knob. (Measured to REGRESS at 2× on its own — a graft is better; see relocation below.)
  • Transient-reset refractory gate (TransientPhasePolicy::Config::refractory_frames, default 3): the detector fires a phase reset per high-flux frame, so a drum hit's DECAY/ring re-fires it on many consecutive frames — and each full-spectrum reset discards the vocoder's accumulated synthesis-phase lead. Sustained re-firing degenerates the PV toward raw OLA at the synthesis hop, which (a) pitches partials DOWN by the stretch factor and (b) breaks phase coherence — audible as a "blown-out / wobbly" sound on the harder, DEEPER hits (kicks). The gate fires once at the onset then suppresses re-fires for N frames (≈8 ms at hop 128), keeping the attack sharp while killing the over-fire. This is NOT detectable by peak/clip metrics (the output never exceeds ~0.8 full-scale) — it's a perceptual transient distortion; trust ears + a controlled FULL/HALF/OFF reset A/B over a metric here.
  • Verbatim transient relocation (StretchTransientMode::verbatim_relocate, or --relocate): grafts each ORIGINAL attack back onto the PV output, PEAK-ALIGNED, restoring the punch the PV smears (transient peak ~73% → ~97%+ of source across 0.25–4×; tonal/sine = perfect no-op; identity at ratio 1). On the tempo-only spectral path. The three gotchas that make-or-break it (each cost a debugging cycle): (1) the output transient is NOT at oi*ratio — tempo_stretch leaves a ratio-dependent group-delay offset (~205 samples at 2×), so search |out| for the real peak near the nominal position, don't graft at oi*ratio; (2) the energy-window onset detector returns the window START, which LEADS the true peak, so the input-peak search must go FORWARD from the onset (a back-search grafts silence → makes it WORSE); (3) the output-peak search width scales with the stretched onset spacing so it can't lock onto a neighbour at high compression. Offline-only (allocates; runs on the render worker, never the audio thread). Enabled by default in the PulpTempoSampler render path. Crest factor is the WRONG success metric (restoring all peaks moves peak AND RMS together) — use per-transient peak-vs-source + attack slope. (4) Graft only the HIGH band (kReloHpHz, 300 Hz): the PV smears high-frequency attacks but reconstructs sustained LOW frequencies cleanly + continuously. A full-band graft re-injects low-frequency attack energy whose phase can't match the PV body across the short (~1 ms) seam — and a deep kick's period (~15 ms) is longer than the whole graft window — so the seam can't bridge it: a low-frequency discontinuity that "blows out" deep hits, ONLY at stretch (no PV body to mismatch at ratio 1). The graft high-passes both sides and swaps only the high band, leaving the kick body to the PV. Crossover tuned by ear on a real break (180 Hz still blew out; 300 Hz clean + punchy). This artifact is INVISIBLE to peak/clip/wobble metrics (output stays ~0.8 full-scale) — found only via a graft-on/off vs PV-only listening A/B.
  • Spectral output conditioning (match_spectral_rms): a stretch reconstructs BROADBAND material ~3-4 dB QUIET (the WOLA is unity for COHERENT overlap — proven by the spectral-engine tests — but the incoherent broadband residual loses the sqrt(8/3) Hann figure; tonal/peak-locked energy stays at level). Do NOT add a constant at the normalization site (breaks the coherent-unity tests). Instead the spectral paths (tempo / pitch / R+S; NOT identity / repitch / varispeed, which are already level-correct) make up the interior RMS to the input (make-up only) then soft-clip (transparent below 0.9, tanh shoulder to 0.999). The soft-clip (not a whole-buffer peak-scale) is load-bearing: it also tames the verbatim graft's ADDITIVE overshoot (raw peak ~1.2, previously hidden by the sampler's master limiter) without attenuating the whole buffer — so the make-up survives and the engine never emits |x|>1. Test: broadband RMS within ~0.02 dB of source across 0.5–2x, peak ≤ 1.0, a sine NOT inflated.
  • Onset soft-start (restore_onset_head): the PV reconstructs a hard onset at sample 0 from an EMPTY analysis history (the Hann edge is ~0), so the attack ramps up over ~fft/2 samples (~10 ms percussive) — the first ~10 ms is too quiet. The length-lock trim aligns input[0]→output[0] but does NOT remove the ramp, and detect_onsets structurally misses a sample-0 attack (no flux RISE), so relocate_transients can't fix it. Graft the input's leading attack over the head (~10 ms, kHeadMs) with an equal-power crossfade (input[0] is the true sample — no peak search needed). No-op when the input head is silent (a real fade-in, kHeadEps) or the PV didn't lose the attack (kHeadRatio). Runs tempo_stretch → relocate → restore_onset_head → match_spectral_rms (the make-up's interior-RMS window excludes the head; its soft-clip bounds the grafted peak).
    • The head length tracks the window, not a fixed 10 ms. The PV ramp it has to cover is ~fft/2 — ~10 ms only at the 1024 percussive window, but ~42 ms at the 4096 default and ~85 ms at 8192 for sustained/tonal material. A fixed-10 ms head hands the crossfade back to a still-ramping PV and scoops the envelope between the head and the body, so head = min(out, in, max(kHeadMs·sr, engine_.fft_size()/2)). match_spectral_rms floors its interior-RMS edge guard at that same head span (head_guard), so the graft never skews the make-up gain on short (<~80 ms) outputs. Both read engine_.fft_size() (==0 ⇒ engine default 4096).

Fine-tune + share a preset (StretchPreset)

Presets are a layer ON TOP of the core engine (they don't fork it). Tunable knobs: character, fft/hop, transient sensitivity, STN, relocation. Tiny human-editable key = value text spec.

# dial in, then save
stretchcli in.wav out.wav --character varispeed --transient-sens 1.5 --save-preset my.preset
# others load it (later flags still override)
stretchcli in.wav out.wav --ratio 1.5 --preset my.preset

Share my.preset (a few lines of text). API: apply_preset() / capture_preset() in stretch_preset.hpp. Ratio/pitch are the caller's, never the preset's.

A/B measure (the fine-tune loop) — examples/offline-stretch/eval/

pip install -r examples/offline-stretch/eval/requirements.txt   # numpy + soundfile
python examples/offline-stretch/eval/ab_compare.py drum.wav \
  --cli build/examples/offline-stretch/stretchcli --ratios 0.75,1.5,2.0 \
  --configs "clean:--character clean" "tape:--character varispeed"

Metrics: centroid (brightness), onset (punch), peak_hz vs source (pitch fidelity), wobble (pitch stability), spectral-L1 + band balance (EQ match). Metrics are necessary, not sufficient — they repeatedly mislead on subtle artifacts; confirm by ear. To compare against Rubber Band (GPL, NOT bundled), render with your own rubberband and pass --reference <file>.

Tuning methodology (when metrics lie)

Subtle transient/phase artifacts (smear, "wobble", blown-out deep hits) are often INVISIBLE to peak/RMS/clip/AM metrics — output can sit at ~0.8 full-scale and read "clean" while clearly wrong by ear. Tune by ear, with discipline:

  1. Level-match before listening. Normalize every render to the SOURCE's RMS first. The engine can render a few dB quieter than a reference (a known energy leak), and loudness alone decides a blind A/B — match it or your ears lie.
  2. Bisect by ear: isolate ONE variable per render. Don't compare "old vs new everything." Render variants that differ in a single knob and listen to the exact moment that sounds wrong — e.g. graft on / off / different crossover, or a parameter sweep (180/300/500/800). The first clean one is the answer. This is how the high-pass-graft crossover and the refractory window were found.
  3. Reproduce the real path. The sampler renders with --relocate (verbatim graft); a no---relocate A/B tests a DIFFERENT signal path and can read "great" while the app blows out. Match the flags the product uses.
  4. After the ear picks, guard the INVARIANT, not the sound. Land a regression test for the property the fix establishes — "stretch preserves a tone's f0", "the graft doesn't alter the low band (<2% energy)", "a hit resets once not N times", "output never clips" — never a golden-audio compare (too brittle).

Gotchas

  • RealtimePitchTimeProcessor sets SpectralFrameEngineConfig::full_overlap_stream_start = false. Its stretched frame map and input_priming_samples() assume analysis frame k covers input [k * hop, k * hop + fft_size); the engine's default full-overlap grid starts before the stream (first_frame_start() < 0) and would shift every frame ordinal. Flipping it is a stream-contract change, not a one-line fix: the frame map, finalize limits and the stream-contract tests all key on the anchored grid, and its first fft_size - hop output samples keep the tapered start.

  • Don't trust harmonic-clarity (peak/valley) or autocorr-f0-on-drums — both misled during tuning. Use centroid + peak-Hz-vs-source + wobble + the ear.

  • Keep integer types explicit around frame counts on Windows. MSVC's long is 32-bit, while std::llround returns long long; passing that directly into an initializer-list such as std::min<long>({ ... }) can fail the Windows release-path gate as narrowing. Cast or store the rounded value as long before it enters std::min<long> / frame-count lists.

  • A faithful time-stretch must keep peak_hz IDENTICAL to the source at every ratio. If it drifts, something's wrong (that's how the vertical-coherence and the bass-FFT bugs were caught).

  • The test corpus sources hard-cut at full energy, so clean/time-stretch outputs ending abruptly is FAITHFUL, not a bug — don't add fades to the spectral path. (Varispeed is the exception: it fades, because tape doesn't hard-cut.)

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

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.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

android

無料

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.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

ara

無料

Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

Generous-Corp のスキルをすべて見る

このスキルの問題を報告する