From bd55d035deb0b569d76d39d5ac75846c9abbbddf Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 26 Jul 2026 07:16:17 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20=C4=91=C3=A3=20s=E1=BB=ADa=20l=E1=BB=97i?= =?UTF-8?q?=20MIDI=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.precompiled.js | 2 +- app/storage/sonicforge.db | Bin 118784 -> 118784 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index d1b4c11..8dfd86e 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -266,7 +266,7 @@ while(true){const beatNum=nextMetronomeBeatRef.current;const elapsedBeats=beatNu const now=Date.now();if(now-lastTempCompileTimeRef.current>200){lastTempCompileTimeRef.current=now;// Audio preview for(let trackId in recordingPCMDataRef.current){const data=recordingPCMDataRef.current[trackId];if(data&&data.length>0){const tempBuf=audioCtx.createBuffer(1,data.length,audioCtx.sampleRate);tempBuf.getChannelData(0).set(data);setRecTempAudioBuffer(tempBuf);}}// MIDI preview let combinedNotes=[];const armedTracks=activeTracksRef.current.filter(t=>t.isArmed);for(let track of armedTracks){const midiRec=activeMIDIRecordersRef.current[track.id];if(midiRec){const currentBeat=(audioCtx.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec)/secondsPerBeat;const notes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];combinedNotes=combinedNotes.concat(notes);}}if(combinedNotes.length>0||armedTracks.some(t=>activeMIDIRecordersRef.current[t.id])){setRecTempMidiNotes(combinedNotes);setCanvasRedrawCount(n=>n+1);}}}const isSubTab=subTabsRef.current.some(sub=>sub.id===activeTabRef.current);if(isSubTab){const st=subTabsRef.current.find(s=>s.id===activeTabRef.current);if(!st||!st.isPlaying||!st.buffer)return;const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;const wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;// Loop sub-tab selection (bufferPos is buffer-time) -if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&(st.isLooping||isLoopingSelection)){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:start,isPlaying:true}:s));startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording +if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&(st.isLooping||isLoopingSelection)){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){stopAllPlayback();if(st.type==='PIANO_ROLL'&&window.SonicSF){const ctx2=getAudioContext();const notes2=st.notes||[];const bpmVal2=parseInt(bpmRef?.current||bpm)||120;const spb2=60.0/bpmVal2;const now2=ctx2.currentTime;const track2=activeTracks.find(t=>t.id===st.trackId);const dest2=getOrCreateTrackNode(track2,ctx2);const ip2=track2?track2.instrumentProgram:undefined;notes2.forEach(n=>{const s=n.start_beat||0;const d=n.duration_beats||1;if(window.SonicSF)window.SonicSF.playNote(n.pitch||60,n.velocity||0.8,(d*spb2*1000),now2+s*spb2,ip2,dest2);});}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:start,isPlaying:true}:s));startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording let maxEnd=0;notes.forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});return Math.max(maxEnd*beatSec+1.0, 16*beatSec);})():st.buffer.duration;if(bufferPos>=effectiveDuration){stopAllPlayback();if(isLoopingSelection){if(st.type==='PIANO_ROLL'&&window.SonicSF){const ctx2=getAudioContext();const notes2=st.notes||[];const bpmVal2=parseInt(bpmRef?.current||bpm)||120;const spb2=60.0/bpmVal2;const now2=ctx2.currentTime;const track2=activeTracks.find(t=>t.id===st.trackId);const dest2=getOrCreateTrackNode(track2,ctx2);const ip2=track2?track2.instrumentProgram:undefined;notes2.forEach(n=>{const s=n.start_beat||0;const d=n.duration_beats||1;if(window.SonicSF)window.SonicSF.playNote(n.pitch||60,n.velocity||0.8,(d*spb2*1000),now2+s*spb2,ip2,dest2);});}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:0,isPlaying:true}:s));startSubTabPlayback(st,0);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:0,isPlaying:false}:s));}return;}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:wallTime}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(!isPlaying)return;const context=getAudioContext();const elapsed=context.currentTime-startAudioTimeRef.current;const updatedTime=startOffsetTimeRef.current+elapsed;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2 // If selection cleared by user, play linearly (don't loop) if(!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null){if(selRight>selLeft&&updatedTime>=selRight){if(soloedTrackId!==null||selectionMode==='local'){stopAllPlayback();startOffsetTimeRef.current=selLeft;startAudioTimeRef.current=context.currentTime;const soloTid=soloedTrackId!==null?soloedTrackId:localSelectionTrackId;startLocalTrackPlayback(soloTid,selLeft);setCurrentTime(selLeft);setIsPlaying(true);}else{stopAllPlayback();startOffsetTimeRef.current=selLeft;startAudioTimeRef.current=context.currentTime;startTrackPlayback(selLeft);setCurrentTime(selLeft);setIsPlaying(true);}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}if(updatedTime>=maxDurationRef.current){if(recordingStateRef.current==='RECORDING'){setCurrentTime(updatedTime);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(isLoopingSelection){stopAllPlayback();startOffsetTimeRef.current=0;startAudioTimeRef.current=context.currentTime;startTrackPlayback(0);setCurrentTime(0);setIsPlaying(true);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}stopAllPlayback();setCurrentTime(0);return;}setCurrentTime(updatedTime);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);};useEffect(()=>{if(isPlaying||subTabs.some(s=>s.isPlaying)){animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{cancelAnimationFrame(animationFrameIdRef.current);}return()=>cancelAnimationFrame(animationFrameIdRef.current);},[isPlaying,subTabs,isLoopingSelection,selLeft,selRight,selectionMode,localSelectionTrackId,selectionCleared,activeTab]);// ── FX Nodes ── diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 1adf4dd2f9e7e2440f6dab8dc3570993a3d147d0..8a2f6598f5c0feb03d2b54da7574b3be3b24dfa5 100644 GIT binary patch literal 118784 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 GCh-5FOXTMO delta 1266 zcma)6&rj1(9N%LhW58rT1KEPeq7f5P`uh6%S`r14{Q-I~9y~nDein3eTla${B=8?- zYVtJEgo{@Q?#e;qg#l;ws(EoyS|r+K+5Cs zOj{G|%&R#9QU}jI><@cSJ>JuUonvq4%^dL4$4h(b{vn@{eW6pceR#XoKAapH6%|Fq zG8P5YdjCBI%v(n{N4l>hD6x}C@O0n{hNr+nDed-*(j;HR;0-MebfO_gr1JB zCX33|AgEbi+o;qq71>M_#6yS^$$%k(%+$n{dgQvEm-1SbVx}j-bO70oUJoFTav1{_ zbt6W?v*i&G4@#WuNS>)~ z6XlO5rZqYR3BqwK#1R;qQioNG`h*t!Y4h^$<*B)0BKx?o_w zS)>rL&pu1H%!!VcxSBU*{`neUdkY{Eh8z)*V4V!TiyDC6RqIMubImh6#NT##!^E7