@@ -13,6 +13,7 @@ import { useWAMStore } from '../store/wamStore';
1313import { effectsEngine , initWasmDsp } from '../engine/EffectsEngine' ;
1414import { pluginEngine } from '../engine/PluginEngine' ;
1515import { getAudioEngine } from './useAudioEngine' ;
16+ import { VST3LatencyCompensation } from '../services/vst3bridge/VST3LatencyCompensation' ;
1617import { createDebugLogger } from '../utils/debugLogger' ;
1718import type { CompressorParams } from '../types/project' ;
1819import type { WAMActiveInstance } from '../types/wam' ;
@@ -51,12 +52,12 @@ export function buildCombinedEffectsChain(trackId: string): { input: AudioNode |
5152
5253/**
5354 * Derive a stable fingerprint of VST3 instances for effect chain syncing.
54- * Only includes fields that affect audio routing (instanceId, trackId, enabled) .
55+ * Only includes fields that affect audio routing or timing .
5556 */
5657function selectVst3EffectChainKey ( s : { instances : Record < string , VST3ActiveInstance > } ) : string {
5758 const parts : string [ ] = [ ] ;
5859 for ( const [ id , inst ] of Object . entries ( s . instances ) ) {
59- parts . push ( `${ id } :${ inst . trackId ?? '' } :${ inst . enabled ? '1' : '0' } ` ) ;
60+ parts . push ( `${ id } :${ inst . trackId ?? '' } :${ inst . enabled ? '1' : '0' } : ${ inst . latencySamples ?? 0 } ` ) ;
6061 }
6162 return parts . sort ( ) . join ( '|' ) ;
6263}
@@ -69,6 +70,56 @@ function selectWamEffectChainKey(s: { instances: Record<string, WAMActiveInstanc
6970 return parts . sort ( ) . join ( '|' ) ;
7071}
7172
73+ export function calculatePluginDelayCompensation ( trackIds : string [ ] , sampleRate : number ) : Map < string , number > {
74+ return VST3LatencyCompensation . calculateCompensation (
75+ trackIds . map ( ( id ) => ( {
76+ id,
77+ pluginLatencies : [ pluginEngine . getChainLatency ( id ) ] ,
78+ } ) ) ,
79+ sampleRate ,
80+ ) ;
81+ }
82+
83+ interface PluginDelayCompensationTrack {
84+ id : string ;
85+ isGroup ?: boolean ;
86+ parentTrackId ?: string ;
87+ }
88+
89+ function getSignalPathTrackIds (
90+ track : PluginDelayCompensationTrack ,
91+ trackById : Map < string , PluginDelayCompensationTrack > ,
92+ ) : string [ ] {
93+ const path = [ track . id ] ;
94+ const visited = new Set ( path ) ;
95+ let parentTrackId = track . parentTrackId ;
96+ while ( parentTrackId && ! visited . has ( parentTrackId ) ) {
97+ visited . add ( parentTrackId ) ;
98+ path . push ( parentTrackId ) ;
99+ parentTrackId = trackById . get ( parentTrackId ) ?. parentTrackId ;
100+ }
101+ return path ;
102+ }
103+
104+ export function calculatePluginDelayCompensationForTracks (
105+ tracks : PluginDelayCompensationTrack [ ] ,
106+ sampleRate : number ,
107+ ) : Map < string , number > {
108+ const trackById = new Map ( tracks . map ( ( track ) => [ track . id , track ] ) ) ;
109+ const sourceTracks = tracks . filter ( ( track ) => ! track . isGroup ) ;
110+ const compensationByTrack = VST3LatencyCompensation . calculateCompensation (
111+ sourceTracks . map ( ( track ) => ( {
112+ id : track . id ,
113+ pluginLatencies : getSignalPathTrackIds ( track , trackById ) . map ( ( id ) => pluginEngine . getChainLatency ( id ) ) ,
114+ } ) ) ,
115+ sampleRate ,
116+ ) ;
117+ for ( const track of tracks ) {
118+ if ( track . isGroup ) compensationByTrack . set ( track . id , 0 ) ;
119+ }
120+ return compensationByTrack ;
121+ }
122+
72123export function useEffectsSync ( ) {
73124 const tracks = useProjectStore ( ( s ) => s . project ?. tracks ) ;
74125 const dspBackend = useUIStore ( ( s ) => s . dspBackend ) ;
@@ -116,6 +167,8 @@ export function useEffectsSync() {
116167 instancesByTrack . set ( inst . trackId , list ) ;
117168 }
118169
170+ const trackNodes = new Map < string , ReturnType < typeof engine . getOrCreateTrackNode > > ( ) ;
171+
119172 // First pass: rebuild built-in effect chains + sync VST3 bypass, then splice combined chain
120173 for ( const track of tracks ) {
121174 const effects = track . effects ?? [ ] ;
@@ -133,14 +186,19 @@ export function useEffectsSync() {
133186 if ( trackNode ) {
134187 const { input, output } = buildCombinedEffectsChain ( track . id ) ;
135188 trackNode . spliceEffects ( input , output ) ;
136-
137- // Always apply latency compensation (including clearing to 0 when plugins removed/bypassed)
138- const pluginLatency = pluginEngine . getChainLatency ( track . id ) ;
139- const sampleRate = engine . ctx ?. sampleRate ?? 44100 ;
140- trackNode . setLatencyCompensation ( pluginLatency , sampleRate ) ;
189+ trackNodes . set ( track . id , trackNode ) ;
141190 }
142191 }
143192
193+ const sampleRate = engine . ctx ?. sampleRate ?? 44100 ;
194+ const compensationByTrack = calculatePluginDelayCompensationForTracks (
195+ tracks . filter ( ( track ) => trackNodes . has ( track . id ) ) ,
196+ sampleRate ,
197+ ) ;
198+ for ( const [ trackId , trackNode ] of trackNodes ) {
199+ trackNode . setLatencyCompensation ( compensationByTrack . get ( trackId ) ?? 0 , sampleRate ) ;
200+ }
201+
144202 // Second pass: wire sidechain connections (all chains must exist first)
145203 for ( const track of tracks ) {
146204 for ( const effect of track . effects ?? [ ] ) {
0 commit comments