Compare commits
38 Commits
fe5e8cb58e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e3c652551 | |||
| 393df8fd7b | |||
| 688a7a57e2 | |||
| 652e745d0a | |||
| 45ab31d42e | |||
| c3182f6baa | |||
| 1d9b426d15 | |||
| 4f34491f3e | |||
| 9e269d3b79 | |||
| 58c6ec794d | |||
| 173a1f227b | |||
| c1aa1eb4d4 | |||
| 6c16bf8410 | |||
| 4981785913 | |||
| bca661bffd | |||
| 4c820bfa77 | |||
| 8cdf2942b9 | |||
| 23675dab83 | |||
| 71b9231b57 | |||
| 93dc13b17a | |||
| f1e914fa62 | |||
| 4dfab954d8 | |||
| e99e54773f | |||
| 0ba31c57bf | |||
| 454dd91f96 | |||
| e9f29e09ca | |||
| 289746f187 | |||
| b3ced7a7b3 | |||
| 4233c1eeda | |||
| 35f7c3822f | |||
| d37f4e7557 | |||
| 8a9d6b3c27 | |||
| 55d3464b1e | |||
| cfc114b9d7 | |||
| 856a8183b6 | |||
| 0272912cff | |||
| c047934fc4 | |||
| fc663921ff |
@@ -259,3 +259,32 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
|
||||
"max_tracks": row["max_tracks"] or 16
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/first-time")
|
||||
async def auth_first_time():
|
||||
# "Lần đăng nhập đầu" = tài khoản admin vẫn dùng mật khẩu MẶC ĐỊNH
|
||||
# (chưa từng đổi). Sau khi đổi lần đầu → first_time = false → UI xóa
|
||||
# gợi ý username/mật khẩu (Tùy chọn - Admin có thể bỏ trống, lần đầu:
|
||||
# admin123, nút Điền nhanh).
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, hashed_password, must_change_password FROM users "
|
||||
"WHERE LOWER(role) = 'admin' ORDER BY created_at ASC LIMIT 1"
|
||||
)
|
||||
row = cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if not row:
|
||||
return {"first_time": True}
|
||||
still_default = False
|
||||
try:
|
||||
still_default = verify_password("admin123", row["hashed_password"])
|
||||
except Exception:
|
||||
still_default = False
|
||||
return {"first_time": bool(row["must_change_password"]) and still_default}
|
||||
except Exception:
|
||||
return {"first_time": True}
|
||||
|
||||
@@ -151,7 +151,11 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
|
||||
@router.get("/soundfonts/download/{sf_id}")
|
||||
async def download_soundfont_asset(sf_id: str):
|
||||
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
|
||||
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
|
||||
# Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước
|
||||
# đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng)
|
||||
# không tải được font → instrument CÂM (browser thường dùng cache nên OK).
|
||||
static_sf_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts")
|
||||
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR, static_sf_dir]:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
# Prefer SF2: the client FluidSynth WASM cannot decode SF3 (Ogg Vorbis)
|
||||
|
||||
+6
-1
@@ -81,7 +81,12 @@ async def get_index():
|
||||
if not os.path.exists(index_path):
|
||||
return HTMLResponse(content=f"<h1>SonicForge Studio: index.html not found at {index_path}</h1>", status_code=404)
|
||||
with open(index_path, "r", encoding="utf-8") as file:
|
||||
return HTMLResponse(content=file.read(), status_code=200)
|
||||
resp = HTMLResponse(content=file.read(), status_code=200)
|
||||
# no-cache: index.html PHẢI luôn mới (các bundle JS dùng ?v= để bust) —
|
||||
# nếu browser cache HTML cũ → stamp cũ → tải bundle cũ (bug "không load
|
||||
# được bundle mới" ở incognito — cache heuristic không có Cache-Control).
|
||||
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
return resp
|
||||
|
||||
|
||||
@app.get("/favicon.svg")
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"type": { "type": "string", "enum": ["AUDIO", "MIDI", "SECTION"] },
|
||||
"color": { "type": ["string", "null"], "default": null },
|
||||
"volume_db": { "type": "number", "default": 0.0 },
|
||||
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
|
||||
"mute": { "type": "boolean", "default": false },
|
||||
|
||||
+3144
-539
File diff suppressed because it is too large
Load Diff
+782
-141
File diff suppressed because one or more lines are too long
@@ -121,6 +121,57 @@ const AIGateway = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
// ai_midi_rearrange_specification.md §3 — Function Tool Schema hỗ trợ 2 mode:
|
||||
// SIMILAR_VARIATION (biến tấu cùng độ dài) / EXTEND_CONTINUATION (viết tiếp
|
||||
// các bar sau). AI trả gói dữ liệu có vị trí target trên Timeline.
|
||||
const REARRANGE_EXTEND_TOOL_SPEC = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'rearrange_or_extend_midi_melody',
|
||||
description: 'Analyzes source MIDI melody data and returns either a variation (Variation) or continuation (Extend) based on user instructions.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['SIMILAR_VARIATION', 'EXTEND_CONTINUATION'],
|
||||
description: "Mode: 'SIMILAR_VARIATION' (new arrangement of equal length) or 'EXTEND_CONTINUATION' (writes subsequent bars)."
|
||||
},
|
||||
composition_title: {
|
||||
type: 'string',
|
||||
description: 'Short title describing the new melody style (e.g., Jazz Swing Variation, Epic Extension Part 2)'
|
||||
},
|
||||
target_start_bar: {
|
||||
type: 'number',
|
||||
description: 'Starting bar number for the generated notes on the Timeline'
|
||||
},
|
||||
target_duration_bars: {
|
||||
type: 'number',
|
||||
description: 'Total bar duration covered by the generated sequence'
|
||||
},
|
||||
soundfont_id: { type: 'string', default: 'generaluser_gs' },
|
||||
soundfont_bank: { type: 'integer', default: 0 },
|
||||
soundfont_program: { type: 'integer', default: 0 },
|
||||
generated_notes: {
|
||||
type: 'array',
|
||||
description: 'Array of AI-generated MIDI notes.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pitch: { type: 'integer', minimum: 0, maximum: 127 },
|
||||
start_beat: { type: 'number', description: 'Starting beat position relative to beat 0.0 of the generated item' },
|
||||
duration_beats: { type: 'number', minimum: 0.1 },
|
||||
velocity: { type: 'number', minimum: 0.0, maximum: 1.0 }
|
||||
},
|
||||
required: ['pitch', 'start_beat', 'duration_beats', 'velocity']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['mode', 'composition_title', 'target_start_bar', 'target_duration_bars', 'generated_notes']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const REARRANGE_SCENARIOS = [
|
||||
{
|
||||
id: 'arpeggio',
|
||||
@@ -456,6 +507,7 @@ Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó
|
||||
return {
|
||||
DEFAULT_TOOLS,
|
||||
REARRANGE_TOOL_SPEC,
|
||||
REARRANGE_EXTEND_TOOL_SPEC,
|
||||
REARRANGE_SCENARIOS,
|
||||
detectRearrangeScenario,
|
||||
buildRearrangeMessage,
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
let _activeOscillators = {};
|
||||
let _gainNode = null;
|
||||
let _pendingOutputDestination = null;
|
||||
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
|
||||
let _validPercCache = {}; // { sfId: [bank, prog] | null } — preset percussion hợp lệ
|
||||
let _sfLoadFailAt = {}; // { sfId: timestamp } — cooldown 10s sau load fail
|
||||
let _scheduledNotes = [];
|
||||
let _loadPromises = {};
|
||||
let _sfloadSeq = 0;
|
||||
@@ -32,7 +35,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = _audioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return _audioCtx;
|
||||
}
|
||||
@@ -41,7 +45,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = ctx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination));
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -54,7 +59,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = window.__sharedAudioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(_pendingOutputDestination || window.__sharedAudioCtx.destination);
|
||||
_outputDestination = _pendingOutputDestination || window.__sharedAudioCtx.destination;
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
return window.__sharedAudioCtx;
|
||||
};
|
||||
@@ -68,10 +74,26 @@
|
||||
setOutputDestination: function (node) {
|
||||
try {
|
||||
if (_gainNode) {
|
||||
const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
|
||||
// DEDUPE: đích không đổi → KHÔNG disconnect/reconnect.
|
||||
// Swap dư giữa dòng notes đang phát (applyAllTrackMuteSolo →
|
||||
// updateSfRouting gọi lại cùng đích sfEntry sau noteon đầu)
|
||||
// làm ScriptProcessor xuất buffer uninitialized → NaN →
|
||||
// 11 biquad "state is bad" → CÂM (mọi log: state-bad nổ
|
||||
// ngay sau setOutputDestination lần 2).
|
||||
if (dest === _outputDestination) return;
|
||||
_gainNode.disconnect();
|
||||
_gainNode.connect(node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination)));
|
||||
_gainNode.connect(dest);
|
||||
// Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
|
||||
// trên _gainNode): disconnect() không đối số ngắt MỌI kết
|
||||
// nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
|
||||
// không nhảy khi có âm (user bug 06:35).
|
||||
if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
|
||||
_outputDestination = dest;
|
||||
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
|
||||
} else {
|
||||
_pendingOutputDestination = node || null;
|
||||
console.log('[SonicSF] setOutputDestination pending:', node ? 'track node (sfEntry)' : 'null');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[SonicSF] setOutputDestination error:', e);
|
||||
@@ -101,7 +123,8 @@
|
||||
if (!_gainNode) {
|
||||
_gainNode = _audioCtx.createGain();
|
||||
_gainNode.gain.value = 0.3;
|
||||
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
|
||||
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
|
||||
_gainNode.connect(_outputDestination);
|
||||
}
|
||||
|
||||
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
|
||||
@@ -137,7 +160,7 @@
|
||||
// expected notice when a soundfont simply has no
|
||||
// preset for a bank (e.g. bank 128 on a melodic-only
|
||||
// font) — the note is just silent, not an error.
|
||||
if (msg && msg.indexOf('No preset found on channel') !== -1) return;
|
||||
if (msg && (msg.indexOf('No preset found on channel') !== -1 || msg.indexOf('There is no preset with bank number') !== -1)) return;
|
||||
console.warn('[FluidSynth:err]', msg);
|
||||
}
|
||||
});
|
||||
@@ -188,17 +211,31 @@
|
||||
var spn = _audioCtx.createScriptProcessor(spBufSz, 0, 2);
|
||||
var lp = _fluidModule._malloc(spBufSz * 4);
|
||||
var rp = _fluidModule._malloc(spBufSz * 4);
|
||||
// Heap WASM có thể realloc khi load SoundFont lớn (SGM-V2.01
|
||||
// ~300MB) → lp/rp DANGLE → đọc vùng nhớ đã free → NaN/garbage
|
||||
// → master chain "state is bad" → CÂM + stuck. Theo dõi
|
||||
// buffer + re-malloc khi đổi.
|
||||
var _heapBufRef = _fluidModule.HEAPU8.buffer;
|
||||
spn.onaudioprocess = function (e) {
|
||||
var left = e.outputBuffer.getChannelData(0);
|
||||
var right = e.outputBuffer.getChannelData(1);
|
||||
var sz = left.length;
|
||||
try {
|
||||
if (_fluidModule.HEAPU8.buffer !== _heapBufRef) {
|
||||
try { _fluidModule._free(lp); _fluidModule._free(rp); } catch (er2) {}
|
||||
lp = _fluidModule._malloc(sz * 4);
|
||||
rp = _fluidModule._malloc(sz * 4);
|
||||
_heapBufRef = _fluidModule.HEAPU8.buffer;
|
||||
}
|
||||
_fluidModule._fluid_synth_write_float(_synthPtr, sz, lp, 0, 1, rp, 0, 1);
|
||||
var hf = _fluidModule.HEAPF32;
|
||||
var lpb = lp >> 2, rpb = rp >> 2;
|
||||
for (var si = 0; si < sz; si++) {
|
||||
left[si] = hf[lpb + si];
|
||||
right[si] = hf[rpb + si];
|
||||
// NaN sweep: mẫu NaN/Inf → 0 (chain biquad
|
||||
// KHÔNG BAO GIỜ được nhận NaN → không state-bad).
|
||||
var L = hf[lpb + si], R = hf[rpb + si];
|
||||
left[si] = isFinite(L) ? L : 0;
|
||||
right[si] = isFinite(R) ? R : 0;
|
||||
}
|
||||
} catch (er) {}
|
||||
};
|
||||
@@ -269,6 +306,12 @@
|
||||
|
||||
_doLoadSoundFont: async function (sfId) {
|
||||
try {
|
||||
// KHÔNG unload SF cũ khi sfload SF mới: unload làm handle cũ
|
||||
// thành rác trong khi channel state vẫn trỏ tới → program_select
|
||||
// bị skip (progAlreadySet) → noteon trên handle đã unload →
|
||||
// "Instrument not found ... substituted prog 0". Heap 256MB đủ
|
||||
// cho vài SF (SGM + latin = 2 handle — log OK). SF cũ khi cần
|
||||
// lại chỉ được sfload lại nếu map bị xóa (không xảy ra ở đây).
|
||||
var cache = window.SonicSFStorage;
|
||||
var buf = cache ? await cache.getBuffer(sfId) : null;
|
||||
if (buf) {
|
||||
@@ -289,8 +332,15 @@
|
||||
var url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now();
|
||||
var resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
console.warn("[SonicSF] SoundFont not found:", sfId);
|
||||
return false;
|
||||
// Fallback: font bundled theo deployment (static/soundfonts —
|
||||
// serve qua /soundfonts/{f} — catalog default-soundfonts).
|
||||
var url2 = "/soundfonts/" + encodeURIComponent(sfId.replace(/^sf_/, '')) + "?t=" + Date.now();
|
||||
var resp2 = await fetch(url2);
|
||||
if (!resp2.ok) {
|
||||
console.warn("[SonicSF] SoundFont not found:", sfId);
|
||||
return false;
|
||||
}
|
||||
resp = resp2;
|
||||
}
|
||||
buf = await resp.arrayBuffer();
|
||||
if (cache) await cache.saveBuffer(sfId, buf);
|
||||
@@ -449,10 +499,11 @@
|
||||
},
|
||||
|
||||
_playNoteFluid: function (note, velocity, durationMs, startTime, program, channel, synthEngine) {
|
||||
var midiPitch = Math.min(127, Math.max(0, parseInt(note) || 60));
|
||||
var midiVel = Math.min(127, Math.max(1, Math.floor(
|
||||
typeof velocity === 'number' ? (velocity > 1 ? velocity : velocity * 127) : 100
|
||||
)));
|
||||
var parsedPitch = parseInt(note);
|
||||
var midiPitch = isNaN(parsedPitch) ? 60 : Math.min(127, Math.max(0, parsedPitch));
|
||||
var rawVel = (typeof velocity === 'number' && isFinite(velocity)) ? (velocity > 1 ? velocity : velocity * 127) : 100;
|
||||
if (isNaN(rawVel)) rawVel = 100;
|
||||
var midiVel = Math.min(127, Math.max(1, Math.floor(rawVel)));
|
||||
var _origChannel = channel;
|
||||
var usedBank = 0, usedProg = 0;
|
||||
if (synthEngine) {
|
||||
@@ -485,8 +536,10 @@
|
||||
var self = this;
|
||||
var doNote = function () {
|
||||
try {
|
||||
var finalBank = usedBank;
|
||||
var finalProg = usedProg;
|
||||
var finalBank = parseInt(usedBank);
|
||||
if (isNaN(finalBank) || !isFinite(finalBank)) finalBank = 0;
|
||||
var finalProg = parseInt(usedProg);
|
||||
if (isNaN(finalProg) || !isFinite(finalProg)) finalProg = 0;
|
||||
var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined;
|
||||
var cachedCh = _channels[ch];
|
||||
// The note's own synth engine (track instrument) is
|
||||
@@ -495,16 +548,32 @@
|
||||
// re-picked instrument plays the wrong soundfont. Without an
|
||||
// engine, fall back to the soundfont configured on the channel.
|
||||
if (!synthEngine && cachedCh && cachedCh.sfId !== undefined) {
|
||||
finalBank = cachedCh.bank;
|
||||
finalProg = cachedCh.program;
|
||||
finalBank = parseInt(cachedCh.bank) || 0;
|
||||
finalProg = parseInt(cachedCh.program) || 0;
|
||||
finalSfId = cachedCh.sfId;
|
||||
}
|
||||
// Ensure the soundfont is actually loaded before the note plays.
|
||||
// Quick instrument pick on a track does not pre-load it, so load
|
||||
// lazily here and retry the note once the font is ready.
|
||||
if (finalSfId && !_sfHandleMap.has(finalSfId)) {
|
||||
// Cooldown lỗi: font 404 → KHÔNG spam fetch mỗi note (10s)
|
||||
// — note chạy thẳng fallback để CÓ ÂM.
|
||||
var _lastFail = _sfLoadFailAt[finalSfId] || 0;
|
||||
if (Date.now() - _lastFail < 10000) {
|
||||
try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
console.log('[SonicSF] soundfont not loaded yet, loading:', finalSfId);
|
||||
self.loadSoundFont(finalSfId).then(function (ok) {
|
||||
if (ok) doNote();
|
||||
console.log('[SonicSF] loadSoundFont result:', ok, 'for:', finalSfId);
|
||||
if (ok) {
|
||||
doNote();
|
||||
} else {
|
||||
// Font KHÔNG tải được (404/format) → KHÔNG drop note
|
||||
// câm lặng ("bỏ qua WASM") — fallback oscillator.
|
||||
_sfLoadFailAt[finalSfId] = Date.now();
|
||||
try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -512,12 +581,43 @@
|
||||
// instrument for each item regardless of processing order.
|
||||
// Skip if the channel already has this exact instrument (avoids
|
||||
// per-note soundfont reloads that cause audible crackle/glitches).
|
||||
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId;
|
||||
// ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu
|
||||
// không → vẫn program_select lại (tránh dùng handle đã unload).
|
||||
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId
|
||||
&& (finalSfId ? _sfHandleMap.has(finalSfId) : true);
|
||||
if ((synthEngine || program !== undefined) && !progAlreadySet) {
|
||||
var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined;
|
||||
console.log('[SonicSF] selectProgram for channel:', ch, 'sfHandle:', sfHandle, 'bank:', finalBank, 'prog:', finalProg);
|
||||
if (sfHandle !== undefined) {
|
||||
try {
|
||||
_fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
|
||||
// Percussion (bank 128): tìm preset HỢP LỆ trong
|
||||
// font — quét bank 128 + bank 0 (0-127) MỘT LẦN,
|
||||
// cache theo sfId. Trước đây chỉ thử 4 preset cố
|
||||
// định → font không có → cache channel = (128,0)
|
||||
// INVALID → note sau skip re-select (progAlreadySet)
|
||||
// → noteon preset rỗng = CÂM ("1 âm đầu rồi câm").
|
||||
if (finalBank === 128) {
|
||||
var _vKey = finalSfId || ('h' + sfHandle);
|
||||
if (_validPercCache[_vKey] === undefined) {
|
||||
var _found = null;
|
||||
for (var _b = 0; _b < 2 && !_found; _b++) {
|
||||
var _bk = _b === 0 ? 128 : 0;
|
||||
for (var _p = 0; _p < 128 && !_found; _p++) {
|
||||
try {
|
||||
if (_fluidModule._fluid_synth_program_select(_synthPtr, 9, sfHandle, _bk, _p) === 0) {
|
||||
_found = [_bk, _p];
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
_validPercCache[_vKey] = _found;
|
||||
}
|
||||
if (_validPercCache[_vKey]) {
|
||||
finalBank = _validPercCache[_vKey][0];
|
||||
finalProg = _validPercCache[_vKey][1];
|
||||
}
|
||||
}
|
||||
var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {}
|
||||
@@ -528,6 +628,7 @@
|
||||
_channels[ch].program = finalProg;
|
||||
_channels[ch].sfId = finalSfId;
|
||||
}
|
||||
console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId);
|
||||
_fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);
|
||||
var noteMapKey = (_origChannel !== undefined ? _origChannel : 0) + ':' + midiPitch;
|
||||
if (!_activeNotes[noteMapKey]) _activeNotes[noteMapKey] = [];
|
||||
@@ -558,6 +659,31 @@
|
||||
}
|
||||
},
|
||||
|
||||
// Hủy mọi note-on được schedule (tương lai) + note-off mọi notes đang
|
||||
// ngân — gọi khi STOP/PAUSE để hết "âm thanh bị stuck" (note-on chưa
|
||||
// bắn vẫn bắn sau khi dừng; notes durationMs>=60000 không có note-off
|
||||
// tự động → ngân vô hạn → VU master nhảy dù không play).
|
||||
panic: function () {
|
||||
_scheduledNotes.forEach(function (sn) { if (sn.on) { clearTimeout(sn.on); sn.on = null; } });
|
||||
_scheduledNotes = [];
|
||||
if (_initialized && _fluidModule) {
|
||||
// noteoff TỪNG note đang ngân (binding _fluid_synth_noteoff chắc
|
||||
// chắn tồn tại — đã dùng cho duration hết) — all_notes_off có
|
||||
// thể không có trong WASM exports (catch nuốt → notes kẹt).
|
||||
Object.keys(_activeNotes).forEach(function (key) {
|
||||
var parts = key.split(':');
|
||||
var pitch = parseInt(parts[1], 10);
|
||||
(_activeNotes[key] || []).forEach(function (ch) {
|
||||
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
|
||||
});
|
||||
});
|
||||
try {
|
||||
for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
|
||||
} catch (e) {}
|
||||
}
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
_playNoteFallback: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
|
||||
var ctx = getCtx();
|
||||
var freq = 440 * Math.pow(2, (note - 69) / 12);
|
||||
@@ -574,7 +700,11 @@
|
||||
var oscType = 'triangle';
|
||||
var attackTime = 0.03, decayTime = 0.1, sustainLevel = 0.5, releaseTime = 0.2, volFactor = 0.25;
|
||||
var prog = program !== undefined ? parseInt(program) : 0;
|
||||
if (channel !== undefined && channel >= 0 && channel < 16) {
|
||||
// CHỈ dùng cache channel khi KHÔNG có program/synthEngine được
|
||||
// truyền — trước đây override program của track bằng cache channel
|
||||
// (bị track khác cùng channel ghi đè → preview note vẽ mới mang
|
||||
// nhạc cụ của track TRƯỚC).
|
||||
if (program === undefined && channel !== undefined && channel >= 0 && channel < 16) {
|
||||
prog = _channels[channel].program || prog;
|
||||
}
|
||||
if (prog >= 0 && prog <= 7) { oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; }
|
||||
@@ -592,7 +722,8 @@
|
||||
osc.frequency.setValueAtTime(freq, 0);
|
||||
var startAt = startTime !== undefined ? startTime : ctx.currentTime;
|
||||
var durSec = durationMs / 1000;
|
||||
var vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||
var vel = (typeof velocity === 'number' && isFinite(velocity) && !isNaN(velocity)) ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||
if (isNaN(vel)) vel = 0.8;
|
||||
var targetGain = vel * volFactor;
|
||||
noteGain.gain.setValueAtTime(0, startAt);
|
||||
noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
|
||||
@@ -613,6 +744,15 @@
|
||||
|
||||
stopAll: function () {
|
||||
if (_initialized && _fluidModule) {
|
||||
// noteoff từng note đang ngân (binding chắc chắn tồn tại) —
|
||||
// phòng all_notes_off không có trong WASM exports.
|
||||
Object.keys(_activeNotes).forEach(function (key) {
|
||||
var parts = key.split(':');
|
||||
var pitch = parseInt(parts[1], 10);
|
||||
(_activeNotes[key] || []).forEach(function (ch) {
|
||||
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
|
||||
});
|
||||
});
|
||||
for (var ch = 0; ch < 16; ch++) {
|
||||
try { _fluidModule._fluid_synth_all_notes_off(_synthPtr, ch); } catch (e) {}
|
||||
}
|
||||
@@ -631,6 +771,7 @@
|
||||
} catch (e) {}
|
||||
});
|
||||
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
saveToIndexedDB: async function (name, arrayBuffer) {
|
||||
|
||||
@@ -60,11 +60,13 @@
|
||||
}
|
||||
|
||||
let autoSaveTimer = null;
|
||||
let lastGetProjectStateCallback = null;
|
||||
function scheduleTempAutoSave(getProjectStateCallback) {
|
||||
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
|
||||
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(async () => {
|
||||
try {
|
||||
const state = getProjectStateCallback();
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
@@ -76,10 +78,26 @@
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
|
||||
async function flushTempAutoSave() {
|
||||
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
||||
try {
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
|
||||
await window.SonicAPI.saveTempProject(dataJson).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Flush temp project warning:", e);
|
||||
}
|
||||
}
|
||||
|
||||
window.SonicStorage = {
|
||||
exportProjectToSFS,
|
||||
importProjectFromSFSFile,
|
||||
scheduleTempAutoSave
|
||||
scheduleTempAutoSave,
|
||||
flushTempAutoSave
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/api.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608070635"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608035800" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user