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()
File diff suppressed because one or more lines are too long
+111 -9
View File
@@ -30,21 +30,123 @@
return buffer;
},
// Play a MIDI note using Web Audio fallback
playNote: function (note, velocity, durationMs) {
playNote: function (note, velocity, durationMs, startTime, program, destinationNode) {
const ctx = getCtx();
const freq = 440 * Math.pow(2, (note - 69) / 12);
if (freq <= 0 || isNaN(freq)) return null;
const osc = ctx.createOscillator();
const noteGain = ctx.createGain();
osc.type = 'triangle';
// Default settings
let oscType = 'triangle';
let attackTime = 0.01;
let decayTime = 0.1;
let sustainLevel = 0.5;
let releaseTime = 0.2;
let volFactor = 0.3;
const prog = program !== undefined ? parseInt(program) : 0;
if (prog >= 0 && prog <= 7) { // Pianos
oscType = 'sine';
decayTime = 0.3;
sustainLevel = 0.1;
releaseTime = 0.2;
} else if (prog >= 8 && prog <= 15) { // Chromatic Perc
oscType = 'sine';
decayTime = 0.1;
sustainLevel = 0.0;
releaseTime = 0.1;
} else if (prog >= 16 && prog <= 23) { // Organs
oscType = 'sine';
attackTime = 0.05;
sustainLevel = 0.8;
releaseTime = 0.1;
} else if (prog >= 24 && prog <= 31) { // Guitars
oscType = 'triangle';
decayTime = 0.4;
sustainLevel = 0.2;
releaseTime = 0.3;
} else if (prog >= 32 && prog <= 39) { // Basses
oscType = 'triangle';
attackTime = 0.02;
decayTime = 0.2;
sustainLevel = 0.6;
releaseTime = 0.2;
} else if (prog >= 40 && prog <= 47) { // Strings
oscType = 'sawtooth';
attackTime = 0.15;
sustainLevel = 0.8;
releaseTime = 0.5;
volFactor = 0.15;
} else if (prog >= 48 && prog <= 55) { // Ensemble / Choir
oscType = 'sawtooth';
attackTime = 0.2;
sustainLevel = 0.8;
releaseTime = 0.6;
volFactor = 0.12;
} else if (prog >= 56 && prog <= 63) { // Brass
oscType = 'sawtooth';
attackTime = 0.08;
sustainLevel = 0.7;
releaseTime = 0.3;
volFactor = 0.15;
} else if (prog >= 64 && prog <= 71) { // Reed
oscType = 'square';
attackTime = 0.05;
sustainLevel = 0.6;
releaseTime = 0.2;
volFactor = 0.15;
} else if (prog >= 72 && prog <= 79) { // Pipe
oscType = 'sine';
attackTime = 0.1;
sustainLevel = 0.7;
releaseTime = 0.3;
volFactor = 0.2;
} else if (prog >= 80 && prog <= 119) { // Synth Lead/Pad/FX
oscType = 'sawtooth';
attackTime = 0.05;
sustainLevel = 0.6;
releaseTime = 0.4;
volFactor = 0.15;
}
osc.type = oscType;
osc.frequency.value = freq;
noteGain.gain.setValueAtTime(velocity / 127 * 0.3, ctx.currentTime);
noteGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
const startAt = startTime !== undefined ? startTime : ctx.currentTime;
const durSec = durationMs / 1000;
const vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
const targetGain = vel * volFactor;
// ADSR Envelope
noteGain.gain.setValueAtTime(0, startAt);
noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
const releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
noteGain.gain.setValueAtTime(targetGain * sustainLevel, releaseStart);
noteGain.gain.exponentialRampToValueAtTime(0.001, releaseStart + releaseTime);
osc.connect(noteGain);
noteGain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + durationMs / 1000 + 0.05);
activeOscillators[note] = osc;
const dest = destinationNode || gainNode || ctx.destination;
noteGain.connect(dest);
osc.start(startAt);
const stopAt = releaseStart + releaseTime + 0.05;
osc.stop(stopAt);
const oscId = `${note}_${Date.now()}_${Math.random()}`;
activeOscillators[oscId] = osc;
// Clean up active oscillator reference after it stops
setTimeout(() => {
delete activeOscillators[oscId];
}, (stopAt - ctx.currentTime) * 1000 + 100);
return osc;
},