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:
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user