fix: synth để gắn soundfont cho midi
This commit is contained in:
@@ -15,7 +15,7 @@ os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
|
||||
|
||||
@router.get("/available")
|
||||
async def list_plugins(current_user: dict = Depends(get_current_user)):
|
||||
pm = PluginManager()
|
||||
pm = PluginManager(upload_sf_dir=UPLOAD_SF_DIR)
|
||||
return pm.list_available()
|
||||
|
||||
|
||||
|
||||
+11
-6
@@ -86,9 +86,10 @@ if HAS_PYFLUIDSYNTH:
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts"):
|
||||
def __init__(self, vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts", upload_sf_dir=None):
|
||||
self.vst_dir = vst_dir
|
||||
self.sf_dir = sf_dir
|
||||
self.upload_sf_dir = upload_sf_dir
|
||||
|
||||
def _scan_plugins(self) -> dict:
|
||||
plugins = {}
|
||||
@@ -104,11 +105,15 @@ class PluginManager:
|
||||
|
||||
def _scan_soundfonts(self) -> list:
|
||||
sfonts = []
|
||||
if not os.path.isdir(self.sf_dir):
|
||||
return sfonts
|
||||
for f in os.listdir(self.sf_dir):
|
||||
if f.endswith(".sf2") or f.endswith(".sf3"):
|
||||
sfonts.append({"id": os.path.splitext(f)[0], "name": f, "file": f})
|
||||
dirs = [self.sf_dir]
|
||||
if self.upload_sf_dir and self.upload_sf_dir != self.sf_dir:
|
||||
dirs.append(self.upload_sf_dir)
|
||||
for d in dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for f in os.listdir(d):
|
||||
if f.endswith(".sf2") or f.endswith(".sf3"):
|
||||
sfonts.append({"id": os.path.splitext(f)[0], "name": f, "file": f})
|
||||
return sfonts
|
||||
|
||||
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
||||
|
||||
+208
-4
@@ -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: tạo file MIDI từ tất 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
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
Binary file not shown.
@@ -10,13 +10,13 @@
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/api.js?v=202607231808"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607231808"></script>
|
||||
<script src="/static/js/services/storage.js?v=202607231808"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202607231808"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202607231808"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607231808"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607231808" defer></script>
|
||||
<script src="/static/js/services/api.js?v=202607231850"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607231850"></script>
|
||||
<script src="/static/js/services/storage.js?v=202607231850"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202607231850"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202607231850"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607231850"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607231850" defer></script>
|
||||
<style>
|
||||
:root {
|
||||
--right-sidebar-width: 320px;
|
||||
|
||||
Reference in New Issue
Block a user