Bound the psola/pvoc sample clocks so 32-bit targets never overflow (Stage 0) - #20
Conversation
`long m_n` counted samples in basic_psola and basic_pvoc. long is 32 bits on Cortex-M (ILP32) and Windows (LLP64), so at 48 kHz the count overflowed (signed UB) after ~12.4 h, after which `m_n % ring` went negative and the static_cast<size_t> turned it into an out-of-bounds write. Both clocks are now fixed-width std::int32_t counts that wrap by a multiple of the ring geometry, so they are bounded for unbounded run time and there is no 64-bit division or modulo anywhere in the per-sample path: - pvoc: the clock runs in [0, 6N) and is pulled back by 3N (the overlap-add ring, a multiple of the input ring and the hop). Every ring index and the frame schedule are unchanged, the warm-up guards stay true, and the output is bit-identical across the wrap. Every existing 1 s test already wraps 14 times. - psola: the clock runs in [0, 2 * clock_wrap()) where clock_wrap() is the largest multiple of the ring size not above k_clock_span = 2^18; on the wrap the three fractional mark positions move with it (an exact double subtraction), so only their magnitude changes. No existing test or notebook run (all <= 1 s) reaches the wrap, so they stay bit-identical. Each class gains a documented O(1) test seam, advance_clock_for_testing(), and each battery a test that seeds an elapsed count past 2^31 and runs across the wrap: pvoc pins bit-exact output and whole-run identity (every wrap included); psola pins agreement with an unseeded reference to 1e-6 (measured 2e-9, position rounding only). Both header docstrings state the run-time contract next to latency. Verified locally: Release build with -DTAP_DSP_WERROR=ON, ctest 164/164, clang-format and the clang-tidy mirror clean, and a fingerprint of raw outputs over 30 configurations (both precisions, runs up to 3 s) diffed bit-identical against the previous headers. 32-bit builds are proven only by CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap
left a comment
There was a problem hiding this comment.
Hostile review B (process / downstream)
Scope: ownership, truthfulness of the Verification section against CI logs and the diff, contract/API surface, cross-PR hazards inside wave 1, downstream bumps, commit hygiene. Head reviewed: 907cbf1. Numerics are reviewer A's job; where a numeric fact bears on a process claim I checked it.
What was checked and holds (stated so the findings below are the whole list)
- Ownership.
git diff --stat origin/main...HEADtouches exactlyinclude/tap/dsp/psola.h,include/tap/dsp/pvoc.h,tests/test_psola.cpp,tests/test_pvoc.cpp. No contention file (fft.h, eitherCMakeLists.txt,ci.yml,README.md) is touched. This is the only wave-1 branch of the eight that touches none of them. - CI. 12 check runs = 6 distinct jobs (drift, clang-tidy, linux-ooura, windows-ooura, macos-vdsp, cortex-m55-cmsis compile) each run twice (push + pull_request events). All green. linux-ooura and windows-ooura logs both end
100% tests passed ... out of 164, withpvoc_test.ClockWrapIsBitExactAndCountersDoNotOverflow<float|double>visible in the run. CI configures with-DTAP_DSP_WERROR=ON, so "zero warnings" is implied by the green build. clang-tidy job:clang-tidy clean. - Local reproduction (private scratch dir, fresh clone of
907cbf1). Release +-DTAP_DSP_WERROR=ON: build clean,ctest164/164.clang-format-18 --dry-run --Werroron the four files: clean (note there is no clang-format CI job; only pre-commit and this local check cover it). - Bit-identity, reproduced independently. The PR's fingerprint harness is not committed, so its "30 configurations" claim is not reproducible by a reviewer from the PR. I wrote my own: one driver compiled twice, against the
origin/mainheaders and against this branch's, hashing every output sample over 75 configurations (psola: max_period 900 and 16, ratios 0.25/0.7/1/1.5/4, float and double; pvoc: fft 64/1024/4096, same ratios, formant on/off, float and double; 3 s each). All 75 hashes identical. The pvoc runs cross the new wrap ~45 times each. The psola runs never reach the wrap (first wrap at2 * clock_wrap()= 519,552 samples = 10.8 s for max_period 900), so the psola half of any "bit-identical" claim, mine or the PR's, says nothing about the wrap path; the PR text is honest about that ("the psola runs ... stay below it"). - Arithmetic in the description. 2^31 / 48 kHz = 12.43 h. pvoc first wrap at 6144 then every 3072: a 48,000-sample run wraps 14 times, as stated.
clock_wrap()for max_period 900 = 72 × 3608 = 259,776, a multiple of the ring and ≤ 2^18, as the test asserts. - Notebook.
notebooks/pitchshift.ipynbrunsPsola(900)andPvoc(1024)onseconds=1.0signals (cells 1, 9). pvoc is bit-identical (verified above); psola stays below its wrap. No re-execution owed. Agree with the PR. - Merge hazards.
git merge-tree --write-treeof this branch against each ofwave1-stage1-legs,stage1b-bench,stage2a-tests,stage3a-traits,stage5-spectrum,docs-provenance,capi-fft: all seven merge clean. Onlystage5-spectrumshares files (pvoc.h,test_pvoc.cpp), in disjoint hunks (it rewrites the bin-access code and adds#include <complex>/fft/spectrum.h; this PR touches the clock and adds<cstdint>). I built the mergedstage0 + stage5tree with-DTAP_DSP_WERROR=ONand ran its pvoc + spectrum tests: 32/32 green. - Commit trailers. Single commit, 68-char imperative title, both trailers present (
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>,Claude-Session: ...). Template sections all filled with substance.
Findings
1. should-fix (truthfulness). PR Verification and the commit body both say the 32-bit behaviour "is proven only by CI" / "ILP32/LLP64 behaviour is proven only by CI". Half of that is false. LLP64: yes, windows-ooura runs the full suite including the new tests. ILP32: no CI leg on this PR compiles psola.h or pvoc.h at all. The cortex-m55-cmsis (compile) job configures with -DTAP_DSP_BUILD_TESTS=OFF and builds only --target tap_dsp_fft, which is the two Ooura C files (CMakeLists.txt:21). Nothing proves the ILP32 path; the fixed-width type is the only argument, and it is a good one, but say that. Fix: reword both texts to "LLP64 is proven by the windows leg; no CI leg on this PR compiles these headers for ILP32 (the M55 job builds only the Ooura C); the Stage 1 legs will be the first to. The counter type is fixed-width so the width no longer depends on the toolchain."
2. should-fix (contract documentation). The psola constructor gains a new precondition, assert(max_period < (size_t{1} << 26)), but the docstring still reads only /// @pre max_period >= 16. STYLE.md §4 requires bounds to be expressed "via @pre documentation and debug asserts"; the inline // four rings must fit the int32 sample clock is not the documented contract. The "Notes for the reviewer" line "Contract change. None of the documented contract points move" understates this: it is an additive change (a new precondition plus a new run-time guarantee), not none. Fix: /// @pre 16 <= max_period < 2^26 — ... on the constructor, and reword the reviewer note to "additive: new precondition on max_period, new run-time contract point; no existing point moves".
3. should-fix (public surface). Three new public members: clock_wrap(), k_clock_span (psola) and advance_clock_for_testing() (both). The seam's docstring does say "Not part of the processing contract", which is the minimum; k_clock_span says the opposite in effect: it is a public constant documented as "arbitrary within (longest test run, 2^29)", which is an invitation for a consumer to depend on an arbitrary number. Under CLAUDE.md's discipline the public numbers in these headers are the contract. Fix (cheapest): keep clock_wrap() public (it is a real geometry query), make k_clock_span private (the test only needs clock_wrap() <= 2^18, which can be written as a literal in the test or via a static constexpr in the test file), and add one sentence to the seam docstring: "Not a contract point; may change or disappear without a version note." Optional but consistent: move advance_clock_for_testing behind a friend struct psola_test_access; in a detail namespace so a shipping header exposes no _for_testing mutator to the audio thread at all. README is a contention file owned by another agent this wave, so do not touch it here; hand the README owner a one-line request per shifter ("run time unbounded; 32-bit sample clock wraps by ring geometry") so the "contract summary" CLAUDE.md points consumers at is not silent on the new point.
4. should-fix (downstream process). The PR says "the plan calls for the MuTap/TapTools bump immediately after this merges" and nothing else. Facts: MuTap pins submodules/dsptap at 5ca3b1c, which is exactly this PR's base; MuTap includes only tap/dsp/fft.h and tap/dsp/nn.h (grep over /home/user/MuTap outside the submodule: zero hits for psola/pvoc); MuTap-Max pins MuTap, not DspTap, and has zero hits too. So the MuTap bump is behaviourally a no-op for MuTap but is still owed by the rollout rule (DspTap squash → MuTap pin → MuTap-Max), and the plan's fingerprint harness is supposed to run once per bump. TapTools, the consumer CLAUDE.md names as the one that pins this repo and the plausible actual user of both shifters, is not on disk in this environment, so its exposure to the new max_period < 2^26 precondition and the psola position rounding past 10.8 s is unverified. Fix: after squash, open the MuTap pin-bump PR (or a tracking issue) and link it from this PR; state plainly in the PR that no on-disk consumer includes these headers and that TapTools was not checked.
5. nit (reproducibility). Two evidence items exist only in the PR prose: the 30-configuration fingerprint (I reproduced it, above, but a future reviewer cannot from the PR) and the two mutation results (drop psola's position shift → 0.35; misalign pvoc's wrap by a hop → 2.4). Committing the fingerprint driver here would violate "nothing else rides along", so the right fix is not a file: put the two mutation numbers in the test comments next to the measured values already there, and name the Stage 1 MuTap fingerprint harness as the permanent home of the bit-identity gate.
6. nit (docstring precision). k_clock_span docstring: "The sample clock wraps every clock_wrap() samples". The first wrap fires at 2 * clock_wrap() and every clock_wrap() thereafter (the clock lives in [0, 2 * m_wrap)); likewise "wraps every 5.5 s" is 5.41 s with the first at 10.8 s. One clause fixes it.
7. nit (footer). The PR description carries two footers: the required 🤖 Generated with [Claude Code](...) + session URL, and then a second --- / _Generated by [Claude Code](...)_ block. Keep one.
Merge position. First in wave 1: leftmost in the Part 12 table, touches no contention file, merges clean against all seven siblings. Only stage5-spectrum needs a rebase afterwards and it is textually disjoint (verified by building the merged tree).
Verdict: mergeable after fixes: 1, 2, 3 (all text/docstring-level; 4 is a post-squash action to be linked here).
Generated by Claude Code
tap
left a comment
There was a problem hiding this comment.
Hostile review A (correctness)
Reviewed at 907cbf1 against 5ca3b1c. Branch suite rebuilt in a private dir with -DTAP_DSP_WERROR=ON: 164/164 green; clang-format --dry-run --Werror and clang-tidy 18 (-p on the scratch compile database, repo .clang-tidy) clean on all four changed files. Own fingerprint harness built twice (old headers from git show origin/main:..., new from the branch), 11 configurations.
Findings
1. should-fix — the pvoc seam test compares an object with itself. With warm = 12000 >= clock_wrap(), the reference has already wrapped and sits at m_n = 12000 - 2*3072 = 5856; the seam folds 12000 + k*3072 to 3072 + 12000 % 3072 = 5856. Measured by reading m_n directly (private-access harness): ref.m_n=5856 sub.m_n(after seam)=5856. So ASSERT_EQ(a, b) in tests/test_pvoc.cpp:231 pins nothing, and the PR's "seeded run bit-equal" evidence is vacuous. Confirmed by mutation: with the wrap misaligned by one hop (m_n -= an - m_hop), the identity check fails (1.77) but ref != sub still holds for 0 of 60,000 samples. Structurally, no seam can distinguish "2^31 elapsed" from the equivalent small count, because the class keeps no memory of it; what the test can pin is that m_n and m_n + clock_wrap() are indistinguishable. Fix: apply the seam with warm < clock_wrap() (measured: warm = 2048 gives ref.m_n=2048, sub.m_n=5120, so the two objects differ for the next 1024 samples and ASSERT_EQ becomes a real pin), or drop the seam from this test and rename it for what it does pin (identity reconstruction across the 15 wraps a 60,000-sample run makes). The "counters do not overflow" claim is a bound (m_n < 2 * clock_wrap()), not an observable; say that in the test comment.
2. should-fix — psola is not bit-identical to main after 2 * clock_wrap() samples; state the number. Fingerprint of psola<double>(900), period 320, ratio 1.5, 15 s: first differing sample 521,685 (2 * clock_wrap() = 519,552), max abs diff 3.77e-9 against a 0.352 peak; float profile: first diff at 559,764, max 2.98e-8 (exactly one float ulp at 0.35). All 3 s runs (four psola, five pvoc configurations, float and double) and a 15 s pvoc run are byte-identical, so the PR's "identical in every case" is true only because no psola run exceeded 10.8 s. This is the documented rounding-of-magnitude effect and is acceptable, but the class docstring (psola.h:59-63, "changes nothing but the magnitude of the fractional mark positions") and the PR description should carry the consequence as a number: output diverges from an unbounded-clock reference at the ~1e-8 relative level from 2 * clock_wrap() samples on (10.9 s at 48 kHz for max_period <= 65534). The Stage 1 MuTap fingerprint-harness owner needs this: psola fingerprints must stay under that length or carry a tolerance.
3. nit — docstring bound on position magnitude is false in a corner. psola.h:77 says "2^18 keeps every position below 2^19, i.e. with 34 fractional bits". The true bound is 2 * clock_wrap() - 1 + t + t/r <= 2 * k_clock_span + 5 * max_period. Measured with max_period = 16382 (ring 65,536 divides 2^18 exactly, so clock_wrap() = 262,144): max of m_next_synth over a run crossing the wrap is 556,990 > 524,288, i.e. 33 fractional bits. For max_period = 900 it is 523,802, just under. No effect on exactness (which needs only positions < 2^53); fix the sentence.
4. nit — the exactness argument in shift_clock() is half Sterbenz. psola.h:191-192: "no larger than twice any position" is the lower half of the Sterbenz condition; the upper half (position <= 2 * wrap) is violated by m_next_synth, which reaches 2W + 5P. The subtraction is exact for the other reason already given (wrap is an integer, so a multiple of every position's ulp, and the result's magnitude does not exceed the position's). Drop the "twice" clause so the stated proof is the correct one.
5. nit — constructor comment and the missing symmetric bound in pvoc. psola.h:87 "four rings must fit the int32 sample clock": the clock is bounded by two rings; four rings is the grain-extent bound (positions to ~3.25 rings, place_grain last beyond). Reword. pvoc has no upper-bound assert at all: 6 * fft_size must fit int32 (fft_size <= 2^28), and 3 * m_n_size in the constructor already overflows int at 2^30 (pre-existing). An assert(fft_size <= (size_t{1} << 28)) would make the two headers symmetric.
6. nit — the PR's mutation numbers do not reproduce. A gtest-free mirror of both tests measured 0.78 (PR: 0.35) for dropping the position shift and 1.77 (PR: 2.4) for the one-hop misalignment; a third mutation (m_wrap + 1, not a multiple of the ring) fails at 0.71. All three fail loudly, so the tests can fail; just do not cite the numbers as if they were the tests' own.
Verified, no finding
- Wrap arithmetic. pvoc: pull-back 3N is a multiple of N, N/4 and 3N, so
m_n % N,(m_n+1) % hop,(m_n - N) % 3N,start = m_n + 1 - N(>= 0 by the guard, <= 5N) and both warm-up guards (post-wrapm_n = 3N >= N) are invariant; at the exact wrap sample (m_n = 6N-1) the frame runs withstart = 5Nand the next call's indices equal the unwrapped ones. psola:m_wrapis a multiple of the ring, post-wrapm_n = m_wrap >= ring > latency; the three positions are all in(W, 2W + 5P)at the wrap, so the pull-back is exact in double for everymax_periodthe assert admits (positions < 2^30 << 2^53). max_periodwhere the ring exceeds 2^18:std::max<size_t>(1, ...)givesclock_wrap() == ring. Measuredpsola(70000): ring 280,008,clock_wrap()280,008, 5 wraps over3 * 2Wsamples, all output finite,m_nback at 280,008. Boundaries:psola(65534)ring 262,144 -> wrap 262,144;psola(65535)ring 262,148 -> wrap 262,148.assert(max_period < 2^26)leaves16P + 32 < 2^31, comfortable.- Seam reachability. Both seams' fold
(target < W) ? target : W + (target - W) % Wequals whatprocess()produces aftertargetreal samples for everytarget, and the psola shift is a multiple of the ring by construction; for the test's positions the shift is exact (measured delta 0). The psola test is meaningful:subsits at 455,784 after the seam and wraps 63,768 samples into a 270,336-sample run whilerefnever wraps. - Real-time path.
g++ -O2 -Sofprocess()for both classes: old 3xidivq, new 3xidivl, no libgcc helper, no 64-bit operation; the only 64-bit division is inadvance_clock_for_testing, outside the per-sample path. New per-sample cost is one compare and a rarely-taken branch. - Public surface:
clock_wrap(),k_clock_span,advance_clock_for_testing()are used by the tests, documented as outside the processing contract, and leak into neither README nor capi. No precedent for a_for_testingmember elsewhere in the repo; the maintainer decides whether a test-only public member is house style. - Notebook: every psola/pvoc signal is 1 s, so no re-execution is needed.
Verdict
mergeable after fixes: 1, 2
Generated by Claude Code
Review A #1: the pvoc seam test compared an object with itself, because a warm-up of 12000 samples is already past clock_wrap() (3072) and the seam folds to the reference's own clock value. The seam is now applied at 2048 samples: the reference reads 2048, the seeded clock 5120, and ASSERT_EQ pins that the two are indistinguishable until both wrap 1024 samples later. Re-verified by mutation through the gtest binary: misaligning the wrap by one hop (m_n -= an - m_hop) now fails the test on its first differing sample (-0.989 vs 0.685) in both profiles. Review A #2: the psola class docstring now carries the consequence of the wrap as a number: from 2 * clock_wrap() samples on (at most 2^19, 10.9 s at 48 kHz; 519,552 for max_period 900) the output diverges from an unbounded-clock reference at ~1e-8 relative (measured 3.8e-9 absolute in double, first at sample 521,685; one float ulp in float, first at 559,764; 0.35 peak). Review B #2: the new precondition is in the @pre (16 <= max_period < 2^26). Review B #3: k_clock_span is private; the tests pin clock_wrap() <= 2^18 as a literal; both seams say "not a contract point; may change or disappear without a version note". Review A #3-#5 and B #6: position bound corrected to 2^19 + 5 * max_period (at least 33 fractional bits); the exactness argument in shift_clock() no longer cites the Sterbenz upper half; "four rings" wording dropped; "first wrap at 2 * clock_wrap(), every clock_wrap() after" stated on both clock_wrap() accessors; pvoc gains the symmetric @pre fft_size <= 2^28 and its assert. Both class docstrings state that the run-time contract is the bound m_n < 2 * clock_wrap(), not an observable. Verified: Release build with -DTAP_DSP_WERROR=ON, ctest 164/164, clang-format and the clang-tidy mirror clean; the 30-configuration fingerprint (60 hashes, both profiles, runs up to 3 s) is still identical to origin/main. LLP64 is proven by the Windows CI leg; no CI leg on this PR compiles these headers for ILP32 (the M55 job builds only the Ooura C). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Fix pass for reviews A and B (head dd7a8ef)Review A (correctness)
Review B (process / downstream)
Verification on dd7a8ef: Release + Generated by Claude Code |
What this changes
Replaces the
long m_nsample counters inbasic_psolaandbasic_pvocwith fixed-widthstd::int32_tsample clocks bounded below2 * clock_wrap()(a multiple of their ring geometry), adds a documented O(1) test seam to each class, and a test in each battery that seeds an elapsed count past 2^31 and runs across the wrap. Stage 0 ofdocs/audit-fft-and-code-smells.md; nothing else rides along.Why
longis 32 bits on Cortex-M (ILP32) and Windows (LLP64). At 48 kHz the counter overflowed (signed UB) after ~12.4 h, after whichm_n % ringwent negative and thestatic_cast<size_t>turned it into an out-of-bounds write (psola.h:105,pvoc.h:147).Design, chosen so there is no 64-bit division or modulo anywhere in the per-sample path (an M4 would pay a libgcc call per sample):
[0, 6N)and is pulled back by3N(the overlap-add ring, a multiple of the input ring and the hop). Every ring index and the frame schedule are unchanged, the warm-up guards (m_n >= N) stay true, and the output is bit-identical across the wrap. Every existing 1 s test already wraps 14 times.[0, 2 * clock_wrap())whereclock_wrap()is the largest multiple of the ring size not above 2^18 (a private constant), and on the wrap the three fractional mark positions move with it (an exact double subtraction: the wrap is an integer, hence a multiple of every position's ulp, and the result does not exceed the position in magnitude). Only their magnitude changes. Consequence, stated as a number in the class docstring: from2 * clock_wrap()samples on (at most 2^19 = 10.9 s at 48 kHz; 519,552 samples for max_period 900) the output diverges from an unbounded-clock reference at ~1e-8 relative (measured 3.8e-9 absolute in double, first at sample 521,685; one float ulp, 2.98e-8, in float, first at 559,764; 0.35 peak). Positions stay below 2^19 + 5 * max_period (at least 33 fractional bits). The wrap fires every 5.4 s after the first, so the path is exercised routinely.Both headers state the run-time contract in the class docstring next to latency: the contract is the bound
m_n < 2 * clock_wrap(), not an observable. The seamadvance_clock_for_testing(std::uint64_t)is documented as "not a contract point; may change or disappear without a version note".Verification
Ran locally (Linux x86-64, GCC, LP64):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DTAP_DSP_WERROR=ON,cmake --build build -j8,ctest --test-dir build --output-on-failure: 164/164 green, zero warnings. The four new tests add ~0.3 s.clang-format --dry-run --Werroron the four changed files andscripts/tidy.sh(the CI clang-tidy mirror, clang-tidy 18) on both test TUs: clean.origin/mainheaders and against these: all 60 hashes identical. The pvoc runs cross the new wrap ~70 times; the psola runs and the notebook's 1 s signals stay below2 * clock_wrap()(10.8 s), so they are unaffected and no notebook re-execution is needed. Above2 * clock_wrap()psola is not bit-identical tomain; the divergence is the ~1e-8 figure above (15 s run, measured againstorigin/mainheaders).psola_test.ClockWrapIsSeamlessAndCountersDoNotOverflowmeasures 2e-9 against a 0.35 peak (bound 1e-6);pvoc_test.ClockWrapIsBitExactAndCountersDoNotOverflowapplies the seam at a warm-up belowclock_wrap()(reference clock 2048, seeded clock 5120) soASSERT_EQis a real pin, plus whole-run identity from 2×latency (1e-15 double / 4.4e-7 float; bounds 1e-8 / 2e-3). Mutation-checked through the gtest binary itself: dropping the position shift in psola's wrap fails withworst = 0.3526; misaligning pvoc's wrap by one hop (m_n -= an - m_hop) fails on the first differing sample (-0.989 vs 0.685), in both profiles.Not run here: any 32-bit build. LLP64 is proven by the
windows-oouraleg, which runs the full suite including the new tests. No CI leg on this PR compilespsola.horpvoc.hfor ILP32: thecortex-m55-cmsis (compile)job builds only the Ooura C library with tests off; the Stage 1 DspTap embedded legs (#17) will be the first to. The counter type is fixed-width, so its width no longer depends on the toolchain.Notes for the reviewer
16 <= max_period < 2^26for psola,fft_size <= 2^28for pvoc, both in the@preand asserted) and a run-time contract point (bounded clock; psola's ~1e-8 divergence past2 * clock_wrap()). Public surface added:clock_wrap()on both (a geometry query) andadvance_clock_for_testing()on both (not a contract point).k_clock_spanis private.fft.handnn.h. TapTools, the consumer CLAUDE.md names as pinning this repo, is not on disk here and was not checked. The MuTap pin bump after squash is owed by the rollout rule and is behaviourally a no-op for MuTap; it is not part of this PR. README is a contention file this wave; request to its owner: one line per shifter ("run time unbounded; 32-bit sample clock wraps by ring geometry; psola diverges ~1e-8 from an unbounded clock past 2 * clock_wrap()").process()itself is exact.🤖 Generated with Claude Code
https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy