Fix: carry expression_curve qua native track render + cache key

This commit is contained in:
2026-08-26 16:20:53 +07:00
parent 5689536fb1
commit f10f9873e9
3 changed files with 6 additions and 5 deletions
+3 -2
View File
@@ -264,7 +264,7 @@ const _cleanNativeNotes = (notes) => {
dup.duration_beats = Math.max(dup.duration_beats, n.duration_beats || 1);
return;
}
out.push({ pitch: pitch, start_beat: start, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 });
out.push({ pitch: pitch, start_beat: start, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8, ...(Array.isArray(n.expression_curve) && n.expression_curve.length ? { expression_curve: n.expression_curve } : {}) });
});
out.forEach(function (n) {
let nextStart = Infinity;
@@ -310,6 +310,7 @@ const _buildNativeTrackNotes = (track, bpm, baseOffsetSec, notesOverride) => {
start_beat: itemStartAbs / secPerBeat + (n.start_beat || 0),
duration_beats: n.duration_beats || 1,
velocity: n.velocity != null ? n.velocity : 0.8,
...(Array.isArray(n.expression_curve) && n.expression_curve.length ? { expression_curve: n.expression_curve } : {}),
});
});
});
@@ -322,7 +323,7 @@ const _nativeTrackKey = (track, bpm, baseOffsetSec, notesOverride) => {
const sfId = (eng && eng.soundfont_id) || '';
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
const nk = data.absNotes.map(function (n) {
return n.pitch + ':' + n.start_beat + ':' + n.duration_beats + ':' + n.velocity;
return n.pitch + ':' + n.start_beat + ':' + n.duration_beats + ':' + n.velocity + (Array.isArray(n.expression_curve) && n.expression_curve.length ? ':' + n.expression_curve.map(function (p) { return p.position_ratio + '=' + p.value; }).join('~') : '');
}).join(',');
return [track && track.id ? track.id : '', instrumentId, sfId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', parseFloat(bpm) || 120, baseOffsetSec || 0, nk].join('|');
} catch (e) { return ''; }
+2 -2
View File
@@ -61,10 +61,10 @@ let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFa
// (tránh synth instantiate nhiều voice cùng pitch — spec §II.5).
// Legato cleanup: note trước cùng pitch kéo dài qua start_beat note sau → cắt
// 0.01 beats trước Note-On mới (tránh duplicate voice allocation).
const _cleanNativeNotes=notes=>{const out=[];notes.forEach(function(n){const pitch=n.pitch||60;const start=n.start_beat||0;const dup=out.find(function(o){return o.pitch===pitch&&o.start_beat===start;});if(dup){dup.velocity=Math.max(dup.velocity,n.velocity!=null?n.velocity:0.8);dup.duration_beats=Math.max(dup.duration_beats,n.duration_beats||1);return;}out.push({pitch:pitch,start_beat:start,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8});});out.forEach(function(n){let nextStart=Infinity;out.forEach(function(m){if(m===n||m.pitch!==n.pitch||m.start_beat<=n.start_beat)return;if(m.start_beat<nextStart)nextStart=m.start_beat;});if(nextStart<n.start_beat+n.duration_beats){n.duration_beats=Math.max(0.01,nextStart-n.start_beat-0.01);}});return out;};// Polyphony gain compensation (spec §II.1): scale velocity theo số voice đồng
const _cleanNativeNotes=notes=>{const out=[];notes.forEach(function(n){const pitch=n.pitch||60;const start=n.start_beat||0;const dup=out.find(function(o){return o.pitch===pitch&&o.start_beat===start;});if(dup){dup.velocity=Math.max(dup.velocity,n.velocity!=null?n.velocity:0.8);dup.duration_beats=Math.max(dup.duration_beats,n.duration_beats||1);return;}out.push({pitch:pitch,start_beat:start,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8,...(Array.isArray(n.expression_curve)&&n.expression_curve.length?{expression_curve:n.expression_curve}:{})});});out.forEach(function(n){let nextStart=Infinity;out.forEach(function(m){if(m===n||m.pitch!==n.pitch||m.start_beat<=n.start_beat)return;if(m.start_beat<nextStart)nextStart=m.start_beat;});if(nextStart<n.start_beat+n.duration_beats){n.duration_beats=Math.max(0.01,nextStart-n.start_beat-0.01);}});return out;};// Polyphony gain compensation (spec §II.1): scale velocity theo số voice đồng
// thời tại thời điểm note bắt đầu — Equal Power G = 1/sqrt(N). Solo N=1 → giữ
// nguyên; chord 8 note → -9dB mỗi voice, tổng peak không vượt trần.
const _scaleNativePolyphony=notes=>{notes.forEach(function(n){const nStart=n.start_beat||0;let active=0;notes.forEach(function(m){const mEnd=(m.start_beat||0)+(m.duration_beats||1);if((m.start_beat||0)<=nStart&&nStart<mEnd)active++;});if(active>1){const g=1/Math.sqrt(active);n.velocity=(n.velocity!=null?n.velocity:0.8)*g;}});return notes;};const _buildNativeTrackNotes=(track,bpm,baseOffsetSec,notesOverride)=>{const secPerBeat=60.0/(parseInt(bpm)||120);const items=notesOverride?[{startTime:0,notes:notesOverride}]:track&&track.midiItems||[];let trackStartAbs=Infinity;const absNotes=[];items.forEach(function(item){const itemStartAbs=(baseOffsetSec||0)+(item.startTime||0);if(itemStartAbs<trackStartAbs)trackStartAbs=itemStartAbs;(item.notes||[]).forEach(function(n){absNotes.push({pitch:n.pitch||60,start_beat:itemStartAbs/secPerBeat+(n.start_beat||0),duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8});});});return{absNotes:_scaleNativePolyphony(_cleanNativeNotes(absNotes)),trackStartAbs:trackStartAbs};};const _nativeTrackKey=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId||'';const sfId=eng&&eng.soundfont_id||'';const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);const nk=data.absNotes.map(function(n){return n.pitch+':'+n.start_beat+':'+n.duration_beats+':'+n.velocity;}).join(',');return[track&&track.id?track.id:'',instrumentId,sfId,eng&&eng.preset_id||'',eng&&eng.preset_path||'',parseFloat(bpm)||120,baseOffsetSec||0,nk].join('|');}catch(e){return'';}};const _renderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);if(!data.absNotes.length)return Promise.resolve(null);if(isSfTrackEngine(eng)){const sfId=eng&&eng.soundfont_id;if(!sfId)return Promise.resolve(null);return window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:eng&&eng.soundfont_bank||0,program:eng&&eng.soundfont_program||0,bpm:parseFloat(bpm)||120,notes:data.absNotes}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return Promise.resolve(null);return window.SonicAPI.midiRender({instrument_id:instrumentId,notes:data.absNotes,bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}catch(e){return Promise.resolve(null);}};// Dedupe in-flight render + cache WAV: play lặp/re-schedule trong lúc render
const _scaleNativePolyphony=notes=>{notes.forEach(function(n){const nStart=n.start_beat||0;let active=0;notes.forEach(function(m){const mEnd=(m.start_beat||0)+(m.duration_beats||1);if((m.start_beat||0)<=nStart&&nStart<mEnd)active++;});if(active>1){const g=1/Math.sqrt(active);n.velocity=(n.velocity!=null?n.velocity:0.8)*g;}});return notes;};const _buildNativeTrackNotes=(track,bpm,baseOffsetSec,notesOverride)=>{const secPerBeat=60.0/(parseInt(bpm)||120);const items=notesOverride?[{startTime:0,notes:notesOverride}]:track&&track.midiItems||[];let trackStartAbs=Infinity;const absNotes=[];items.forEach(function(item){const itemStartAbs=(baseOffsetSec||0)+(item.startTime||0);if(itemStartAbs<trackStartAbs)trackStartAbs=itemStartAbs;(item.notes||[]).forEach(function(n){absNotes.push({pitch:n.pitch||60,start_beat:itemStartAbs/secPerBeat+(n.start_beat||0),duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8,...(Array.isArray(n.expression_curve)&&n.expression_curve.length?{expression_curve:n.expression_curve}:{})});});});return{absNotes:_scaleNativePolyphony(_cleanNativeNotes(absNotes)),trackStartAbs:trackStartAbs};};const _nativeTrackKey=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId||'';const sfId=eng&&eng.soundfont_id||'';const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);const nk=data.absNotes.map(function(n){return n.pitch+':'+n.start_beat+':'+n.duration_beats+':'+n.velocity+(Array.isArray(n.expression_curve)&&n.expression_curve.length?':'+n.expression_curve.map(function(p){return p.position_ratio+'='+p.value;}).join('~'):'');}).join(',');return[track&&track.id?track.id:'',instrumentId,sfId,eng&&eng.preset_id||'',eng&&eng.preset_path||'',parseFloat(bpm)||120,baseOffsetSec||0,nk].join('|');}catch(e){return'';}};const _renderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);if(!data.absNotes.length)return Promise.resolve(null);if(isSfTrackEngine(eng)){const sfId=eng&&eng.soundfont_id;if(!sfId)return Promise.resolve(null);return window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:eng&&eng.soundfont_bank||0,program:eng&&eng.soundfont_program||0,bpm:parseFloat(bpm)||120,notes:data.absNotes}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return Promise.resolve(null);return window.SonicAPI.midiRender({instrument_id:instrumentId,notes:data.absNotes,bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}catch(e){return Promise.resolve(null);}};// Dedupe in-flight render + cache WAV: play lặp/re-schedule trong lúc render
// đang chạy → chờ cùng promise, không bắn duplicate request (tránh queue sau
// RENDER_LOCK + render 2 lần cùng nội dung).
const ensureRenderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{const key=_nativeTrackKey(track,bpm,baseOffsetSec,notesOverride);if(!key)return _renderNativeTrack(track,bpm,baseOffsetSec,notesOverride);if(_nativeTrackUrls[key])return Promise.resolve(_nativeTrackUrls[key]);if(_nativeTrackPending[key])return _nativeTrackPending[key];const p=_renderNativeTrack(track,bpm,baseOffsetSec,notesOverride).then(function(url){if(url)_nativeTrackUrls[key]=url;return url;}).then(function(url){delete _nativeTrackPending[key];return url;});_nativeTrackPending[key]=p;return p;};// Giới hạn decodeAudioData song song (max 3) — nhiều WAV fetch về cùng lúc →
+1 -1
View File
@@ -32,7 +32,7 @@
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
<script src="/static/vendor/react.production.min.js"></script>
<script src="/static/vendor/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=20260826161545"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=2026082616201545"></script>
<script src="/static/js/services/runtime.js?v=202608101800"></script>
<script src="/static/js/services/api.js?v=202608212400"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>