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:
@@ -81,10 +81,12 @@ std::optional<DecodedPcm> decodeRelative(const std::string& projectDir,
|
||||
// never be built from a different fold than the one the user is hearing.
|
||||
std::optional<SampleData> buildFromRef(const SelectedSample& sel,
|
||||
const InstrumentParams& params,
|
||||
const std::string& projectDir, ChannelMode mode) {
|
||||
const std::string& projectDir, ChannelMode mode,
|
||||
int renderRate) {
|
||||
std::optional<DecodedPcm> pcm = decodeRelative(projectDir, sel.relativePath, mode);
|
||||
if (!pcm) return std::nullopt;
|
||||
SampleData sample = buildSampleData(resolveCapture(sel, params), std::move(*pcm));
|
||||
SampleData sample =
|
||||
buildSampleData(resolveCapture(sel, params), std::move(*pcm), renderRate);
|
||||
if (!sample.playable()) return std::nullopt;
|
||||
return sample;
|
||||
}
|
||||
@@ -143,7 +145,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
mode = channelMode_;
|
||||
}
|
||||
if (std::optional<SampleData> decoded =
|
||||
buildFromRef(*sel, params, projectDir, mode)) {
|
||||
buildFromRef(*sel, params, projectDir, mode, hostRenderRate())) {
|
||||
sample = std::move(*decoded);
|
||||
havePlayable = true;
|
||||
// Point the built snapshot at the instance's ONE engine-facing block and seed the
|
||||
@@ -156,9 +158,10 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// BEFORE the publish: the publish is what makes the audio thread re-merge, and the
|
||||
// merge resolves every held automation stage time against this rate. Stored after,
|
||||
// a merge in the window between them would resolve them against the previous
|
||||
// capture's rate — or, on the first-ever build, against 0, where secondsToFrames
|
||||
// build's rate — or, on the first-ever build, against 0, where secondsToFrames
|
||||
// collapses every automated envelope stage to zero frames.
|
||||
builtSampleRate_.store(sample.sampleRate, std::memory_order_relaxed);
|
||||
builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed);
|
||||
loadedSourceRate_.store(sample.sampleRate, std::memory_order_relaxed);
|
||||
{
|
||||
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here;
|
||||
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
|
||||
@@ -189,7 +192,7 @@ std::string ReaSamplerProcessor::usageInstanceGuid() {
|
||||
return instanceGuid_;
|
||||
}
|
||||
|
||||
std::optional<SampleData> ReaSamplerProcessor::bakeSnapshot() {
|
||||
std::optional<SampleData> ReaSamplerProcessor::bakeSnapshot(int renderRate) {
|
||||
const std::string selId = selectedSampleId();
|
||||
if (selId.empty()) return std::nullopt;
|
||||
const InstrumentParams params = instrumentParams();
|
||||
@@ -202,7 +205,7 @@ std::optional<SampleData> ReaSamplerProcessor::bakeSnapshot() {
|
||||
if (!sel) return std::nullopt;
|
||||
// No live block is attached: the bake renders the stored parameter set, which is what
|
||||
// every knob has already written, rather than whatever the audio thread is observing.
|
||||
return buildFromRef(*sel, params, bridge_.activeProjectDir(), channelMode());
|
||||
return buildFromRef(*sel, params, bridge_.activeProjectDir(), channelMode(), renderRate);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
|
||||
@@ -328,13 +331,8 @@ ReaSamplerProcessor::buildInstrumentLocked(SampleData sample) {
|
||||
builtVoiceMode = voiceMode_;
|
||||
builtMonoTrigger = monoTrigger_;
|
||||
}
|
||||
// Preserve OLA window in output frames from the host rate (kPreserveWindowMs), pre-sized
|
||||
// here so process()-time note-on never allocates. Floored at 2 so a valid window is always
|
||||
// a real ring, covering a pathological host rate <= 0 too. Re-derived per build, so a
|
||||
// reactivation after the host changed its rate gets a ring sized for the new one.
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
// Pre-sized here so process()-time note-on never allocates.
|
||||
const std::int64_t preserveWindow = preserveWindowFrames(sample);
|
||||
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
return std::make_unique<LoadedInstrument>(
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
@@ -358,11 +356,18 @@ bool ReaSamplerProcessor::resumeDormantInstrument() {
|
||||
// Off the audio thread (setActive only). The reactivation half of the lifetime split: the
|
||||
// voice state the deactivate destroyed is rebuilt, the PCM it parked is reused as-is.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
// Every frame count in a built sample — and in the live block published beside it — was
|
||||
// resolved for the host rate of its build. setupProcessing may have changed that rate while
|
||||
// inactive (or a setState load ran before the host reported one), and neither a park nor a
|
||||
// publish can be re-used across it: the full reload re-resolves both.
|
||||
const int renderRate = hostRenderRate();
|
||||
// A publish that landed while inactive (setState's reload, a bake's adopt) IS the
|
||||
// activation state — its voices have never rendered, and it has already superseded the
|
||||
// park. Rebuilding here would displace a correct instrument into the drain slot.
|
||||
if (live_.load(std::memory_order_acquire)) return true;
|
||||
if (!dormantSample_) return false;
|
||||
if (const LoadedInstrument* cur = live_.load(std::memory_order_acquire)) {
|
||||
return cur->sample.renderRate == renderRate;
|
||||
}
|
||||
if (!dormantSample_ || dormantSample_->renderRate != renderRate) return false;
|
||||
|
||||
// The activation is still where a bank change made with NO EDITOR OPEN is picked up:
|
||||
// pollBankSync, the only other route to either of the two calls below, runs off the
|
||||
|
||||
Reference in New Issue
Block a user