FIX: sửa lỗi chuyển từ tab MAIN SESSION sang SECTION-TAB và PIANO ROLL TAB thì bị bypass
This commit is contained in:
+161
-20
@@ -118,12 +118,20 @@ const masteringChainOn = () => !!(window.currentMasteringSettings && window.curr
|
||||
// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải
|
||||
// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ.
|
||||
const effMidiBypass = (track) => {
|
||||
// GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules)
|
||||
return trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
|
||||
// GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules).
|
||||
// ƯU TIÊN track object (field midiBypass): track object phân biệt đúng
|
||||
// context (main track vs section clone — id TRÙNG nhau); map keyed theo id
|
||||
// gây collision (clone section ghi đè map của main track cùng id → MIDI
|
||||
// section bị route theo ♪ main = bypass sai). Map chỉ là fallback realtime.
|
||||
if (track && track.midiBypass !== undefined) return !!track.midiBypass;
|
||||
if (track && track.id && trackMidiBypassMap[track.id] !== undefined) return !!trackMidiBypassMap[track.id];
|
||||
return false;
|
||||
};
|
||||
const effAudioBypass = (track) => {
|
||||
// GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
|
||||
return trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : !!(track.audioBypass ?? track.masteringBypass);
|
||||
if (track && track.audioBypass !== undefined) return !!track.audioBypass;
|
||||
if (track && track.id && trackAudioBypassMap[track.id] !== undefined) return !!trackAudioBypassMap[track.id];
|
||||
return false;
|
||||
};
|
||||
|
||||
// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||
@@ -133,10 +141,18 @@ function createMasteringRoute(ctx, track, bus) {
|
||||
// falling back to the track object — this guarantees the A-button toggle is
|
||||
// picked up even if a stale track object is passed in.
|
||||
let bypass = false;
|
||||
if (track && track.id && trackAudioBypassMap[track.id] !== undefined) {
|
||||
bypass = !!trackAudioBypassMap[track.id];
|
||||
// Bypass theo LOẠI nội dung track (rules nút A/♪): track MIDI-only → nút ♪
|
||||
// quyết định — KHÔNG dùng audioBypass (section track default audioBypass=true
|
||||
// → route dry → midi section bị bypass dù ♪ tắt — log: SF → track node
|
||||
// (sfEntry) + noteon chạy nhưng âm dry).
|
||||
const hasClips = track && ((track.clips && track.clips.length > 0) || !!track.buffer);
|
||||
const hasMidi = track && track.midiItems && track.midiItems.length > 0;
|
||||
if (hasMidi && !hasClips) {
|
||||
// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
// clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
|
||||
bypass = track.midiBypass !== undefined ? !!track.midiBypass : (track.id && trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : false);
|
||||
} else if (track) {
|
||||
bypass = !!(track.audioBypass ?? track.masteringBypass);
|
||||
bypass = track.audioBypass !== undefined ? !!track.audioBypass : (track.id && trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : false);
|
||||
}
|
||||
// Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement)
|
||||
// ⚠️ GỠ 17:00 — override nuốt nút A/♪ (log: audioBypass=true nhưng
|
||||
@@ -156,6 +172,23 @@ function createMasteringRoute(ctx, track, bus) {
|
||||
return routeObj;
|
||||
}
|
||||
|
||||
// Bypass theo LOẠI nội dung track (dùng chung: createMasteringRoute, sync
|
||||
// effect, startTrackPlayback re-apply) — MIDI-only track → nút ♪; có clips →
|
||||
// nút A. Section track default audioBypass=true — nếu route đọc audioBypass
|
||||
// cho track MIDI → midi bị kéo vào dry (bị bypass) dù ♪ tắt.
|
||||
function routeBypassFor(track) {
|
||||
const hasC = track && ((track.clips && track.clips.length > 0) || !!track.buffer);
|
||||
const hasM = track && track.midiItems && track.midiItems.length > 0;
|
||||
if (hasM && !hasC) {
|
||||
// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
// clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
|
||||
routeBypassFor._lastType = 'midi';
|
||||
return track.midiBypass !== undefined ? !!track.midiBypass : (track.id && trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : false);
|
||||
}
|
||||
routeBypassFor._lastType = 'audio';
|
||||
return track.audioBypass !== undefined ? !!track.audioBypass : (track.id && trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : false);
|
||||
}
|
||||
|
||||
// Live-toggle a route. HARD switch: cancel any pending automation and assign
|
||||
// .value directly (instant, cannot be delayed by the automation queue).
|
||||
function setMasteringRoute(route, bypass) {
|
||||
@@ -14731,9 +14764,13 @@ const App = () => {
|
||||
// tracks state (loads, undo, AI ops…). Placed AFTER the tracks/subTabs/
|
||||
// sessionTabs declarations (TDZ-safe).
|
||||
useEffect(() => {
|
||||
const all = [...(tracks || []), ...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), [])];
|
||||
// Sync theo thứ tự: session/clone TRƯỚC, MAIN tracks SAU — khi id trùng
|
||||
// (section clone giữ id sub-track = id track main), MAIN thắng: map là
|
||||
// fallback cho track không có field; clone có field (reset 0330) nên route
|
||||
// dùng field object (context đúng) — map chéo không ảnh hưởng.
|
||||
const all = [...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), []), ...(tracks || [])];
|
||||
all.forEach(t => {
|
||||
trackMasteringBypassMap[t.id] = !!t.audioBypass;
|
||||
trackMasteringBypassMap[t.id] = !!t.masteringBypass;
|
||||
trackAudioBypassMap[t.id] = !!t.audioBypass;
|
||||
trackMidiBypassMap[t.id] = !!t.midiBypass;
|
||||
});
|
||||
@@ -14896,7 +14933,7 @@ const App = () => {
|
||||
_n.sfDryGain.gain.value = _b ? 1 : 0;
|
||||
}
|
||||
if (_n.route && _n.route.routeGain && _n.route.dryGain) {
|
||||
const _ab = effAudioBypass(_t);
|
||||
const _ab = routeBypassFor(_t);
|
||||
_n.route.routeGain.gain.value = _ab ? 0 : 1;
|
||||
_n.route.dryGain.gain.value = _ab ? 1 : 0;
|
||||
}
|
||||
@@ -16587,6 +16624,14 @@ const App = () => {
|
||||
instrumentId: t.instrumentId || null,
|
||||
instrumentProgram: t.instrumentProgram,
|
||||
instrumentName: t.instrumentName || null,
|
||||
// Reset bypass theo CONTEXT SECTION (KHÔNG kế thừa từ track gốc):
|
||||
// track gốc có thể audioByPass/masteringBypass=true (data lưu cũ) →
|
||||
// kế thừa làm track MIDI section bị kéo vào dry bus khi play (bypass
|
||||
// mastering dù ♪ tắt). Section default: audio raw (A sáng = bypass),
|
||||
// MIDI qua mastering (♪ tắt).
|
||||
masteringBypass: false,
|
||||
audioBypass: true,
|
||||
midiBypass: false,
|
||||
_isSectionClone: true
|
||||
};
|
||||
}) : tracks.filter(t => t.id === trackId).map(t => ({
|
||||
@@ -16605,6 +16650,9 @@ const App = () => {
|
||||
soundfont_program: undefined,
|
||||
instrument_source: null,
|
||||
synth_engine: undefined,
|
||||
masteringBypass: false,
|
||||
audioBypass: true,
|
||||
midiBypass: false,
|
||||
_isSectionClone: true
|
||||
}));
|
||||
setSessionTabs(prev => [...prev, { id: tabId, name: tabName, sectionId: sectionId, tracks: clonedTracks, color: section.color || null }]);
|
||||
@@ -18661,6 +18709,7 @@ const App = () => {
|
||||
sfDryGain = context.createGain();
|
||||
sfRouteGain.gain.value = sfBypass ? 0 : 1;
|
||||
sfDryGain.gain.value = sfBypass ? 1 : 0;
|
||||
console.log('[Bypass] sfEntry created track', track.id, 'midiBypass=' + sfBypass, sfBypass ? 'DRY' : 'MASTERING', '| midiItems:', (track.midiItems || []).length);
|
||||
sfPan.connect(sfRouteGain);
|
||||
sfPan.connect(sfDryGain);
|
||||
sfRouteGain.connect(masterBus.input);
|
||||
@@ -18758,7 +18807,16 @@ const App = () => {
|
||||
const _activeTabId = activeTabRef.current;
|
||||
const _activeSub = subTabsRef.current ? subTabsRef.current.find(s => s.id === _activeTabId) : null;
|
||||
if (_activeSub && _activeSub.type === 'PIANO_ROLL') {
|
||||
const _prNode = activeTrackNodesRef.current[_activeSub.trackId];
|
||||
// PIANO ROLL của track TRONG SECTION (parent_tab_id = session_*): id
|
||||
// track trùng main track id → lookup key = id trúng node MAIN (route
|
||||
// theo ♪ main — sai context → MIDI bị kéo dry). ƯU TIÊN SUB-NODE
|
||||
// (key <parent>_sub_<id> — route theo ♪ section + thẳng masterBus);
|
||||
// không có sub-node → null (masterBus.input — qua mastering).
|
||||
let _prNode = activeTrackNodesRef.current[_activeSub.trackId];
|
||||
if (_activeSub.parent_tab_id && _activeSub.parent_tab_id.startsWith('session_')) {
|
||||
const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId));
|
||||
_prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null;
|
||||
}
|
||||
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
if (_prNode.sfEntry) {
|
||||
window.SonicSF.setOutputDestination(_prNode.sfEntry);
|
||||
@@ -18779,23 +18837,40 @@ const App = () => {
|
||||
}
|
||||
}
|
||||
const midiAudible = list.filter(t => (t.midiItems && t.midiItems.length > 0) && computeTrackAudibleGain(list, t) > 0);
|
||||
console.log('[SFRoute] activeTab=' + _activeTabId, 'midiAudible=' + midiAudible.map(t => t.id + '(midi' + (t.midiItems || []).length + ')').join(',') || 'none');
|
||||
if (midiAudible.length === 1) {
|
||||
const t = midiAudible[0];
|
||||
const node = activeTrackNodesRef.current[t.id];
|
||||
// SECTION-TAB: track id của section clone TRÙNG main track id → node
|
||||
// chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI
|
||||
// section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
|
||||
// route theo ♪ section + thẳng masterBus.input) — không có sub-node →
|
||||
// null (SF về masterBus.input — qua mastering, KHÔNG bypass).
|
||||
let node = activeTrackNodesRef.current[t.id];
|
||||
if (_activeTabId && _activeTabId.startsWith('session_')) {
|
||||
const subKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + t.id));
|
||||
node = subKeys.length ? activeTrackNodesRef.current[subKeys[0]] : null;
|
||||
}
|
||||
// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
|
||||
// PWR ON). The ♪ button only switches the post-FX route (sfRouteGain /
|
||||
// sfDryGain) to skip or include the Mastering FX Chain.
|
||||
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING'));
|
||||
window.SonicSF.setOutputDestination(node.sfEntry);
|
||||
// Ép route qua mastering chain NGAY tại thời điểm routing.
|
||||
if (node.sfRouteGain && node.sfDryGain) {
|
||||
const _b = effMidiBypass(t.id);
|
||||
// ⚠️ FIX: truyền track OBJECT (t) — trước đây truyền t.id (string)
|
||||
// → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn
|
||||
// false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua
|
||||
// đường này.
|
||||
const _b = effMidiBypass(t);
|
||||
node.sfRouteGain.gain.value = _b ? 0 : 1;
|
||||
node.sfDryGain.gain.value = _b ? 1 : 0;
|
||||
console.log('[Bypass] sf route sync track', t.id, 'midiBypass=' + _b, _b ? 'DRY' : 'MASTERING');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING'));
|
||||
window.SonicSF.setOutputDestination(node.gainNode);
|
||||
return;
|
||||
}
|
||||
@@ -18909,21 +18984,35 @@ const App = () => {
|
||||
// RULES: sub-track có CLIPS → nút A (audioBypass); có MIDI → nút ♪ (midiBypass).
|
||||
const subHasClips = (subTrack.clips && subTrack.clips.length > 0) || !!subTrack.buffer;
|
||||
const subBypass = subHasClips
|
||||
? (trackAudioBypassMap[subTrack.id] !== undefined ? !!trackAudioBypassMap[subTrack.id] : !!(subTrack.audioBypass ?? subTrack.masteringBypass))
|
||||
: (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : !!(subTrack.midiBypass ?? subTrack.masteringBypass));
|
||||
? (subTrack.audioBypass !== undefined ? !!subTrack.audioBypass : (trackAudioBypassMap[subTrack.id] !== undefined ? !!trackAudioBypassMap[subTrack.id] : false))
|
||||
: (subTrack.midiBypass !== undefined ? !!subTrack.midiBypass : (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : false));
|
||||
const routeGain = context.createGain();
|
||||
const dryGain = context.createGain();
|
||||
routeGain.gain.value = subBypass ? 0 : 1;
|
||||
dryGain.gain.value = subBypass ? 1 : 0;
|
||||
gainNode.connect(routeGain);
|
||||
gainNode.connect(dryGain);
|
||||
routeGain.connect(parentNode);
|
||||
// analyserNode cho VU meter của SECTION-TAB track strip (VU tick đọc
|
||||
// node.analyserNode — sub-node trước đây không có → VU không animation).
|
||||
const subAnalyser = context.createAnalyser();
|
||||
subAnalyser.fftSize = 256;
|
||||
subAnalyser.smoothingTimeConstant = 0.4;
|
||||
gainNode.connect(subAnalyser);
|
||||
subAnalyser.connect(routeGain);
|
||||
subAnalyser.connect(dryGain);
|
||||
// Mastering path → masterBus.input TRỰC TIẾP (không qua parent node:
|
||||
// parent có route riêng với bypass riêng — parent audio_bypass=true →
|
||||
// nối qua parent làm MIDI/audio section bị kéo vào dry bus = bypass
|
||||
// mastering dù sub-track ♪/A đang tắt).
|
||||
if (window.masterBus && window.masterBus.input) {
|
||||
routeGain.connect(window.masterBus.input);
|
||||
} else {
|
||||
routeGain.connect(parentNode);
|
||||
}
|
||||
if (window.masterBus && window.masterBus.dryInput) {
|
||||
dryGain.connect(window.masterBus.dryInput);
|
||||
} else {
|
||||
dryGain.connect(parentNode);
|
||||
}
|
||||
node = { gainNode, pannerNode: null, route: { routeGain: routeGain, dryGain: dryGain, _trackId: subTrack.id } };
|
||||
node = { gainNode, pannerNode: null, analyserNode: subAnalyser, route: { routeGain: routeGain, dryGain: dryGain, _trackId: subTrack.id } };
|
||||
activeTrackNodesRef.current[subKey] = node;
|
||||
}
|
||||
return node.gainNode;
|
||||
@@ -18964,6 +19053,17 @@ const App = () => {
|
||||
|
||||
const gainNode = getOrCreateTrackNode(track, context);
|
||||
const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
|
||||
// Re-apply route gains theo content-type NGAY tại thời điểm play — node
|
||||
// có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ)
|
||||
// → first play sau double-click vào SECTION-TAB bị bypass. Re-apply
|
||||
// routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng".
|
||||
const playNode = activeTrackNodesRef.current[track.id];
|
||||
if (playNode && playNode.route && playNode.route.routeGain) {
|
||||
const rb = routeBypassFor(track);
|
||||
playNode.route.routeGain.gain.value = rb ? 0 : 1;
|
||||
playNode.route.dryGain.gain.value = rb ? 1 : 0;
|
||||
console.log('[Bypass] reapply track', track.id, 'contentType=' + (routeBypassFor._lastType ? 'midi' : 'audio'), 'bypass=' + rb, '→ routeGain=' + playNode.route.routeGain.gain.value + '/dryGain=' + playNode.route.dryGain.gain.value);
|
||||
}
|
||||
console.log('[Play] track', track.id, track.name, 'node ok:', !!(gainNode && pannerNode), 'muted:', track.muted);
|
||||
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
|
||||
id: 'default',
|
||||
@@ -19084,6 +19184,15 @@ const App = () => {
|
||||
|
||||
const subNode = getOrCreateSubTrackNode(track, subTrack, context);
|
||||
if (!subNode) return;
|
||||
// Re-apply route sub-node theo content-type mỗi lần schedule —
|
||||
// sub-node có thể STALE (route theo trạng thái cũ) → midi section
|
||||
// bị kéo vào dry. routeBypassFor: MIDI-only → ♪, clips → A.
|
||||
const subNodeObj = activeTrackNodesRef.current[track.id + '_sub_' + subTrack.id];
|
||||
if (subNodeObj && subNodeObj.route && subNodeObj.route.routeGain) {
|
||||
const srb = routeBypassFor(subTrack);
|
||||
subNodeObj.route.routeGain.gain.value = srb ? 0 : 1;
|
||||
subNodeObj.route.dryGain.gain.value = srb ? 1 : 0;
|
||||
}
|
||||
|
||||
// 1. Play clips in subTrack
|
||||
const subClips = subTrack.clips || [];
|
||||
@@ -19166,6 +19275,14 @@ const App = () => {
|
||||
subTrack.synth_engine
|
||||
);
|
||||
}
|
||||
// Trigger VU meter flash đúng nhịp (giống main-track midi 19066)
|
||||
// Cả subTrack (VU section tab) + parent track (VU MAIN — section
|
||||
// item đang play) — midiVuActivity keyed theo track id → trigger
|
||||
// cả 2 để VU main nhảy theo section.
|
||||
if (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8);
|
||||
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -24603,6 +24720,12 @@ STRICT CONSTRAINTS:
|
||||
const node = trackNodes[trackId];
|
||||
const canvas = trackVuRefs.current[key];
|
||||
if (!canvas) return;
|
||||
// Section tab: track nodes nằm dưới key <parent>_sub_<trackId> —
|
||||
// lookup plain key trả undefined → VU không animation. Fallback scan _sub_.
|
||||
const vNode = node || (function() {
|
||||
for (var k in trackNodes) { if (k.endsWith('_sub_' + trackId)) return trackNodes[k]; }
|
||||
return null;
|
||||
})();
|
||||
|
||||
// Only animate when real audio actually passes through the track (solo/mute aware)
|
||||
var vuTracks = activeTracksRef.current || [];
|
||||
@@ -24611,8 +24734,8 @@ STRICT CONSTRAINTS:
|
||||
var isAudible = vuTrack ? (anySoloVU ? !!vuTrack.solo : !vuTrack.muted) : true;
|
||||
|
||||
let audioPeak = 0;
|
||||
if (isAudible && node && node.analyserNode) {
|
||||
const analyser = node.analyserNode;
|
||||
if (isAudible && vNode && vNode.analyserNode) {
|
||||
const analyser = vNode.analyserNode;
|
||||
const data = new Uint8Array(128);
|
||||
analyser.getByteTimeDomainData(data);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
@@ -24620,6 +24743,24 @@ STRICT CONSTRAINTS:
|
||||
if (v > audioPeak) audioPeak = v;
|
||||
}
|
||||
}
|
||||
// Section item trên MAIN track: VU theo sub-nodes của track này (key
|
||||
// <trackId>_sub_*) — âm section đi thẳng masterBus.input (không qua
|
||||
// node chính) → node chính không có tín hiệu → VU track đứng yên.
|
||||
if (audioPeak <= 0.001) {
|
||||
for (var sk in trackNodes) {
|
||||
if (sk.indexOf(trackId + '_sub_') === 0) {
|
||||
const sn = trackNodes[sk];
|
||||
if (sn && sn.analyserNode) {
|
||||
const d2 = new Uint8Array(128);
|
||||
sn.analyserNode.getByteTimeDomainData(d2);
|
||||
for (let i = 0; i < d2.length; i++) {
|
||||
const v = Math.abs(d2[i] - 128) / 128;
|
||||
if (v > audioPeak) audioPeak = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play
|
||||
// MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
|
||||
|
||||
@@ -25,18 +25,34 @@ const trackMasteringBypassMap={};const trackAudioBypassMap={};const trackMidiByp
|
||||
const masteringChainOn=()=>!!(window.currentMasteringSettings&&window.currentMasteringSettings.masterConnected&&!window.currentMasteringSettings.isBypassed);// ♪ bypass hiệu lực CHỈ khi mastering chain TẮT — khi chain ON, MỌI track
|
||||
// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải
|
||||
// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ.
|
||||
const effMidiBypass=track=>{// GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules)
|
||||
return trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:!!(track.midiBypass??track.masteringBypass);};const effAudioBypass=track=>{// GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
|
||||
return trackAudioBypassMap[track.id]!==undefined?!!trackAudioBypassMap[track.id]:!!(track.audioBypass??track.masteringBypass);};// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||
const effMidiBypass=track=>{// GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules).
|
||||
// ƯU TIÊN track object (field midiBypass): track object phân biệt đúng
|
||||
// context (main track vs section clone — id TRÙNG nhau); map keyed theo id
|
||||
// gây collision (clone section ghi đè map của main track cùng id → MIDI
|
||||
// section bị route theo ♪ main = bypass sai). Map chỉ là fallback realtime.
|
||||
if(track&&track.midiBypass!==undefined)return!!track.midiBypass;if(track&&track.id&&trackMidiBypassMap[track.id]!==undefined)return!!trackMidiBypassMap[track.id];return false;};const effAudioBypass=track=>{// GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
|
||||
if(track&&track.audioBypass!==undefined)return!!track.audioBypass;if(track&&track.id&&trackAudioBypassMap[track.id]!==undefined)return!!trackAudioBypassMap[track.id];return false;};// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
|
||||
function createMasteringRoute(ctx,track,bus){// Prefer the live bypass map (synced from the tracks state on every render),
|
||||
// falling back to the track object — this guarantees the A-button toggle is
|
||||
// picked up even if a stale track object is passed in.
|
||||
let bypass=false;if(track&&track.id&&trackAudioBypassMap[track.id]!==undefined){bypass=!!trackAudioBypassMap[track.id];}else if(track){bypass=!!(track.audioBypass??track.masteringBypass);}// Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement)
|
||||
let bypass=false;// Bypass theo LOẠI nội dung track (rules nút A/♪): track MIDI-only → nút ♪
|
||||
// quyết định — KHÔNG dùng audioBypass (section track default audioBypass=true
|
||||
// → route dry → midi section bị bypass dù ♪ tắt — log: SF → track node
|
||||
// (sfEntry) + noteon chạy nhưng âm dry).
|
||||
const hasClips=track&&(track.clips&&track.clips.length>0||!!track.buffer);const hasMidi=track&&track.midiItems&&track.midiItems.length>0;if(hasMidi&&!hasClips){// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
// clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
|
||||
bypass=track.midiBypass!==undefined?!!track.midiBypass:track.id&&trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:false;}else if(track){bypass=track.audioBypass!==undefined?!!track.audioBypass:track.id&&trackAudioBypassMap[track.id]!==undefined?!!trackAudioBypassMap[track.id]:false;}// Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement)
|
||||
// ⚠️ GỠ 17:00 — override nuốt nút A/♪ (log: audioBypass=true nhưng
|
||||
// routeGain=1) — rules hiện tại: NÚT A/♪ quyết định bypass, không phụ thuộc
|
||||
// mastering ON.
|
||||
const routeGain=ctx.createGain();const dryGain=ctx.createGain();const masterDest=bus?bus.input:ctx.destination;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Live-toggle a route. HARD switch: cancel any pending automation and assign
|
||||
const routeGain=ctx.createGain();const dryGain=ctx.createGain();const masterDest=bus?bus.input:ctx.destination;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Bypass theo LOẠI nội dung track (dùng chung: createMasteringRoute, sync
|
||||
// effect, startTrackPlayback re-apply) — MIDI-only track → nút ♪; có clips →
|
||||
// nút A. Section track default audioBypass=true — nếu route đọc audioBypass
|
||||
// cho track MIDI → midi bị kéo vào dry (bị bypass) dù ♪ tắt.
|
||||
function routeBypassFor(track){const hasC=track&&(track.clips&&track.clips.length>0||!!track.buffer);const hasM=track&&track.midiItems&&track.midiItems.length>0;if(hasM&&!hasC){// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
// clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
|
||||
routeBypassFor._lastType='midi';return track.midiBypass!==undefined?!!track.midiBypass:track.id&&trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:false;}routeBypassFor._lastType='audio';return track.audioBypass!==undefined?!!track.audioBypass:track.id&&trackAudioBypassMap[track.id]!==undefined?!!trackAudioBypassMap[track.id]:false;}// Live-toggle a route. HARD switch: cancel any pending automation and assign
|
||||
// .value directly (instant, cannot be delayed by the automation queue).
|
||||
function setMasteringRoute(route,bypass){if(!route)return;const on=!!bypass;try{const ctx=typeof getAudioContext==='function'?getAudioContext():null;if(!ctx)return;const t=ctx.currentTime;route.routeGain.gain.cancelScheduledValues(t);route.dryGain.gain.cancelScheduledValues(t);route.routeGain.gain.value=on?0:1;route.dryGain.gain.value=on?1:0;route._bypass=on;console.log('[Bypass] track',route._trackId,'audioBypass='+on,'→',on?'DRY BUS (bỏ mastering + bỏ track FX)':'MASTERING CHAIN (qua FX + mastering)');}catch(e){console.warn('setMasteringRoute error:',e);}}// Realtime mute/solo: audible linear gain for a track given the full track list
|
||||
// of the CURRENT context. Solo semantics: if ANY track is soloed, only soloed
|
||||
@@ -663,7 +679,11 @@ const trackAudibleRef=useRef({});// Audio-relevant signature (mute/solo/vol/bypa
|
||||
const trackAudioSyncSigRef=useRef(null);// Keep the mastering-bypass routing map + realtime mute/solo in sync with the
|
||||
// tracks state (loads, undo, AI ops…). Placed AFTER the tracks/subTabs/
|
||||
// sessionTabs declarations (TDZ-safe).
|
||||
useEffect(()=>{const all=[...(tracks||[]),...(sessionTabs||[]).reduce((acc,s)=>acc.concat(s.tracks||[]),[])];all.forEach(t=>{trackMasteringBypassMap[t.id]=!!t.audioBypass;trackAudioBypassMap[t.id]=!!t.audioBypass;trackMidiBypassMap[t.id]=!!t.midiBypass;});const list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:all;// Audio-relevant signature: đổi MÀU/rename track (không liên quan audio)
|
||||
useEffect(()=>{// Sync theo thứ tự: session/clone TRƯỚC, MAIN tracks SAU — khi id trùng
|
||||
// (section clone giữ id sub-track = id track main), MAIN thắng: map là
|
||||
// fallback cho track không có field; clone có field (reset 0330) nên route
|
||||
// dùng field object (context đúng) — map chéo không ảnh hưởng.
|
||||
const all=[...(sessionTabs||[]).reduce((acc,s)=>acc.concat(s.tracks||[]),[]),...(tracks||[])];all.forEach(t=>{trackMasteringBypassMap[t.id]=!!t.masteringBypass;trackAudioBypassMap[t.id]=!!t.audioBypass;trackMidiBypassMap[t.id]=!!t.midiBypass;});const list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:all;// Audio-relevant signature: đổi MÀU/rename track (không liên quan audio)
|
||||
// KHÔNG được trigger re-sync/re-route (tránh mất âm khi play sau đó —
|
||||
// updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm).
|
||||
const audioSig=list.map(t=>(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0)+':'+(t.audioBypass?'1':'0')+(t.midiBypass?'1':'0')+':'+(t.midiItems||[]).length+':'+(t.clips||[]).length+':'+(t.sections||[]).length).join('|');if(trackAudioSyncSigRef.current===audioSig)return;trackAudioSyncSigRef.current=audioSig;list.forEach(t=>{// ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes
|
||||
@@ -683,7 +703,7 @@ const[isMandatoryLogin,setIsMandatoryLogin]=useState(false);const[profileModalOp
|
||||
const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
|
||||
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
|
||||
// ngay lên node đang phát (không chờ tracks effect).
|
||||
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=effAudioBypass(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót →
|
||||
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót →
|
||||
// audioclip trong section hiện "(audio chưa được tải)" sau save+open).
|
||||
const updatedSections=await Promise.all((t.sections||[]).map(async s=>{const updatedSTracks=await Promise.all((s.tracks||[]).map(async st=>{const updatedSClips=await Promise.all((st.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||st.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));return{...st,clips:updatedSClips};}));return{...s,tracks:updatedSTracks};}));if(trackBuffer&&updatedClips.length===0){const clipId=`default_${t.id}`;return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:[{id:clipId,buffer:trackBuffer,startTime:t.startTime||0,name:t.name,speed:1.0}],sections:updatedSections};}return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:updatedClips,sections:updatedSections};}));if(hasLoadedAny){setTracks(prev=>{// Merge by TRACK ID (not array index): if the state changed between the
|
||||
// fetch start and now (e.g. another project opened), index-based merging
|
||||
@@ -751,7 +771,12 @@ setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast
|
||||
const handleEditSectionInTab=(trackId,sectionId)=>{const track=tracks.find(t=>t.id===trackId);if(!track)return;const section=(track.sections||[]).find(s=>s.id===sectionId);if(!section)return;const existing=sessionTabs.find(s=>s.sectionId===sectionId);if(existing){setActiveTab(existing.id);showToast(`Tab "${section.name}" already open.`,'info');return;}const tabId='session_'+Date.now();const tabName=section.name||'Section';const clonedTracks=section.tracks?section.tracks.map(t=>{// Giữ CLIPS + gắn lại buffer THẬT (JSON.parse(JSON.stringify()) DROP
|
||||
// AudioBuffer → audioclip mất khỏi section editor). Trước đây clips:[]
|
||||
// → section item mở bằng nhấp đôi không chứa audioclip.
|
||||
const base=JSON.parse(JSON.stringify(t));const srcClips=(t.clips||[]).map(c=>({...c,buffer:c.buffer||null}));return{...base,clips:srcClips,sections:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:t.instrumentId||null,instrumentProgram:t.instrumentProgram,instrumentName:t.instrumentName||null,_isSectionClone:true};}):tracks.filter(t=>t.id===trackId).map(t=>({...t,clips:[],sections:[],midiItems:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:null,instrumentProgram:undefined,instrumentName:null,soundfont_id:null,soundfont_bank:undefined,soundfont_program:undefined,instrument_source:null,synth_engine:undefined,_isSectionClone:true}));setSessionTabs(prev=>[...prev,{id:tabId,name:tabName,sectionId:sectionId,tracks:clonedTracks,color:section.color||null}]);setActiveTab(tabId);};// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
|
||||
const base=JSON.parse(JSON.stringify(t));const srcClips=(t.clips||[]).map(c=>({...c,buffer:c.buffer||null}));return{...base,clips:srcClips,sections:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:t.instrumentId||null,instrumentProgram:t.instrumentProgram,instrumentName:t.instrumentName||null,// Reset bypass theo CONTEXT SECTION (KHÔNG kế thừa từ track gốc):
|
||||
// track gốc có thể audioByPass/masteringBypass=true (data lưu cũ) →
|
||||
// kế thừa làm track MIDI section bị kéo vào dry bus khi play (bypass
|
||||
// mastering dù ♪ tắt). Section default: audio raw (A sáng = bypass),
|
||||
// MIDI qua mastering (♪ tắt).
|
||||
masteringBypass:false,audioBypass:true,midiBypass:false,_isSectionClone:true};}):tracks.filter(t=>t.id===trackId).map(t=>({...t,clips:[],sections:[],midiItems:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:null,instrumentProgram:undefined,instrumentName:null,soundfont_id:null,soundfont_bank:undefined,soundfont_program:undefined,instrument_source:null,synth_engine:undefined,masteringBypass:false,audioBypass:true,midiBypass:false,_isSectionClone:true}));setSessionTabs(prev=>[...prev,{id:tabId,name:tabName,sectionId:sectionId,tracks:clonedTracks,color:section.color||null}]);setActiveTab(tabId);};// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
|
||||
const applySubTab=tabId=>{const subTab=subTabs.find(s=>s.id===tabId);if(!subTab||!subTab.buffer)return;const track=activeTracks.find(t=>t.id===subTab.trackId);if(!track||!track.buffer)return;const beforeSnap=captureTrackSnapshot(subTab.trackId);// Clone buffer and apply effects
|
||||
const ctx=getAudioContext();const sr=subTab.buffer.sampleRate;const eff=subTab.buffer.getChannelData(0);let resultBuffer=ctx.createBuffer(1,eff.length,sr);let resultData=resultBuffer.getChannelData(0);resultData.set(eff);// Apply effects inline
|
||||
const fx=subTab.effects||{};const applyResample=(data,ratio)=>{const newLen=Math.round(data.length*ratio);const out=new Float32Array(newLen);for(let i=0;i<newLen;i++){const srcIdx=i/ratio;const idx0=Math.floor(srcIdx);const idx1=Math.min(idx0+1,data.length-1);const frac=srcIdx-idx0;out[i]=data[idx0]*(1-frac)+data[idx1]*frac;}return out;};if(fx.reverse){const rev=new Float32Array(resultData);for(let i=0;i<resultData.length;i++)rev[i]=resultData[resultData.length-1-i];resultData.set(rev);}if(fx.gainDb!==0){const gain=Math.pow(10,fx.gainDb/20);for(let i=0;i<resultData.length;i++)resultData[i]=Math.max(-1,Math.min(1,resultData[i]*gain));}if(fx.fadeInMs>0){const fadeSamples=Math.min(resultData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i<fadeSamples;i++)resultData[i]*=i/fadeSamples;}// Graph Editor fade curves (trigonometric, 15_GRAPH_EDIT.md §2.3)
|
||||
@@ -914,7 +939,7 @@ sfEntry.gain.value=1;sfOut.connect(sfPan);// Feed the soundfont into the shared
|
||||
sfOut.connect(scopeSplitter);// ♪ button: bypass Mastering FX Chain for the soundfont ONLY (track FX
|
||||
// Rack modules are still applied — PWR controls those). sfRouteGain →
|
||||
// masterBus.input (mastering), sfDryGain → dry bus (skip mastering).
|
||||
const sfBypass=effMidiBypass(track);sfRouteGain=context.createGain();sfDryGain=context.createGain();sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);sfOut.connect(sfAnalyser);const sfModsBuilt=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,context,m.params);}catch(e){return null;}}).filter(Boolean):[];sfMods=sfModsBuilt;let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);}node={gainNode,pannerNode,fxStopFn,analyserNode,route,fxEntry,fxLegacyIn,scopeAnalyserL,scopeAnalyserR,sfEntry,sfOut,sfPan,sfAnalyser,sfRouteGain,sfDryGain,fxMods:chainMods,sfMods};// Realtime mute/solo: apply the track's current mute/solo/volume state to
|
||||
const sfBypass=effMidiBypass(track);sfRouteGain=context.createGain();sfDryGain=context.createGain();sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;console.log('[Bypass] sfEntry created track',track.id,'midiBypass='+sfBypass,sfBypass?'DRY':'MASTERING','| midiItems:',(track.midiItems||[]).length);sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);sfOut.connect(sfAnalyser);const sfModsBuilt=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,context,m.params);}catch(e){return null;}}).filter(Boolean):[];sfMods=sfModsBuilt;let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);}node={gainNode,pannerNode,fxStopFn,analyserNode,route,fxEntry,fxLegacyIn,scopeAnalyserL,scopeAnalyserR,sfEntry,sfOut,sfPan,sfAnalyser,sfRouteGain,sfDryGain,fxMods:chainMods,sfMods};// Realtime mute/solo: apply the track's current mute/solo/volume state to
|
||||
// the fresh node so items of muted/soloed tracks start correctly.
|
||||
const trackList=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:[track];const nodeAudibleGain=computeTrackAudibleGain(trackList,track);setTrackNodeGain(node,nodeAudibleGain);if(node.sfEntry){try{const g=typeof nodeAudibleGain==='number'&&isFinite(nodeAudibleGain)&&!isNaN(nodeAudibleGain)?nodeAudibleGain:1.0;node.sfEntry.gain.setTargetAtTime(g,context.currentTime,0.02);}catch(e){}}activeTrackNodesRef.current[track.id]=node;console.log('[Bypass] node created track',track.id,'initial audioBypass=',!!trackAudioBypassMap[track.id],'routeGain=',node.route.routeGain.gain.value,'dryGain=',node.route.dryGain.gain.value);updateSfRouting();}return node.gainNode;};// Per-context audio graph reconstruction (unified_fx_rack_panel.md §III.2):
|
||||
// re-routes ONLY this track's fx chain (fxEntry → active modules → fxLegacyIn),
|
||||
@@ -930,13 +955,27 @@ const updateSfRouting=()=>{try{const list=activeTracksRef.current&&activeTracksR
|
||||
// TRACK đang edit (sfEntry → FX Rack riêng, fallback gainNode) để fader/
|
||||
// pan/FX áp đúng cho notes đang nghe — kể cả khi project có >1 track MIDI
|
||||
// audible (bình thường rơi về masterBus.input, mất FX track).
|
||||
const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;if(_activeSub&&_activeSub.type==='PIANO_ROLL'){const _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có
|
||||
const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;if(_activeSub&&_activeSub.type==='PIANO_ROLL'){// PIANO ROLL của track TRONG SECTION (parent_tab_id = session_*): id
|
||||
// track trùng main track id → lookup key = id trúng node MAIN (route
|
||||
// theo ♪ main — sai context → MIDI bị kéo dry). ƯU TIÊN SUB-NODE
|
||||
// (key <parent>_sub_<id> — route theo ♪ section + thẳng masterBus);
|
||||
// không có sub-node → null (masterBus.input — qua mastering).
|
||||
let _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_activeSub.parent_tab_id&&_activeSub.parent_tab_id.startsWith('session_')){const _prSubKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+_activeSub.trackId));_prNode=_prSubKeys.length?activeTrackNodesRef.current[_prSubKeys[0]]:null;}if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có
|
||||
// thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON).
|
||||
if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);if(midiAudible.length===1){const t=midiAudible[0];const node=activeTrackNodesRef.current[t.id];// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
|
||||
if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node
|
||||
// chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI
|
||||
// section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
|
||||
// route theo ♪ section + thẳng masterBus.input) — không có sub-node →
|
||||
// null (SF về masterBus.input — qua mastering, KHÔNG bypass).
|
||||
let node=activeTrackNodesRef.current[t.id];if(_activeTabId&&_activeTabId.startsWith('session_')){const subKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+t.id));node=subKeys.length?activeTrackNodesRef.current[subKeys[0]]:null;}// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
|
||||
// PWR ON). The ♪ button only switches the post-FX route (sfRouteGain /
|
||||
// sfDryGain) to skip or include the Mastering FX Chain.
|
||||
if(node&&node.sfEntry&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing.
|
||||
if(node.sfRouteGain&&node.sfDryGain){const _b=effMidiBypass(t.id);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── MIDI preview cache (bounce nhanh offline) ──
|
||||
if(node&&node.sfEntry&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=sfEntry node',t.id,'sfRouteGain='+(node.sfRouteGain?node.sfRouteGain.gain.value:'MISSING'),'sfDryGain='+(node.sfDryGain?node.sfDryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing.
|
||||
if(node.sfRouteGain&&node.sfDryGain){// ⚠️ FIX: truyền track OBJECT (t) — trước đây truyền t.id (string)
|
||||
// → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn
|
||||
// false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua
|
||||
// đường này.
|
||||
const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── MIDI preview cache (bounce nhanh offline) ──
|
||||
// Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo
|
||||
// track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache
|
||||
// (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime.
|
||||
@@ -957,9 +996,19 @@ window.__getTrackSfFxModule=(trackId,type)=>{const node=activeTrackNodesRef.curr
|
||||
// effAudioBypass (override mastering-ON ép false → route luôn no-bypass
|
||||
// lúc tạo node → nút A phải bấm 2 lần; stop+play → node tái tạo → mất).
|
||||
// RULES: sub-track có CLIPS → nút A (audioBypass); có MIDI → nút ♪ (midiBypass).
|
||||
const subHasClips=subTrack.clips&&subTrack.clips.length>0||!!subTrack.buffer;const subBypass=subHasClips?trackAudioBypassMap[subTrack.id]!==undefined?!!trackAudioBypassMap[subTrack.id]:!!(subTrack.audioBypass??subTrack.masteringBypass):trackMidiBypassMap[subTrack.id]!==undefined?!!trackMidiBypassMap[subTrack.id]:!!(subTrack.midiBypass??subTrack.masteringBypass);const routeGain=context.createGain();const dryGain=context.createGain();routeGain.gain.value=subBypass?0:1;dryGain.gain.value=subBypass?1:0;gainNode.connect(routeGain);gainNode.connect(dryGain);routeGain.connect(parentNode);if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);const program=track?track.instrumentProgram:undefined;var prevCh=track?assignTrackMidiChannel(track,activeTracks):0;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với
|
||||
const subHasClips=subTrack.clips&&subTrack.clips.length>0||!!subTrack.buffer;const subBypass=subHasClips?subTrack.audioBypass!==undefined?!!subTrack.audioBypass:trackAudioBypassMap[subTrack.id]!==undefined?!!trackAudioBypassMap[subTrack.id]:false:subTrack.midiBypass!==undefined?!!subTrack.midiBypass:trackMidiBypassMap[subTrack.id]!==undefined?!!trackMidiBypassMap[subTrack.id]:false;const routeGain=context.createGain();const dryGain=context.createGain();routeGain.gain.value=subBypass?0:1;dryGain.gain.value=subBypass?1:0;// analyserNode cho VU meter của SECTION-TAB track strip (VU tick đọc
|
||||
// node.analyserNode — sub-node trước đây không có → VU không animation).
|
||||
const subAnalyser=context.createAnalyser();subAnalyser.fftSize=256;subAnalyser.smoothingTimeConstant=0.4;gainNode.connect(subAnalyser);subAnalyser.connect(routeGain);subAnalyser.connect(dryGain);// Mastering path → masterBus.input TRỰC TIẾP (không qua parent node:
|
||||
// parent có route riêng với bypass riêng — parent audio_bypass=true →
|
||||
// nối qua parent làm MIDI/audio section bị kéo vào dry bus = bypass
|
||||
// mastering dù sub-track ♪/A đang tắt).
|
||||
if(window.masterBus&&window.masterBus.input){routeGain.connect(window.masterBus.input);}else{routeGain.connect(parentNode);}if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,analyserNode:subAnalyser,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);const program=track?track.instrumentProgram:undefined;var prevCh=track?assignTrackMidiChannel(track,activeTracks):0;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với
|
||||
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
|
||||
scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING'));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;console.log('[Play] track',track.id,track.name,'node ok:',!!(gainNode&&pannerNode),'muted:',track.muted);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback
|
||||
scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING'));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;// Re-apply route gains theo content-type NGAY tại thời điểm play — node
|
||||
// có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ)
|
||||
// → first play sau double-click vào SECTION-TAB bị bypass. Re-apply
|
||||
// routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng".
|
||||
const playNode=activeTrackNodesRef.current[track.id];if(playNode&&playNode.route&&playNode.route.routeGain){const rb=routeBypassFor(track);playNode.route.routeGain.gain.value=rb?0:1;playNode.route.dryGain.gain.value=rb?1:0;console.log('[Bypass] reapply track',track.id,'contentType='+(routeBypassFor._lastType?'midi':'audio'),'bypass='+rb,'→ routeGain='+playNode.route.routeGain.gain.value+'/dryGain='+playNode.route.dryGain.gain.value);}console.log('[Play] track',track.id,track.name,'node ok:',!!(gainNode&&pannerNode),'muted:',track.muted);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback
|
||||
const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){// Preview cache: capture the soundfont (pre track-FX) for fast offline export
|
||||
ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth
|
||||
if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items)
|
||||
@@ -970,11 +1019,18 @@ if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(track.id,note.velo
|
||||
if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data)
|
||||
// This block is intentionally empty - audio clips already play above
|
||||
}// SECTION items playback
|
||||
const sections=track.sections||[];if(activeTab==='main'&§ions.length>0){const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const secondsPerBar=secondsPerBeat*4;sections.forEach(sec=>{const secStart=sec.start||0;const secEnd=secStart+(sec.duration||0);const openTab=sessionTabsRef.current?sessionTabsRef.current.find(st=>st.sectionId===sec.sectionId||st.sectionId===sec.id):null;const secTracks=openTab?openTab.tracks:sec.tracks||[];const hasSubSolo=secTracks.some(st=>st.solo);secTracks.forEach((subTrack,subIdx)=>{const isSubPlayable=hasSubSolo?subTrack.solo:!subTrack.muted;if(!isSubPlayable)return;const subNode=getOrCreateSubTrackNode(track,subTrack,context);if(!subNode)return;// 1. Play clips in subTrack
|
||||
const sections=track.sections||[];if(activeTab==='main'&§ions.length>0){const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const secondsPerBar=secondsPerBeat*4;sections.forEach(sec=>{const secStart=sec.start||0;const secEnd=secStart+(sec.duration||0);const openTab=sessionTabsRef.current?sessionTabsRef.current.find(st=>st.sectionId===sec.sectionId||st.sectionId===sec.id):null;const secTracks=openTab?openTab.tracks:sec.tracks||[];const hasSubSolo=secTracks.some(st=>st.solo);secTracks.forEach((subTrack,subIdx)=>{const isSubPlayable=hasSubSolo?subTrack.solo:!subTrack.muted;if(!isSubPlayable)return;const subNode=getOrCreateSubTrackNode(track,subTrack,context);if(!subNode)return;// Re-apply route sub-node theo content-type mỗi lần schedule —
|
||||
// sub-node có thể STALE (route theo trạng thái cũ) → midi section
|
||||
// bị kéo vào dry. routeBypassFor: MIDI-only → ♪, clips → A.
|
||||
const subNodeObj=activeTrackNodesRef.current[track.id+'_sub_'+subTrack.id];if(subNodeObj&&subNodeObj.route&&subNodeObj.route.routeGain){const srb=routeBypassFor(subTrack);subNodeObj.route.routeGain.gain.value=srb?0:1;subNodeObj.route.dryGain.gain.value=srb?1:0;}// 1. Play clips in subTrack
|
||||
const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd]
|
||||
const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack
|
||||
const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet
|
||||
if(noteStartMain>=secStart&¬eStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
|
||||
if(noteStartMain>=secStart&¬eStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);}// Trigger VU meter flash đúng nhịp (giống main-track midi 19066)
|
||||
// Cả subTrack (VU section tab) + parent track (VU MAIN — section
|
||||
// item đang play) — midiVuActivity keyed theo track id → trigger
|
||||
// cả 2 để VU main nhảy theo section.
|
||||
if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity(track.id,note.velocity||0.8);}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
|
||||
const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control
|
||||
const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track
|
||||
// node already wires gainNode → fxEntry → [FX modules] → fxLegacyIn →
|
||||
@@ -1211,8 +1267,13 @@ const masterVUAnimRef=useRef(null);useEffect(()=>{function tick(){try{// Khi STO
|
||||
// activity → hết animation dính + master không full sau khi dừng play.
|
||||
const isRecActive=activeAudioRecordersRef.current&&Object.keys(activeAudioRecordersRef.current).length>0;if(!isPlaying&&!isRecActive){setMasterVU(0);setMasterMeterPeak(0);Object.keys(trackVuRefs.current).forEach(k=>{const c=trackVuRefs.current[k];if(!c)return;if(k.endsWith('_mixer'))drawMixerVuMeter(c,0);else drawVuMeter(c,-60);});masterVUAnimRef.current=requestAnimationFrame(tick);return;}// 1. Master VU Meter
|
||||
if(masterBus&&masterBus.analyser){const data=new Uint8Array(128);masterBus.analyser.getByteTimeDomainData(data);let peak=0;for(let i=0;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>peak)peak=v;}setMasterVU(peak);setMasterMeterPeak(prev=>Math.max(prev*0.97,peak));}// 2. Track VU Meters
|
||||
const trackNodes=activeTrackNodesRef.current||{};const activeKeys=Object.keys(trackVuRefs.current);activeKeys.forEach(key=>{const trackId=key.replace('_mixer','');const isRecordingThisTrack=activeAudioRecordersRef.current&&activeAudioRecordersRef.current[trackId];if(isRecordingThisTrack)return;const node=trackNodes[trackId];const canvas=trackVuRefs.current[key];if(!canvas)return;// Only animate when real audio actually passes through the track (solo/mute aware)
|
||||
var vuTracks=activeTracksRef.current||[];var anySoloVU=vuTracks.some(function(t){return t.solo;});var vuTrack=vuTracks.find(function(t){return t.id===trackId;});var isAudible=vuTrack?anySoloVU?!!vuTrack.solo:!vuTrack.muted:true;let audioPeak=0;if(isAudible&&node&&node.analyserNode){const analyser=node.analyserNode;const data=new Uint8Array(128);analyser.getByteTimeDomainData(data);for(let i=0;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>audioPeak)audioPeak=v;}}// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play
|
||||
const trackNodes=activeTrackNodesRef.current||{};const activeKeys=Object.keys(trackVuRefs.current);activeKeys.forEach(key=>{const trackId=key.replace('_mixer','');const isRecordingThisTrack=activeAudioRecordersRef.current&&activeAudioRecordersRef.current[trackId];if(isRecordingThisTrack)return;const node=trackNodes[trackId];const canvas=trackVuRefs.current[key];if(!canvas)return;// Section tab: track nodes nằm dưới key <parent>_sub_<trackId> —
|
||||
// lookup plain key trả undefined → VU không animation. Fallback scan _sub_.
|
||||
const vNode=node||function(){for(var k in trackNodes){if(k.endsWith('_sub_'+trackId))return trackNodes[k];}return null;}();// Only animate when real audio actually passes through the track (solo/mute aware)
|
||||
var vuTracks=activeTracksRef.current||[];var anySoloVU=vuTracks.some(function(t){return t.solo;});var vuTrack=vuTracks.find(function(t){return t.id===trackId;});var isAudible=vuTrack?anySoloVU?!!vuTrack.solo:!vuTrack.muted:true;let audioPeak=0;if(isAudible&&vNode&&vNode.analyserNode){const analyser=vNode.analyserNode;const data=new Uint8Array(128);analyser.getByteTimeDomainData(data);for(let i=0;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>audioPeak)audioPeak=v;}}// Section item trên MAIN track: VU theo sub-nodes của track này (key
|
||||
// <trackId>_sub_*) — âm section đi thẳng masterBus.input (không qua
|
||||
// node chính) → node chính không có tín hiệu → VU track đứng yên.
|
||||
if(audioPeak<=0.001){for(var sk in trackNodes){if(sk.indexOf(trackId+'_sub_')===0){const sn=trackNodes[sk];if(sn&&sn.analyserNode){const d2=new Uint8Array(128);sn.analyserNode.getByteTimeDomainData(d2);for(let i=0;i<d2.length;i++){const v=Math.abs(d2[i]-128)/128;if(v>audioPeak)audioPeak=v;}}}}}// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play
|
||||
// MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
|
||||
// nhảy trong cả 2 trường hợp (không chặn bởi isPlaying).
|
||||
let midiPeak=isAudible?midiVuActivityRef.current[trackId]||0:0;if(midiPeak>0){midiVuActivityRef.current[trackId]=midiPeak*0.90;if(midiVuActivityRef.current[trackId]<0.01){midiVuActivityRef.current[trackId]=0;}}const peak=Math.max(audioPeak,midiPeak);const db=peak>0?20*Math.log10(peak):-60;if(peak>0.001){if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,peak);}else{drawVuMeter(canvas,db);}}else{if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,0);}else{drawVuMeter(canvas,-60);}}});}catch(e){console.warn('VU tick error:',e);}masterVUAnimRef.current=requestAnimationFrame(tick);}masterVUAnimRef.current=requestAnimationFrame(tick);return()=>{if(masterVUAnimRef.current)cancelAnimationFrame(masterVUAnimRef.current);};},[isPlaying]);const handleOpenProject=async(projectId,projectName)=>{setAppWarningModal({title:"Mở dự án",message:"Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",isAlert:false,onConfirm:async()=>{try{const proj=await window.SonicAPI.getCloudProject(projectId);const parsed=JSON.parse(proj.data_json);let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(parsed.main_session){const result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.zoom&&setZoom)setZoom(result.zoom);if(result.masteringSettings)setMasteringSettings(result.masteringSettings);}else{restoredTracks=(parsed.tracks||[]).map(function(t){const rest=Object.assign({},t);delete rest.height;return Object.assign({},rest,{buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null});});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(proj.name);setCurrentProjectId(proj.id);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name);localStorage.setItem('sonic_project_id',proj.id);showToast('Đã nạp dự án "'+proj.name+'" thành công!',"success");}catch(e){showToast(e.message||"Lỗi khi nạp dự án","error");}}});};return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"h-full w-full flex flex-col bg-[#1e1e1e]"},/*#__PURE__*/React.createElement("header",{className:"h-9 bg-[#2e2e2e] border-b border-[#181818] flex items-center px-1 shrink-0 select-none"},[{label:'File',items:[{label:'New Project',icon:'file-plus',shortcut:'Ctrl+N',action:()=>{// New project: đóng hết tab + reset MỌI trạng thái về mặc định
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608062345" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070345" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user