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")
|
@router.get("/available")
|
||||||
async def list_plugins(current_user: dict = Depends(get_current_user)):
|
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()
|
return pm.list_available()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+11
-6
@@ -86,9 +86,10 @@ if HAS_PYFLUIDSYNTH:
|
|||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
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.vst_dir = vst_dir
|
||||||
self.sf_dir = sf_dir
|
self.sf_dir = sf_dir
|
||||||
|
self.upload_sf_dir = upload_sf_dir
|
||||||
|
|
||||||
def _scan_plugins(self) -> dict:
|
def _scan_plugins(self) -> dict:
|
||||||
plugins = {}
|
plugins = {}
|
||||||
@@ -104,11 +105,15 @@ class PluginManager:
|
|||||||
|
|
||||||
def _scan_soundfonts(self) -> list:
|
def _scan_soundfonts(self) -> list:
|
||||||
sfonts = []
|
sfonts = []
|
||||||
if not os.path.isdir(self.sf_dir):
|
dirs = [self.sf_dir]
|
||||||
return sfonts
|
if self.upload_sf_dir and self.upload_sf_dir != self.sf_dir:
|
||||||
for f in os.listdir(self.sf_dir):
|
dirs.append(self.upload_sf_dir)
|
||||||
if f.endswith(".sf2") or f.endswith(".sf3"):
|
for d in dirs:
|
||||||
sfonts.append({"id": os.path.splitext(f)[0], "name": f, "file": f})
|
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
|
return sfonts
|
||||||
|
|
||||||
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
||||||
|
|||||||
+208
-4
@@ -8068,6 +8068,49 @@ const App = () => {
|
|||||||
activeSourcesRef.current.push(source);
|
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);
|
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 = () => {
|
const handlePlayPause = () => {
|
||||||
if (activeTab !== 'main') {
|
if (activeTab !== 'main') {
|
||||||
@@ -8162,6 +8246,9 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
activeSourcesRef.current = [];
|
activeSourcesRef.current = [];
|
||||||
activeTrackNodesRef.current = {};
|
activeTrackNodesRef.current = {};
|
||||||
|
if (window.SonicSF) {
|
||||||
|
window.SonicSF.stopAll();
|
||||||
|
}
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setSubTabs(prev => prev.map(s => ({
|
setSubTabs(prev => prev.map(s => ({
|
||||||
...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);
|
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,
|
...t,
|
||||||
name: `Demo_${type.toUpperCase()}.wav`,
|
name: `Demo_${type.toUpperCase()}.wav`,
|
||||||
buffer: newBuffer
|
buffer: newBuffer
|
||||||
@@ -9476,6 +9563,119 @@ const App = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── 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 ──
|
// ── Insert Track Below Selected ──
|
||||||
const insertTrackBelow = () => {
|
const insertTrackBelow = () => {
|
||||||
const curTracks = activeTracks;
|
const curTracks = activeTracks;
|
||||||
@@ -11642,6 +11842,10 @@ const App = () => {
|
|||||||
label: 'Export Mix...',
|
label: 'Export Mix...',
|
||||||
icon: 'file-output',
|
icon: 'file-output',
|
||||||
action: () => triggerWavExport()
|
action: () => triggerWavExport()
|
||||||
|
}, {
|
||||||
|
label: 'Export MIDI...',
|
||||||
|
icon: 'music',
|
||||||
|
action: () => triggerMidiExport()
|
||||||
}, {
|
}, {
|
||||||
sep: true
|
sep: true
|
||||||
}, ...(currentUser ? [{
|
}, ...(currentUser ? [{
|
||||||
@@ -13237,8 +13441,8 @@ const App = () => {
|
|||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
})), " FX: ", /*#__PURE__*/React.createElement("span", {
|
})), " FX: ", /*#__PURE__*/React.createElement("span", {
|
||||||
className: "text-zinc-500 font-normal"
|
className: "text-zinc-500 font-normal"
|
||||||
}, "None")), /*#__PURE__*/React.createElement("button", {
|
}, " None")), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => generateSynthToTrack(track.id, 'synth'),
|
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"
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
@@ -13247,7 +13451,7 @@ const App = () => {
|
|||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
})), " Synth: ", /*#__PURE__*/React.createElement("span", {
|
})), " Synth: ", /*#__PURE__*/React.createElement("span", {
|
||||||
className: "text-zinc-500 font-normal"
|
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),
|
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",
|
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()
|
onClick: e => e.stopPropagation()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,21 +30,123 @@
|
|||||||
return buffer;
|
return buffer;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Play a MIDI note using Web Audio fallback
|
playNote: function (note, velocity, durationMs, startTime, program, destinationNode) {
|
||||||
playNote: function (note, velocity, durationMs) {
|
|
||||||
const ctx = getCtx();
|
const ctx = getCtx();
|
||||||
const freq = 440 * Math.pow(2, (note - 69) / 12);
|
const freq = 440 * Math.pow(2, (note - 69) / 12);
|
||||||
|
if (freq <= 0 || isNaN(freq)) return null;
|
||||||
|
|
||||||
const osc = ctx.createOscillator();
|
const osc = ctx.createOscillator();
|
||||||
const noteGain = ctx.createGain();
|
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;
|
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);
|
osc.connect(noteGain);
|
||||||
noteGain.connect(ctx.destination);
|
|
||||||
osc.start(ctx.currentTime);
|
const dest = destinationNode || gainNode || ctx.destination;
|
||||||
osc.stop(ctx.currentTime + durationMs / 1000 + 0.05);
|
noteGain.connect(dest);
|
||||||
activeOscillators[note] = osc;
|
|
||||||
|
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;
|
return osc;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -10,13 +10,13 @@
|
|||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<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@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="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/api.js?v=202607231850"></script>
|
||||||
<script src="/static/js/services/audioEngine.js?v=202607231808"></script>
|
<script src="/static/js/services/audioEngine.js?v=202607231850"></script>
|
||||||
<script src="/static/js/services/storage.js?v=202607231808"></script>
|
<script src="/static/js/services/storage.js?v=202607231850"></script>
|
||||||
<script src="/static/js/services/soundfontPlayer.js?v=202607231808"></script>
|
<script src="/static/js/services/soundfontPlayer.js?v=202607231850"></script>
|
||||||
<script src="/static/js/services/aiGateway.js?v=202607231808"></script>
|
<script src="/static/js/services/aiGateway.js?v=202607231850"></script>
|
||||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607231808"></script>
|
<script src="/static/js/services/dawCommandDispatcher.js?v=202607231850"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202607231808" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202607231850" defer></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--right-sidebar-width: 320px;
|
--right-sidebar-width: 320px;
|
||||||
|
|||||||
Reference in New Issue
Block a user