fix: synth để gắn soundfont cho midi

This commit is contained in:
2026-07-23 19:26:26 +07:00
parent 055f351a17
commit a21266736c
7 changed files with 366 additions and 35 deletions
+208 -4
View File
@@ -8068,6 +8068,49 @@ const App = () => {
activeSourcesRef.current.push(source);
}
});
// MIDI items playback
const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
midiItems.forEach(item => {
const notes = item.notes || [];
notes.forEach(note => {
// note start/duration is in beats (for MIDI items)
const noteStartSec = item.startTime + (note.start_beat || 0) * secondsPerBeat;
const noteDurSec = (note.duration_beats || 1) * secondsPerBeat;
const noteEndSec = noteStartSec + noteDurSec;
if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000;
const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
window.SonicSF.playNote(
note.pitch || 60,
note.velocity || 0.8,
durationMs,
startTime,
program,
gainNode
);
} else {
const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
window.SonicSF.playNote(
note.pitch || 60,
note.velocity || 0.8,
remainingDurMs,
context.currentTime,
program,
gainNode
);
}
}
});
});
}
});
};
@@ -8116,6 +8159,47 @@ const App = () => {
activeSourcesRef.current.push(source);
}
});
// MIDI items playback
const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
midiItems.forEach(item => {
const notes = item.notes || [];
notes.forEach(note => {
const noteStartSec = item.startTime + (note.start_beat || 0) * secondsPerBeat;
const noteDurSec = (note.duration_beats || 1) * secondsPerBeat;
const noteEndSec = noteStartSec + noteDurSec;
if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000;
const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
window.SonicSF.playNote(
note.pitch || 60,
note.velocity || 0.8,
durationMs,
startTime,
program,
gainNode
);
} else {
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
window.SonicSF.playNote(
note.pitch || 60,
note.velocity || 0.8,
remainingDurMs,
context.currentTime,
program,
gainNode
);
}
}
});
});
}
};
const handlePlayPause = () => {
if (activeTab !== 'main') {
@@ -8162,6 +8246,9 @@ const App = () => {
});
activeSourcesRef.current = [];
activeTrackNodesRef.current = {};
if (window.SonicSF) {
window.SonicSF.stopAll();
}
setIsPlaying(false);
setSubTabs(prev => prev.map(s => ({
...s,
@@ -9402,7 +9489,7 @@ const App = () => {
channelData[i] = Math.sin(2 * Math.PI * freq * t) * 0.25 * (1.0 - t % 0.5 / 0.5);
}
}
setTracks(prev => prev.map(t => t.id === trackId ? {
updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
...t,
name: `Demo_${type.toUpperCase()}.wav`,
buffer: newBuffer
@@ -9475,6 +9562,119 @@ const App = () => {
return { ...t, inputSource: { deviceType: type, deviceId } };
}));
};
// MIDI Export: to file MIDI t tt c tracks
const triggerMidiExport = () => {
const bpmNum = parseFloat(bpm) || 120;
const ppq = 480; // Pulses Per Quarter Note
const ticksPerBeat = ppq;
const beatDuration = 60 / bpmNum;
// Build MIDI tracks
let midiTracks = [];
let currentTrackNum = 0;
tracks.forEach(track => {
currentTrackNum++;
const events = [];
let hasNotes = false;
// Collect MIDI events from midiItems
const midiItems = track.midiItems || [];
midiItems.forEach(item => {
const notes = item.notes || [];
notes.forEach(note => {
hasNotes = true;
const startTick = Math.round(note.startBeat * ticksPerBeat);
const durTick = Math.round(note.durationBeats * ticksPerBeat);
const velocity = Math.round((note.velocity || 0.8) * 100);
const pitch = note.pitch || 60;
events.push({ tick: startTick, type: 'note_on', pitch, velocity });
events.push({ tick: startTick + durTick, type: 'note_off', pitch, velocity: 0 });
});
});
// If track has audio buffer but no MIDI, create a "rest" track with one silent note
if (!hasNotes && track.buffer) {
const durSec = track.buffer.duration;
const durBeats = durSec / beatDuration;
const durTicks = Math.round(durBeats * ticksPerBeat);
// Use a C-2 (pitch 0 = rest note) indicator
events.push({ tick: 0, type: 'note_on', pitch: 0, velocity: 1 });
events.push({ tick: durTicks, type: 'note_off', pitch: 0, velocity: 0 });
}
if (events.length === 0 && !track.buffer) return; // Skip empty tracks
// Sort events by tick
events.sort((a, b) => a.tick - b.tick);
// MIDI track header bytes
const trackBytes = [];
// Track name
const nameStr = (track.name || ('Track ' + currentTrackNum)).slice(0, 255);
trackBytes.push(0xFF, 0x03, nameStr.length);
for (let i = 0; i < nameStr.length; i++) trackBytes.push(nameStr.charCodeAt(i));
// End of track marker will be calculated later
let lastTick = 0;
events.forEach(ev => {
const delta = ev.tick - lastTick;
lastTick = ev.tick;
// Delta time as variable-length quantity
writeVLQ(trackBytes, delta);
if (ev.type === 'note_on') {
trackBytes.push(0x90, ev.pitch, ev.velocity);
} else {
trackBytes.push(0x80, ev.pitch, 0);
}
});
// End of track
writeVLQ(trackBytes, 0);
trackBytes.push(0xFF, 0x2F, 0x00);
// Track chunk: "MTrk" + length + data
const trackData = [0x4D, 0x54, 0x72, 0x6B]; // "MTrk"
const len = trackBytes.length;
trackData.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF);
trackData.push(...trackBytes);
midiTracks.push(trackData);
});
if (midiTracks.length === 0) {
showToast("Không có dữ liệu MIDI nào để xuất.", "warning");
return;
}
// Header: "MThd" + length(6) + format(1) + tracks + division
const header = [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, (midiTracks.length >> 8) & 0xFF, midiTracks.length & 0xFF, (ppq >> 8) & 0xFF, ppq & 0xFF];
const allBytes = header.concat(...midiTracks.flat());
const uint8 = new Uint8Array(allBytes);
const blob = new Blob([uint8], { type: 'audio/midi' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (projectName || 'Project') + '.mid';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`Đã xuất file MIDI với ${midiTracks.length} tracks!`, "success");
};
function writeVLQ(bytes, value) {
if (value < 0) value = 0;
const buf = [];
buf.push(value & 0x7F);
while (value > 0x7F) {
value >>= 7;
buf.push(0x80 | (value & 0x7F));
}
buf.reverse();
buf.forEach(b => bytes.push(b));
}
// Insert Track Below Selected
const insertTrackBelow = () => {
@@ -11642,6 +11842,10 @@ const App = () => {
label: 'Export Mix...',
icon: 'file-output',
action: () => triggerWavExport()
}, {
label: 'Export MIDI...',
icon: 'music',
action: () => triggerMidiExport()
}, {
sep: true
}, ...(currentUser ? [{
@@ -13237,8 +13441,8 @@ const App = () => {
className: "w-3 h-3"
})), " FX: ", /*#__PURE__*/React.createElement("span", {
className: "text-zinc-500 font-normal"
}, "None")), /*#__PURE__*/React.createElement("button", {
onClick: () => generateSynthToTrack(track.id, 'synth'),
}, " None")), /*#__PURE__*/React.createElement("button", {
onClick: (e) => { e.stopPropagation(); openInstrumentSelector(track.id); },
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-xs font-bold flex items-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
@@ -13247,7 +13451,7 @@ const App = () => {
className: "w-3 h-3"
})), " Synth: ", /*#__PURE__*/React.createElement("span", {
className: "text-zinc-500 font-normal"
}, "None"))), /*#__PURE__*/React.createElement("div", {
}, track.instrumentId ? (track.instrumentProgram !== undefined ? `${track.instrumentId} [${track.instrumentProgram}]` : track.instrumentId) : "None"))), /*#__PURE__*/React.createElement("div", {
onMouseDown: e => handleTrackResizeMouseDown(e, track.id),
className: "absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",
onClick: e => e.stopPropagation()