mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-11 03:09:56 +00:00
audio: replaceIR(slotId, path, gain) for in-place cab/IR swap (#83)
* audio: add replaceIR(slotId, path, gain) for in-place cab/IR swap Swap an existing convolution slot's IR without a full loadPreset, so the rest of the chain — the amp VST above all — is not torn down and rebuilt (that teardown is the ~1-2 s wait when changing cabs / mic position). Mirrors the existing loadIR worker but calls SignalChain::replaceProcessor(slotId, ...); optional gain updates the slot post-gain (the cab makeup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * audio: expose replaceIR over the IPC bridge Wire the native replaceIR(slotId, path, gain) through audio-bridge (ipcMain handle) + preload, so renderers get feedBackDesktop.audio.replaceIR. Lets the rig-builder cab room swap a cab's IRs in place instead of a full loadPreset + param re-apply (that re-apply was the brief 'can't move the mic yet' lag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * audio: replaceIR updates slot name/path so getChainState reflects the swap replaceProcessor deliberately preserves the target slot's name/path during the prepare/fault window (so a fault in prepareToPlay is blocklisted against the right plugin path). It kept them even on success, so after a successful replaceIR the slot's audio was the new IR but getChainState()/preset-save still reported the OLD IR name+path — a footgun for any consumer that persists a chain read back from getChainState(). Add optional newName/newPath to replaceProcessor, applied under the swap lock ONLY on success (empty = keep, so the sandbox-promotion caller is unchanged). ReplaceIRWorker passes "IR: <name>" + the new path, mirroring LoadIRWorker. Built (npm run build:audio) clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Jafz2001
byrongamatos
parent
a53fd38732
commit
56e929da4e
@@ -2474,6 +2474,71 @@ static Napi::Value LoadIR(const Napi::CallbackInfo& info)
|
||||
return deferred.Promise();
|
||||
}
|
||||
|
||||
// Replace the IR of an EXISTING convolution slot in place (cab swap / mic move),
|
||||
// so the rest of the chain — the amp VST above all — is NOT torn down and rebuilt.
|
||||
// Mirrors LoadIRWorker but calls SignalChain::replaceProcessor(slotId, …) instead
|
||||
// of addProcessor. Optional `gain` (>=0) updates the slot's post-gain (the cab
|
||||
// makeup); a negative gain leaves the existing post-gain untouched.
|
||||
class ReplaceIRWorker : public Napi::AsyncWorker
|
||||
{
|
||||
public:
|
||||
ReplaceIRWorker(Napi::Env env, Napi::Promise::Deferred deferred,
|
||||
int slotId, std::string path, float gain)
|
||||
: Napi::AsyncWorker(env), deferred_(deferred),
|
||||
slotId_(slotId), irPath_(std::move(path)), gain_(gain) {}
|
||||
|
||||
void Execute() override
|
||||
{
|
||||
auto liveEngine = snapshotEngine();
|
||||
if (!liveEngine) { ok_ = false; return; }
|
||||
|
||||
const auto sr = loadSafeSampleRate(*liveEngine);
|
||||
const auto bs = loadSafeBlockSize(*liveEngine);
|
||||
auto processor = std::make_unique<IRLoader>();
|
||||
processor->setPlayConfigDetails(2, 2, sr, bs);
|
||||
processor->prepareToPlay(sr, bs);
|
||||
if (! processor->loadIR(juce::File(juce::String(irPath_)))) { ok_ = false; return; }
|
||||
|
||||
auto name = processor->getIRName();
|
||||
ok_ = liveEngine->getSignalChain().replaceProcessor(
|
||||
slotId_, std::move(processor),
|
||||
"IR: " + name, juce::String(irPath_));
|
||||
if (ok_ && gain_ >= 0.0f)
|
||||
liveEngine->getSignalChain().setPostGain(slotId_, gain_);
|
||||
}
|
||||
|
||||
void OnOK() override { deferred_.Resolve(Napi::Boolean::New(Env(), ok_)); }
|
||||
void OnError(const Napi::Error& e) override { deferred_.Reject(e.Value()); }
|
||||
|
||||
private:
|
||||
Napi::Promise::Deferred deferred_;
|
||||
int slotId_;
|
||||
std::string irPath_;
|
||||
float gain_;
|
||||
bool ok_ = false;
|
||||
};
|
||||
|
||||
static Napi::Value ReplaceIR(const Napi::CallbackInfo& info)
|
||||
{
|
||||
auto env = info.Env();
|
||||
auto deferred = Napi::Promise::Deferred::New(env);
|
||||
|
||||
if (!snapshotEngine() || info.Length() < 2
|
||||
|| !info[0].IsNumber() || !info[1].IsString()) {
|
||||
deferred.Resolve(Napi::Boolean::New(env, false));
|
||||
return deferred.Promise();
|
||||
}
|
||||
|
||||
const int slotId = info[0].As<Napi::Number>().Int32Value();
|
||||
const auto irPath = info[1].As<Napi::String>().Utf8Value();
|
||||
const float gain = (info.Length() >= 3 && info[2].IsNumber())
|
||||
? info[2].As<Napi::Number>().FloatValue() : -1.0f;
|
||||
|
||||
auto worker = new ReplaceIRWorker(env, deferred, slotId, irPath, gain);
|
||||
worker->Queue();
|
||||
return deferred.Promise();
|
||||
}
|
||||
|
||||
static Napi::Value RemoveProcessor(const Napi::CallbackInfo& info)
|
||||
{
|
||||
auto liveEngine = snapshotEngine();
|
||||
@@ -3496,6 +3561,7 @@ static Napi::Object InitModule(Napi::Env env, Napi::Object exports)
|
||||
exports.Set("loadVST", Napi::Function::New(env, LoadVST));
|
||||
exports.Set("loadNAMModel", Napi::Function::New(env, LoadNAMModel));
|
||||
exports.Set("loadIR", Napi::Function::New(env, LoadIR));
|
||||
exports.Set("replaceIR", Napi::Function::New(env, ReplaceIR));
|
||||
exports.Set("removeProcessor", Napi::Function::New(env, RemoveProcessor));
|
||||
exports.Set("moveProcessor", Napi::Function::New(env, MoveProcessor));
|
||||
exports.Set("setBypass", Napi::Function::New(env, SetBypass));
|
||||
|
||||
@@ -432,7 +432,8 @@ void SignalChain::removeProcessor(int slotId)
|
||||
if (idx >= 0) slots.remove(idx);
|
||||
}
|
||||
|
||||
bool SignalChain::replaceProcessor(int slotId, std::unique_ptr<juce::AudioProcessor> processor)
|
||||
bool SignalChain::replaceProcessor(int slotId, std::unique_ptr<juce::AudioProcessor> processor,
|
||||
const juce::String& newName, const juce::String& newPath)
|
||||
{
|
||||
if (!processor) return false;
|
||||
|
||||
@@ -469,6 +470,11 @@ bool SignalChain::replaceProcessor(int slotId, std::unique_ptr<juce::AudioProces
|
||||
auto* slot = slots[idx];
|
||||
old = std::move(slot->processor);
|
||||
slot->processor = std::move(staging.processor);
|
||||
// Swap succeeded: adopt the new identity so getChainState()/preset save
|
||||
// report the swapped-in processor, not the one it replaced. Only when
|
||||
// provided — the sandbox-promotion caller passes none and keeps identity.
|
||||
if (newName.isNotEmpty()) slot->name = newName;
|
||||
if (newPath.isNotEmpty()) slot->path = newPath;
|
||||
}
|
||||
// Tear the old processor down OUTSIDE the audio lock: releaseResources() (and
|
||||
// a VST3 destructor) can block, and must never stall process() on it.
|
||||
|
||||
@@ -63,7 +63,12 @@ public:
|
||||
// swaps under the audio lock; the old processor is torn down off the lock.
|
||||
// Returns false if the slot is gone or the incoming processor faulted in
|
||||
// prepareToPlay (in which case the existing processor is left untouched).
|
||||
bool replaceProcessor(int slotId, std::unique_ptr<juce::AudioProcessor> processor);
|
||||
// The OLD slot name/path are preserved during the prepare/fault window (so a
|
||||
// fault is blocklisted against the right path); on SUCCESS, if newName/newPath
|
||||
// are non-empty they replace the slot's identity so getChainState()/preset
|
||||
// metadata reflect the swapped-in processor (used by replaceIR for cab swaps).
|
||||
bool replaceProcessor(int slotId, std::unique_ptr<juce::AudioProcessor> processor,
|
||||
const juce::String& newName = {}, const juce::String& newPath = {});
|
||||
// Snapshot a slot's state for sandbox promotion, SAFELY. Runs hasEditor()
|
||||
// and getStateInformation() under the audio lock (so they can't race
|
||||
// process()'s processBlock on the same instance) and under the SEH/signal
|
||||
|
||||
@@ -1080,6 +1080,10 @@ export function initAudioBridge(): void {
|
||||
return await audio?.loadIR(irPath) ?? -1;
|
||||
});
|
||||
|
||||
ipcMain.handle('audio:replaceIR', async (_event, slotId: number, irPath: string, gain?: number) => {
|
||||
return await audio?.replaceIR(slotId, irPath, typeof gain === 'number' ? gain : -1) ?? false;
|
||||
});
|
||||
|
||||
ipcMain.handle('audio:removeProcessor', (_event, slotId: number) => {
|
||||
audio?.removeProcessor(slotId);
|
||||
vstSlotPaths.delete(slotId);
|
||||
|
||||
@@ -402,6 +402,8 @@ const feedBackDesktopApi = {
|
||||
loadVST: (pluginPath: string) => ipcRenderer.invoke('audio:loadVST', pluginPath),
|
||||
loadNAMModel: (modelPath: string) => ipcRenderer.invoke('audio:loadNAMModel', modelPath),
|
||||
loadIR: (irPath: string) => ipcRenderer.invoke('audio:loadIR', irPath),
|
||||
replaceIR: (slotId: number, irPath: string, gain?: number) =>
|
||||
ipcRenderer.invoke('audio:replaceIR', slotId, irPath, gain),
|
||||
removeProcessor: (slotId: number) => ipcRenderer.invoke('audio:removeProcessor', slotId),
|
||||
moveProcessor: (from: number, to: number) => ipcRenderer.invoke('audio:moveProcessor', from, to),
|
||||
setBypass: (slotId: number, bypassed: boolean) => ipcRenderer.invoke('audio:setBypass', slotId, bypassed),
|
||||
|
||||
Reference in New Issue
Block a user