Play cross-rate captures at their own pitch and length

The voice now converts the file's rate to the host's at note-on instead of assuming they match. Stage times resolve at the host rate, the filter solves at it, and the bake follows. Same-rate renders are unchanged bit for bit.
This commit is contained in:
2026-09-25 13:44:39 -04:00
parent 9ff34b0a9e
commit ec60cf78c6
25 changed files with 749 additions and 137 deletions
+8 -3
View File
@@ -97,9 +97,14 @@ decision about what the render made obsolete.
- **`defaultBakeProgram`'s read-rate bound is an upper bound, not a model.** Anything that
slows the read makes the head take longer to cross its span, so the window is scaled by the
slowest read the voice can reach — a shallower excursion leaves trailing silence in the file.
Rate is a term of it under BOTH engines and the deepest downward pitch offset under Varispeed
alone (`playbackStretch` argues each); both the Trigger span and the Gate exhaustion length
take the product, and the Gate-with-loop branch takes neither.
Rate and the source/host rate conversion are terms of it under BOTH engines and the deepest
downward pitch offset under Varispeed alone (`playbackStretch` argues each); both the Trigger
span and the Gate exhaustion length take the product, and the Gate-with-loop branch takes
neither.
- **The snapshot and the plan must share one rate.** The snapshot's frame counts are resolved
for its `renderRate`; the shell passes the plan's rate into `bakeSnapshot` so they agree, and
`renderBake` refuses a pair that does not rather than printing every stage time off by the
ratio.
- **The bake fires at the instance's PREVIEW velocity, not a constant.** Three velocity curves
are live, so the velocity is a property of the sound being printed and not a detail of the
render; it also feeds the Varispeed bound above (a velocity→pitch curve moves the window).