feat(signal): add FftT::inverse_real() — real-valued inverse FFT#6114
feat(signal): add FftT::inverse_real() — real-valued inverse FFT#6114matthewfudge wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
a2d4c1b to
2869aa9
Compare
|
Good catch — that's a real bug and I've fixed it in
Also added move coverage to Verified fail-before/pass-after: without the fix the |
There was a problem hiding this comment.
Reviewed by building and running it, not by reading it.
Verified locally:
test_signal_spectral— 31,839 assertions passtest_signal_rt_safety— passes withinverse_realadded, so it is genuinely allocation-free in the audio callback (the claim that actually matters for a plugin)- Branch is rebased on current
main(759669953) — clean
The cross-validation is the right one. On macOS with float, inverse_real() always takes the vDSP branch, so the only honest way to check the scale factor is against an independent oracle. Comparing against the hand-composed forward_real → complex inverse() → real part route is exactly that, and asserting the imaginary part is ~0 additionally proves the input really was conjugate-symmetric. That's the test that would catch a wrong 1/N vs 1/(2N). Fft64 genuinely drives the scalar fallback (is_same_v<SampleType, float> never holds there). Move constructor and move assignment both transfer the new scratch buffer correctly.
On the version bump: keep it. 0.668.0 → 0.669.0 is correct — our gate requires a minor bump for a public-header change, and shipyard pr would have applied it anyway. Nothing to drop.
One non-blocking nit (happy to take this as a follow-up rather than another round trip):
inverse_real_scratch_.resize(size) runs unconditionally in the constructor, but the buffer is only ever touched by inverse_real_fallback(). On Apple with float — the common case — it is never used. A 65536-point FftT<float> therefore carries ~512 KB of dead memory per instance. A one-line guard on the vDSP path would avoid it.
Two things from your side that were also right, for the record:
The 0.5 in forward_real_vdsp is not a bug, and you were right not to "fix" it.
And the stale-fork problem you hit is real and was ours to document — a fork several hundred commits behind makes our pre-push gates diff against the wrong base, so they report node-ABI "removals" and hotspot growth in files you never opened, and the message names your branch. CONTRIBUTING.md now says so, with the resync recipe. That same staleness is almost certainly the cause of the Pulp::midi vs pulp::midi report: on current main, find_package(Pulp) already aliases every Pulp::x to a lowercase pulp::x, so target_link_libraries(you PRIVATE pulp::midi) works. No CMake change needed on either side — just resync.
Also: the MIDI writer bugs you reported are all four confirmed and fixed in #6116. Your agent read the write path correctly down to the constant.
2869aa9 to
0181cd8
Compare
|
Thanks — and thanks for reviewing it by building and running it rather than reading it. That's the review I'd want. Nit taken, not deferred — pushed. static constexpr bool uses_inverse_real_fallback() {
#if PULP_FFT_HAS_VDSP
return !std::is_same_v<SampleType, float>;
#else
return true;
#endif
}
...
if constexpr (uses_inverse_real_fallback()) inverse_real_scratch_.resize(size);It mirrors On the stale fork — thank you for documenting it. I'd assumed it was my own mess. Good to know And thank you for #6116 — four-for-four, that was fast. We'd already started writing our own SMF writer on our side, since our Which is the general shape of how we'd like to work with pulp, if it suits you: when we need a capability that isn't there, build it, use it, and send it back — rather than filing a request and waiting. #6115 (joint-channel block oversampling + a second half-band design lane) is the other one currently open. (Edited: removed references to another framework by name — that's your house rule from #6115 and it should apply to my comments too, not just the source.) |
|
Heads-up on the one red check — this is our papercut, not a mistake on your side.
and yours is folded into the feature commit, so the gate can't see a marker. One-line fix — split the version bump into its own commit: git reset --soft HEAD~1 # unstage your commit, keep the changes
git restore --staged CMakeLists.txt
git commit -m "feat(signal): <your original subject>" # the code
git add CMakeLists.txt
git commit -m "chore: bump versions" # the marker — exact subject
git push --force-with-leaseThe subject has to match exactly — Why you hit this and we don't: While I'm here — |
0181cd8 to
736f70e
Compare
|
Fixed — That one was mine rather than the gate's, for the record: the branch originally did have the split, and I collapsed it myself when I amended the scratch-buffer fix in without noticing the bump rode along. The gate was right to complain.
Worth noting #6115 already had the split and its Noted on the |
|
Clear next step so this does not sit in limbo: Matthew: please rebase this branch onto current There is no additional design change being requested at this point. Once the reconciled head is pushed, we will re-run the focused spectral and real-time-safety validation against that exact SHA and either approve it or report a concrete remaining blocker. |
|
Correction to the next-step assignment above: no action needed from you right now. We will handle the final rebase/version reconciliation on the maintainer side at merge-prep time, preserve your authorship, and re-run the focused validation on the resulting exact head. We will come back to you only if that exposes a substantive issue in the contribution itself. |
Mirrors forward_real() so callers using real-only FFT workflows no longer have to hand-compose forward_real() -> complex inverse() -> real part. Dispatches to a vDSP zrip-based path on Apple (float) and a complex-inverse-then-real-part fallback otherwise, both normalized consistently with inverse()'s existing 1/N convention so inverse_real(forward_real(x)) == x with no extra scaling by the caller. Adds round-trip, cross-backend, and DC/Nyquist-bin coverage in test_signal_spectral.cpp, and wires inverse_real() into the existing FFT allocation-free hot-path probe in test_signal_rt_safety.cpp.
736f70e to
c2f37ca
Compare
|
Maintainer integration is complete on current
No substantive contributor change remains requested. Next action — Daniel: final review and merge decision on this exact head. |
|
/shipyard review |
|
Shipyard review passed for |
|
Please hold this one — it's green and mergeable, but it doesn't meet your contributor contract and I'd rather say so than let it through. Two gaps, both ours:
We're also rethinking our contribution shape more broadly. We'd rather send you plans than patches — the capability, the evidence, and the published method, so design and implementation stay with your team and no provenance question lands on you at all. #6146 is the first of those, and it's how we'd like to work from here. So: don't merge this on our account. We'll either come back with the sign-off and provenance done properly, or close it and fold the capability into the plan — either way, the ask is unchanged and it's in #6146 (§3, the real-valued inverse, including the move-constructor/scratch-buffer trap and the point that a round-trip test which takes the accelerated path can't fail in the fallback branch). Sorry for the noise — better a retraction than a licence question you never asked for. |
|
/shipyard review |
Adds
inverse_real()topulp::signal::FftT, mirroring the existingforward_real().The gap
fft.hpphasforward_real()(vDSP branch + scalar fallback) but no real-valued inverse. Any caller doing real-signal spectral work — analyse a real block, modify the spectrum, resynthesise — has to hand-composeforward_real()→ complexinverse()→ take the real part at every call site. That's a round trip the library can do better, and it's easy to get the normalization wrong in caller code.What it does
A straight mirror of
forward_real's structure:inverse_real_vdsp()underPULP_FFT_HAS_VDSPfor float —vDSP_fft_zripwithkFFTDirection_Inverse, packing DC/Nyquist back intorealp[0]/imagp[0](the inverse of the forward unpack), then unpacking to interleaved real samples.inverse_real_fallback()— complexinverse(), take the real part.Normalization follows the existing convention:
forward()/forward_real()are unnormalized,inverse()applies1/Nonce — soinverse_real(forward_real(x)) == xwith no caller-side scaling.Scaling derivation. Web sources conflicted, so I measured it against real
vDSP_fft_zrip(N=16, random real signal): the raw forward zrip output is 2× the true DFT (which is whatforward_real_vdsp's existing0.5corrects), and feeding the corrected spectrum into the raw inverse yields exactlysize × x— so a plain1/size_, matchinginverse_vdsp()'s complex case, not the1/(2·size_)some summaries suggest. The derivation and the empirical check are in the code comment.RT contract preserved. The class comment promises allocation-free-after-construction, so the fallback's scratch is allocated once in the constructor (mirroring
twiddles_) — and only where the fallback can actually be reached, soFftT<float>on Apple carries nothing.inverse_real()is wired into the existing allocation-free probe intest_signal_rt_safety.cpp.Tests
In
test_signal_spectral.cpp(nottest_fft_backends.cpp—MultiBackendFftonly exposes the complex transforms, and the existingforward_realcoverage lives in spectral):Fft64to force the fallback branch (theif constexprgate meansFftT<float>always takes vDSP on Apple, so a float-only test can't observe the fallback at all)inverse_realvs the hand-composedinverse()+real-part, proving the vDSP branch matches the fallback's semantics — an independent oracle for the scale factor, since the vDSP path can't be turned off at runtimeFft64deliberately: the scratch is only touched by the fallback, so a float move test would pass whether or not it's transferredpulp-test-signal-spectral31,839 assertions / 27 cases pass;pulp-test-signal-rt-safetypasses;pulp-test-fft-backendsunaffected.Version bumped 0.668.0 → 0.669.0 per the public-header minor rule, in its own
chore: bump versionscommit.