fix(midi): route MIDI live via masterBus, fix media explorer drag, add action logging

- TrackInstrument: gate native live path (SF/VSTi WASAPI bridge bypassed
  masterBus -> mastering FX + main out VU dead). Fallback to SonicSF WASM
  through masterBus.input; native kept for offline render only.
- MEDIA_LIBRARY_SAMPLES: rename MIDI_Loop_02_Bass/03_Lead/05_Bass to match
  actual /static/midi files (drag was 404 -> parse error).
- Add SonicAppLogger (console + POST /api/v1/logs) wired to: instrument load
  error, VSTi autosample fail, MIDI item -> mastering fx chain, play -> main
  out, drag MIDI errors (media explorer + window explorer).
- Add backend /api/v1/logs endpoint (server-side log, no DB).
This commit is contained in:
2026-08-11 21:48:06 +07:00
parent e3c9d6e4ab
commit 59cf77bcbc
7 changed files with 121 additions and 15 deletions
+39
View File
@@ -0,0 +1,39 @@
# SonicForge Client Logs API — client gửi log hành động (MIDI/mastering/drag
# errors) qua POST /api/v1/logs. Không lưu DB — in ra server log (uvicorn
# console). Public: log không chứa dữ liệu nhạy (client tự quyết định).
import logging
from typing import Any, Optional
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
_logger = logging.getLogger("sonicforge.client_logs")
class ClientLogEntry(BaseModel):
category: str = "GENERAL"
level: str = "info"
message: str = ""
data: Optional[dict[str, Any]] = None
url: Optional[str] = None
ts: Optional[str] = None
@router.post("")
async def post_client_log(entry: ClientLogEntry):
"""Ghi log từ client vào server log (fire-and-forget, luôn 200)."""
tag = f"[ClientLog][{entry.category}][{entry.level}]"
line = f"{tag} {entry.message}"
if entry.data:
line += f" {entry.data}"
if entry.ts:
line += f" (ts={entry.ts})"
if entry.url:
line += f" url={entry.url}"
if entry.level == "error":
_logger.error(line)
elif entry.level == "warn":
_logger.warning(line)
else:
_logger.info(line)
return {"ok": True}
+2
View File
@@ -21,6 +21,7 @@ from app.api.v1.media import router as media_router
from app.api.v1.system import router as system_router
from app.api.v1.presets import router as presets_router
from app.api.v1.native import router as native_router
from app.api.v1.logs import router as logs_router
from app.core.auth import seed_admin
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -114,6 +115,7 @@ app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
app.include_router(system_router, prefix="/api/v1/system", tags=["system"])
app.include_router(presets_router, prefix="/api/v1/presets", tags=["presets"])
app.include_router(native_router, prefix="/api/v1/native", tags=["native"])
app.include_router(logs_router, prefix="/api/v1/logs", tags=["logs"])
@app.get("/health")
+21 -5
View File
@@ -6284,7 +6284,9 @@ const preloadTrackInstruments = async (tracks) => {
const ch = t.midiChannel !== undefined ? t.midiChannel : (i % 16);
try {
await window.SonicSF.selectInstrument(ch, bank, prog, sfId);
} catch (e) {}
} catch (e) {
try { window.SonicAppLogger && window.SonicAppLogger.error('INSTRUMENT', 'Load instrument fail', { track: t.name, sfId: sfId, bank: bank, prog: prog, err: e && e.message }); } catch (e2) {}
}
}
};
@@ -13203,10 +13205,10 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
// Media Explorer Panel (from md/51_MEDIA_EXPLORER.md)
const MEDIA_LIBRARY_SAMPLES = [
{ name: "MIDI_Loop_01.mid", events: 95, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_02_Bass.mid", events: 48, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_03_Lead.mid", events: 110, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_02.mid", events: 48, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_03.mid", events: 110, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_04.mid", events: 76, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_05_Bass.mid", events: 52, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_05.mid", events: 52, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
{ name: "MIDI_Loop_06.mid", events: 88, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" }
];
@@ -15592,7 +15594,10 @@ const App = () => {
window.SonicVstiAutosample.ensure(_warmEngine).then(_sfId => {
if (!_sfId && window.SonicVstiAutosample.failReasonFor) {
const _reason = window.SonicVstiAutosample.failReasonFor(_warmEngine);
if (_reason && window.showToast) window.showToast('Autosample VSTi that bai: ' + _reason + ' - dung am default', 'warning');
if (_reason) {
try { window.SonicAppLogger && window.SonicAppLogger.error('VSTI', 'Autosample VSTi fail', { plugin_id: instrumentId, reason: _reason }); } catch (e2) {}
if (window.showToast) window.showToast('Autosample VSTi that bai: ' + _reason + ' - dung am default', 'warning');
}
}
});
}
@@ -21879,6 +21884,11 @@ const App = () => {
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
try {
const _mActive = !!(masterBus && masterBus.masteringActive);
window.SonicAppLogger && window.SonicAppLogger.info('MIDI_ITEM', nativeSf ? 'render_offline_native -> WAV -> mastering fx chain' : 'WASM -> masterBus.input -> mastering fx chain', { track: track.id, name: track.name, items: midiItems.length, masteringActive: _mActive, nativeSf: !!nativeSf });
window.SonicAppLogger && window.SonicAppLogger.info('MIDI_ITEM', 'play_qua_main_out', { track: track.id, name: track.name, items: midiItems.length, dest: 'masterBus.analyser -> ctx.destination' });
} catch (e2) {}
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
} else {
@@ -24540,9 +24550,11 @@ const App = () => {
return new File([blob], mef.name, { type: 'audio/midi' });
}
} catch (e) {
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Media Explorer drag resolve fail', { name: mef && mef.name, kind: mef && mef.kind, err: e && e.message }); } catch (e2) {}
showToast('Không thể nạp file từ Media Explorer: ' + e.message, 'error');
return null;
}
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Media Explorer drag missing file data', { name: mef && mef.name, kind: mef && mef.kind }); } catch (e2) {}
showToast('Không thể nạp file từ Media Explorer: thiếu dữ liệu file.', 'error');
return null;
};
@@ -24557,6 +24569,7 @@ const App = () => {
var arrayBuffer = await file.arrayBuffer();
var midiResult = parseMidiFile(arrayBuffer);
if (!midiResult || midiResult.length === 0) {
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Window explorer drag parse empty', { name: fileName, size: file.size }); } catch (e2) {}
showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error');
return;
}
@@ -24612,6 +24625,7 @@ const App = () => {
} : t));
showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`, 'success');
} catch (err) {
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Window explorer drag load fail', { name: fileName, isMidi: isMidi, err: err && err.message }); } catch (e2) {}
showToast(isMidi ? "Lỗi giải mã MIDI." : "Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error');
}
};
@@ -24624,6 +24638,7 @@ const App = () => {
var arrayBuffer = await file.arrayBuffer();
var midiResult = parseMidiFile(arrayBuffer);
if (!midiResult || midiResult.length === 0) {
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Drag MIDI parse empty', { name: file && file.name, size: file && file.size }); } catch (e2) {}
showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error');
return;
}
@@ -24645,6 +24660,7 @@ const App = () => {
if (newTracks.length > 0) setSelectedTrackId(newTracks[0].id);
showToast('Đã tải MIDI: ' + newTracks.length + ' track(s) từ ' + file.name, 'success');
} catch (err) {
try { window.SonicAppLogger && window.SonicAppLogger.error('DRAG_MIDI', 'Drag MIDI decode fail', { name: file && file.name, err: err && err.message }); } catch (e2) {}
showToast('Lỗi giải mã MIDI.', 'error');
}
};
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
// SonicForge App Logger — ghi hành động quan trọng của client (console + POST
// /api/v1/logs để server log). Fire-and-forget: không throw, không block UI.
// Các category dùng chung:
// INSTRUMENT — lỗi load instrument (soundfont select/load fail)
// VSTI — VSTi load/autosample/preview fail
// MIDI_ITEM — MIDI item qua mastering fx chain / main out (playback)
// DRAG_MIDI — lỗi drag MIDI từ MEDIA EXPLORER hoặc window explorer
window.SonicAppLogger = window.SonicAppLogger || (function () {
function base() {
return window.API_BASE_URL || window.location.origin || '';
}
function emit(category, level, message, data) {
try {
var line = '[AppLog][' + category + '][' + level + '] ' + message;
if (data !== undefined && data !== null) {
try { line += ' ' + JSON.stringify(data); } catch (e) {}
}
if (level === 'error') console.error(line); else if (level === 'warn') console.warn(line); else console.log(line);
try {
fetch(base() + '/api/v1/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
category: category,
level: level,
message: message,
data: data !== undefined ? data : null,
url: window.location ? window.location.href : '',
ts: new Date().toISOString()
})
}).catch(function () {});
} catch (e) {}
} catch (e) {}
}
return {
info: function (category, message, data) { emit(category, 'info', message, data); },
warn: function (category, message, data) { emit(category, 'warn', message, data); },
error: function (category, message, data) { emit(category, 'error', message, data); }
};
})();
@@ -22,7 +22,15 @@ window.TrackInstrument = window.TrackInstrument || {};
// Loại engine native cho track: 'sf' (soundfont), 'vst3'/'vst2' (VSTi có
// plugin_id) hay null (không native được → fallback WASM/autosample).
// T15-regression fix: native live (SF/VSTi qua WASAPI bridge) phát thẳng ra
// OS device — BỎ QUA WebAudio masterBus → mastering FX + Main out VU không
// nhận tín hiệu. Gate native live path: luôn fallback WASM (SonicSF/
// autosample) qua masterBus.input. Native vẫn dùng cho OFFLINE render
// (scheduleNativeSfItem / /api/v1/native/render) — không đi qua hàm này.
// ponytail: re-enable live native khi C++ bridge có capture/readback →
// set window.__SONICFORGE_NATIVE_LIVE = true.
TrackInstrument.prototype._nativeKind = function () {
if (!window.__SONICFORGE_NATIVE_LIVE) return null;
try {
if (this.sfId) return 'sf';
if (window.SonicNativeAudio && this.synthEngine && this.synthEngine.plugin_id) {
+4 -3
View File
@@ -34,11 +34,12 @@
<script src="/static/vendor/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/runtime.js?v=202608111230"></script>
<script src="/static/js/services/api.js?v=202608111230"></script>
<script src="/static/js/services/api.js?v=202608111301"></script>
<script src="/static/js/services/appLogger.js?v=202608111301"></script>
<script src="/static/js/services/vstiAutosample.js?v=202608111230"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608111230"></script>
<script src="/static/js/services/nativeAudioClient.js?v=202608111230"></script>
<script src="/static/js/services/trackInstrument.js?v=202608111230"></script>
<script src="/static/js/services/trackInstrument.js?v=202608111301"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
@@ -50,7 +51,7 @@
<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=202608111230" defer></script>
<script src="/static/js/app.precompiled.js?v=202608111301" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {