Repository navigation
Carry the mixer's transpose into the exported file - #599
Merged
Merged
Conversation
Transposition happens in the browser, in a SoundTouch AudioWorklet. The server has no pitch stage at all, so a mix exported from a transposed session came back in the original key with nothing to say it had. The export now takes a per-lane semitone shift and applies it with ffmpeg's rubberband filter. Measured on the bundled build: duration holds to the microsecond across the whole -6..+6 range and the pitch lands within 1 Hz of target, which matters because a lane whose length moved would desync the mix. asetrate/atempo was not used for that reason. Drums are never transposed, on the server as on the client. Resampling a snare does not move it to another key, it makes it a different drum, and effective_pitch here mirrors effectivePitch in static/js/pitchBus.js so an export cannot contain something the studio refuses to play. "original" needed more than a filter. It is the complement lane, so it usually has drums summed into it, and shifting that one file would resample them. Playback never does: buildPlaybackStems rebuilds the lane from the component stems and routes drums around the pitch stage. The export now does the same, and falls back to leaving the lane at its original key when a component is missing -- an older job that kept only the rendered mix -- which is the same conservative choice playback makes there. librubberband is GPL and plenty of builds omit it: the bundled macOS ffmpeg has it, the Homebrew one on a dev machine does not. So it is probed once per process, and a transpose asked of a build that cannot do it is refused with a reason rather than served in the original key, which would look exactly like the bug this fixes. An export with no transpose never probes at all and renders exactly as it did before. The video mux takes the same parameter. An MP4 whose audio ignored the transpose would be this issue reported a second time. The shift enters the render cache key only when something is actually shifted, so every export made before this keeps the entry it already had, and a client that predates it omits the parameter and is unchanged. Closes #592
…l audio
Both surfaced by exporting from an actual library rather than a fixture, and
neither was visible to the tests as they stood.
Validity depended on the transpose. `_expand_render_lanes` rebuilt the
"original" lane from component stems without first checking the lane exists
at all, so ?stems=vocals,original 404'd untransposed and rendered when
transposed. Whether a lane exists is a property of the job, not of the
request. The lane is now validated before anything else, and the
reconstruction is a decision made after that.
The fallback pointed at the wrong file. The component loop bound its path to
`path`, the same name already holding the validated original.wav, so a job
whose components are missing fell back to whichever component was tried last
-- drums.wav -- instead of the lane the caller asked for. Renamed, with a
comment saying why the name is not reused.
Verified against a real 224 s track rather than only in tests, measuring the
shift by per-frame median log-spectral correlation and validating that method
against a known-good direct rubberband render first:
bass pitches=+2 -> +2.00 semitones
bass pitches=-3 -> -2.96 semitones
drums pitches=+2 -> byte-identical to no transpose
MP3 and WAV both export; duration is unchanged in every case.
Refs #592
Collaborator
Author
|
Listening check done, on a full six-lane mix from a real 224 s track, 25 s region, exported at 0, +2, +5, -3 and -6 semitones. Every file lands on the requested shift, measured by per-frame median log-spectral correlation (validated against a known-good direct rubberband render first, because two earlier measurement attempts gave wrong answers):
Confirmed by ear at +5 and -6, the two extremes where rubberband artifacts would show first: quality holds, including the drums staying at their own pitch while the rest of the mix moves around them. That was the one thing no test could settle. Nothing outstanding on the audio side. |
thcp
marked this pull request as ready for review
September 8, 2026 16:51
…g it A probe that raised was silently treated as "no pitch support". That is the right answer, but it collapses two different situations into one message: a build genuinely without the filter and an ffmpeg that would not run at all both reach the user as "this ffmpeg build cannot transpose", with nothing to tell them apart. Logs which it was at debug level. Raised by the code-quality review on #599. Refs #592
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #592
Transposition happens in the browser, in a SoundTouch AudioWorklet. The server had no pitch stage at all, so a mix exported from a transposed session came back in the original key with nothing to say it had.
What it does
The export takes a per-lane semitone shift (
?pitches=, parallel tostems/gains, the shape the endpoint already uses) and applies it with ffmpeg'srubberbandfilter.I measured that choice rather than assuming it. On the bundled build, across the whole -6..+6 range:
Duration holding exactly is the part that matters: a lane whose length moved would desync the mix. That is why
asetrate/atempowas not used.Three things that needed care
Drums are never transposed.
effective_pitchmirrorseffectivePitchinstatic/js/pitchBus.js, so the export cannot contain something the studio refuses to play. Verified end to end: rendering vocals+drums at +2 gives 440 -> 494.0 Hz and leaves drums at 1000.0 Hz.originalneeded more than a filter. It is the complement lane, so it usually has drums summed into it, and shifting that file would resample them. Playback never does this --buildPlaybackStemsrebuilds the lane from component stems and routes drums around the pitch stage. The export now does the same. When a component is missing (an older job that kept only the rendered mix) the lane stays at its original key, which is the same conservative choice playback already makes there.librubberbandis GPL and plenty of builds omit it. The bundled macOS ffmpeg has it; the Homebrew one on this dev machine does not. So it is probed once per process, and a transpose asked of a build that cannot do it is refused with a reason rather than served in the original key -- which would look exactly like the bug this fixes. An export with no transpose never probes and renders exactly as before.Compatibility
The shift enters the render cache key only when something is actually shifted, so every export made before this keeps the entry it already had. A client that predates this omits the parameter and behaves identically.
The video mux takes the same parameter. An MP4 whose audio ignored the transpose would be this issue reported a second time.
Verification
pytest tests/ -qtests/test_transpose_export.pynpx playwright testruff check/format --checknode --check+ JS suitesThe 2 failures are the ogg pair that fails on unmodified
mainon this machine (local ffmpeg has nolibvorbis).Not done
No export-settings checkbox. The transpose follows the mixer, so an export matches what you are hearing without a second control to keep in sync. The issue asked for an option; if you want the file to be able to differ from the session deliberately, say so and I will add the toggle.