FIX: sửa MASTERING PANEL thay đổi knob thì thay đổi các thông số hiển thị
This commit is contained in:
+468
-101
@@ -226,6 +226,16 @@ function initMasterBus(ctx) {
|
|||||||
const analyser = ctx.createAnalyser();
|
const analyser = ctx.createAnalyser();
|
||||||
analyser.fftSize = 256;
|
analyser.fftSize = 256;
|
||||||
|
|
||||||
|
const leftAnalyser = ctx.createAnalyser();
|
||||||
|
leftAnalyser.fftSize = 2048;
|
||||||
|
const rightAnalyser = ctx.createAnalyser();
|
||||||
|
rightAnalyser.fftSize = 2048;
|
||||||
|
|
||||||
|
const splitter = ctx.createChannelSplitter(2);
|
||||||
|
output.connect(splitter);
|
||||||
|
splitter.connect(leftAnalyser, 0);
|
||||||
|
splitter.connect(rightAnalyser, 1);
|
||||||
|
|
||||||
masterBus = {
|
masterBus = {
|
||||||
input: ctx.createGain(),
|
input: ctx.createGain(),
|
||||||
compressor: ctx.createDynamicsCompressor(),
|
compressor: ctx.createDynamicsCompressor(),
|
||||||
@@ -236,6 +246,8 @@ function initMasterBus(ctx) {
|
|||||||
// Analysers for metering
|
// Analysers for metering
|
||||||
inputAnalyser,
|
inputAnalyser,
|
||||||
outputAnalyser,
|
outputAnalyser,
|
||||||
|
leftAnalyser,
|
||||||
|
rightAnalyser,
|
||||||
|
|
||||||
// Mastering nodes
|
// Mastering nodes
|
||||||
eqLowFilter, eqMid1Filter, eqMid2Filter, eqHighFilter,
|
eqLowFilter, eqMid1Filter, eqMid2Filter, eqHighFilter,
|
||||||
@@ -6114,6 +6126,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
keybedMouseDownRef.current = true;
|
keybedMouseDownRef.current = true;
|
||||||
try {
|
try {
|
||||||
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(st.trackId, 100);
|
||||||
|
}
|
||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
||||||
}
|
}
|
||||||
@@ -6124,6 +6139,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
onMouseEnter: (e) => {
|
onMouseEnter: (e) => {
|
||||||
if (keybedMouseDownRef.current) {
|
if (keybedMouseDownRef.current) {
|
||||||
try {
|
try {
|
||||||
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(st.trackId, 100);
|
||||||
|
}
|
||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
||||||
}
|
}
|
||||||
@@ -7016,6 +7034,79 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// MASTERING KNOB COMPONENT (Dynamic pointer events version)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
const MasteringKnob = ({ param, min, max, value, unit, label, color, onChange, size = 'small' }) => {
|
||||||
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
|
const startYRef = React.useRef(0);
|
||||||
|
const startValRef = React.useRef(0);
|
||||||
|
|
||||||
|
const handlePointerDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
startYRef.current = e.clientY;
|
||||||
|
startValRef.current = value;
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const deltaY = startYRef.current - e.clientY;
|
||||||
|
let newVal = startValRef.current + (deltaY / 150) * (max - min);
|
||||||
|
newVal = Math.min(max, Math.max(min, newVal));
|
||||||
|
onChange(param, newVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pct = (value - min) / (max - min);
|
||||||
|
const angle = -135 + pct * 270;
|
||||||
|
|
||||||
|
const isLarge = size === 'large';
|
||||||
|
const dialClass = isLarge ? 'w-20 h-20 border-4 bg-slate-900' : 'w-10 h-10 border-2 bg-slate-800';
|
||||||
|
const pointerHeight = isLarge ? 'h-6' : 'h-3';
|
||||||
|
const valClass = isLarge ? 'text-xs text-cyan-300 font-bold mt-2 z-10' : 'text-[9px] text-slate-300 font-mono mt-1 font-bold';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center select-none">
|
||||||
|
{label && <span className="text-[10px] font-bold text-slate-400 mb-1.5 uppercase tracking-wide">{label}</span>}
|
||||||
|
<div
|
||||||
|
className={`${dialClass} rounded-full relative flex items-center justify-center cursor-ns-resize shadow-lg`}
|
||||||
|
style={{ borderColor: color }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerCancel={handlePointerUp}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-0.5 absolute rounded origin-bottom"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
height: isLarge ? '22px' : '12px',
|
||||||
|
top: isLarge ? '6px' : '4px',
|
||||||
|
transform: `rotate(${angle}deg)`,
|
||||||
|
transformOrigin: '50% 100%'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
{isLarge && (
|
||||||
|
<span className="text-[10px] font-bold text-cyan-300 z-10 bg-slate-950/80 px-1 py-0.5 rounded border border-slate-800">
|
||||||
|
{value > 0 && unit === 'dB' ? '+' : ''}{value.toFixed(1)} {unit}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isLarge && (
|
||||||
|
<span className={valClass}>
|
||||||
|
{value > 0 && unit === 'dB' ? '+' : ''}{value.toFixed(1)}{unit}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// MASTERING MODAL COMPONENT (from md/45_MASTERING_MODULE.md)
|
// MASTERING MODAL COMPONENT (from md/45_MASTERING_MODULE.md)
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
@@ -7039,6 +7130,30 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
const animFrameRef = React.useRef(null);
|
const animFrameRef = React.useRef(null);
|
||||||
const knobsInitializedRef = React.useRef(false);
|
const knobsInitializedRef = React.useRef(false);
|
||||||
|
|
||||||
|
// Wave Observer Refs & States
|
||||||
|
const woCanvasRef = React.useRef(null);
|
||||||
|
const woLeftHistoryRef = React.useRef(new Float32Array(400).fill(0));
|
||||||
|
const woRightHistoryRef = React.useRef(new Float32Array(400).fill(0));
|
||||||
|
const woLeftMeterRef = React.useRef(null);
|
||||||
|
const woRightMeterRef = React.useRef(null);
|
||||||
|
|
||||||
|
const [woPaused, setWoPaused] = React.useState(false);
|
||||||
|
const [woChannel, setWoChannel] = React.useState('stereo');
|
||||||
|
const [woMode, setWoMode] = React.useState('waveform');
|
||||||
|
const [woDuration, setWoDuration] = React.useState(2.0);
|
||||||
|
const [woZoom, setWoZoom] = React.useState(0.0);
|
||||||
|
|
||||||
|
const woPausedRef = React.useRef(woPaused);
|
||||||
|
woPausedRef.current = woPaused;
|
||||||
|
const woChannelRef = React.useRef(woChannel);
|
||||||
|
woChannelRef.current = woChannel;
|
||||||
|
const woModeRef = React.useRef(woMode);
|
||||||
|
woModeRef.current = woMode;
|
||||||
|
const woDurationRef = React.useRef(woDuration);
|
||||||
|
woDurationRef.current = woDuration;
|
||||||
|
const woZoomRef = React.useRef(woZoom);
|
||||||
|
woZoomRef.current = woZoom;
|
||||||
|
|
||||||
const ozStateRef = React.useRef(ozState);
|
const ozStateRef = React.useRef(ozState);
|
||||||
ozStateRef.current = ozState;
|
ozStateRef.current = ozState;
|
||||||
|
|
||||||
@@ -7097,6 +7212,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
resizeCanvas(imagerCanvasRef);
|
resizeCanvas(imagerCanvasRef);
|
||||||
resizeCanvas(inMeterCanvasRef);
|
resizeCanvas(inMeterCanvasRef);
|
||||||
resizeCanvas(outMeterCanvasRef);
|
resizeCanvas(outMeterCanvasRef);
|
||||||
|
resizeCanvas(woCanvasRef);
|
||||||
}
|
}
|
||||||
resizeAll();
|
resizeAll();
|
||||||
window.addEventListener('resize', resizeAll);
|
window.addEventListener('resize', resizeAll);
|
||||||
@@ -7223,61 +7339,163 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
};
|
};
|
||||||
renderMeter(masterBus && masterBus.inputAnalyser, inMeterCanvasRef, 'inPeakText');
|
renderMeter(masterBus && masterBus.inputAnalyser, inMeterCanvasRef, 'inPeakText');
|
||||||
renderMeter(masterBus && masterBus.outputAnalyser, outMeterCanvasRef, 'outPeakText');
|
renderMeter(masterBus && masterBus.outputAnalyser, outMeterCanvasRef, 'outPeakText');
|
||||||
|
|
||||||
|
// Wave Observer Oscilloscope Rendering
|
||||||
|
const woCanvas = woCanvasRef.current;
|
||||||
|
if (woCanvas) {
|
||||||
|
const w = woCanvas.width, h = woCanvas.height;
|
||||||
|
const woCtx = woCanvas.getContext('2d');
|
||||||
|
woCtx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
woCtx.strokeStyle = 'rgba(51, 65, 85, 0.2)';
|
||||||
|
woCtx.lineWidth = 1;
|
||||||
|
woCtx.font = '8px JetBrains Mono, monospace';
|
||||||
|
woCtx.fillStyle = '#475569';
|
||||||
|
|
||||||
|
const centerY = h / 2;
|
||||||
|
const gridLines = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
|
||||||
|
gridLines.forEach(g => {
|
||||||
|
const y = centerY + g * centerY;
|
||||||
|
woCtx.beginPath();
|
||||||
|
woCtx.moveTo(0, y);
|
||||||
|
woCtx.lineTo(w, y);
|
||||||
|
woCtx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vertical lines
|
||||||
|
const ticksCount = 10;
|
||||||
|
for (let i = 1; i <= ticksCount; i++) {
|
||||||
|
const x = (i / (ticksCount + 1)) * w;
|
||||||
|
woCtx.beginPath();
|
||||||
|
woCtx.moveTo(x, 0);
|
||||||
|
woCtx.lineTo(x, h);
|
||||||
|
woCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// dB labels on left side
|
||||||
|
woCtx.fillText('-6.0 dB', 5, centerY - 0.5 * centerY + 3);
|
||||||
|
woCtx.fillText('-9.0 dB', 5, centerY - 0.35 * centerY + 3);
|
||||||
|
woCtx.fillText('-15.0 dB', 5, centerY - 0.18 * centerY + 3);
|
||||||
|
woCtx.fillText('-27.0 dB', 5, centerY - 0.05 * centerY + 3);
|
||||||
|
|
||||||
|
woCtx.fillText('-27.0 dB', 5, centerY + 0.05 * centerY + 3);
|
||||||
|
woCtx.fillText('-15.0 dB', 5, centerY + 0.18 * centerY + 3);
|
||||||
|
woCtx.fillText('-9.0 dB', 5, centerY + 0.35 * centerY + 3);
|
||||||
|
woCtx.fillText('-6.0 dB', 5, centerY + 0.5 * centerY + 3);
|
||||||
|
|
||||||
|
// Time indicators at the bottom
|
||||||
|
const durationSec = woDurationRef.current;
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
const timeVal = (i / 6) * durationSec;
|
||||||
|
const x = (i / 6) * w;
|
||||||
|
woCtx.fillText(timeVal.toFixed(2) + 's', x - 10, h - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftPeak = 0;
|
||||||
|
let rightPeak = 0;
|
||||||
|
|
||||||
|
if (!woPausedRef.current && masterBus && masterBus.leftAnalyser && masterBus.rightAnalyser) {
|
||||||
|
const leftData = new Float32Array(512);
|
||||||
|
const rightData = new Float32Array(512);
|
||||||
|
masterBus.leftAnalyser.getFloatTimeDomainData(leftData);
|
||||||
|
masterBus.rightAnalyser.getFloatTimeDomainData(rightData);
|
||||||
|
|
||||||
|
for (let i = 0; i < 512; i++) {
|
||||||
|
const l = Math.abs(leftData[i]);
|
||||||
|
const r = Math.abs(rightData[i]);
|
||||||
|
if (l > leftPeak) leftPeak = l;
|
||||||
|
if (r > rightPeak) rightPeak = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lHistory = woLeftHistoryRef.current;
|
||||||
|
const rHistory = woRightHistoryRef.current;
|
||||||
|
|
||||||
|
// Shift history buffer Left
|
||||||
|
for (let i = 0; i < lHistory.length - 1; i++) {
|
||||||
|
lHistory[i] = lHistory[i + 1];
|
||||||
|
rHistory[i] = rHistory[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (woModeRef.current === 'envelope') {
|
||||||
|
lHistory[lHistory.length - 1] = leftPeak;
|
||||||
|
rHistory[rHistory.length - 1] = rightPeak;
|
||||||
|
} else {
|
||||||
|
lHistory[lHistory.length - 1] = leftData[0];
|
||||||
|
rHistory[rHistory.length - 1] = rightData[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lHistory = woLeftHistoryRef.current;
|
||||||
|
const rHistory = woRightHistoryRef.current;
|
||||||
|
const zoomGain = Math.pow(10, woZoomRef.current / 20);
|
||||||
|
|
||||||
|
// Update input meter bars
|
||||||
|
if (woLeftMeterRef.current && woRightMeterRef.current) {
|
||||||
|
const lPct = Math.min(100, leftPeak * 100);
|
||||||
|
const rPct = Math.min(100, rightPeak * 100);
|
||||||
|
woLeftMeterRef.current.style.width = `${lPct}%`;
|
||||||
|
woRightMeterRef.current.style.width = `${rPct}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left channel line (cyan)
|
||||||
|
if (woChannelRef.current === 'stereo' || woChannelRef.current === 'left') {
|
||||||
|
woCtx.strokeStyle = '#22d3ee';
|
||||||
|
woCtx.lineWidth = 1.2;
|
||||||
|
woCtx.beginPath();
|
||||||
|
for (let i = 0; i < lHistory.length; i++) {
|
||||||
|
const x = (i / (lHistory.length - 1)) * w;
|
||||||
|
const val = lHistory[i] * zoomGain;
|
||||||
|
const y = centerY - val * centerY;
|
||||||
|
if (i === 0) woCtx.moveTo(x, y);
|
||||||
|
else woCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
woCtx.stroke();
|
||||||
|
|
||||||
|
if (woModeRef.current === 'envelope') {
|
||||||
|
woCtx.beginPath();
|
||||||
|
for (let i = 0; i < lHistory.length; i++) {
|
||||||
|
const x = (i / (lHistory.length - 1)) * w;
|
||||||
|
const val = lHistory[i] * zoomGain;
|
||||||
|
const y = centerY + val * centerY;
|
||||||
|
if (i === 0) woCtx.moveTo(x, y);
|
||||||
|
else woCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
woCtx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right channel line (teal)
|
||||||
|
if (woChannelRef.current === 'stereo' || woChannelRef.current === 'right') {
|
||||||
|
woCtx.strokeStyle = '#0d9488';
|
||||||
|
woCtx.lineWidth = 1.2;
|
||||||
|
woCtx.beginPath();
|
||||||
|
for (let i = 0; i < rHistory.length; i++) {
|
||||||
|
const x = (i / (rHistory.length - 1)) * w;
|
||||||
|
const val = rHistory[i] * zoomGain;
|
||||||
|
const y = centerY - val * centerY;
|
||||||
|
if (i === 0) woCtx.moveTo(x, y);
|
||||||
|
else woCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
woCtx.stroke();
|
||||||
|
|
||||||
|
if (woModeRef.current === 'envelope') {
|
||||||
|
woCtx.beginPath();
|
||||||
|
for (let i = 0; i < rHistory.length; i++) {
|
||||||
|
const x = (i / (rHistory.length - 1)) * w;
|
||||||
|
const val = rHistory[i] * zoomGain;
|
||||||
|
const y = centerY + val * centerY;
|
||||||
|
if (i === 0) woCtx.moveTo(x, y);
|
||||||
|
else woCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
woCtx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFrame();
|
renderFrame();
|
||||||
|
|
||||||
// Knob setup
|
|
||||||
if (!knobsInitializedRef.current) {
|
|
||||||
knobsInitializedRef.current = true;
|
|
||||||
const container = document.getElementById('masteringModalBody');
|
|
||||||
if (container) {
|
|
||||||
container.querySelectorAll('.knob-container').forEach(knob => {
|
|
||||||
let isDragging = false, startY = 0, startVal = 0;
|
|
||||||
const param = knob.dataset.param;
|
|
||||||
if (!param) return;
|
|
||||||
const min = parseFloat(knob.dataset.min);
|
|
||||||
const max = parseFloat(knob.dataset.max);
|
|
||||||
const unit = knob.dataset.unit || '';
|
|
||||||
const dial = knob.querySelector('.knob-dial');
|
|
||||||
const valText = knob.querySelector('.knob-val');
|
|
||||||
|
|
||||||
function updateUI(val) {
|
|
||||||
const pct = (val - min) / (max - min);
|
|
||||||
const angle = -135 + pct * 270;
|
|
||||||
if (dial) dial.style.transform = `rotate(${angle}deg)`;
|
|
||||||
if (valText) valText.innerText = `${val > 0 && unit === 'dB' ? '+' : ''}${val.toFixed(1)} ${unit}`;
|
|
||||||
setOzState(prev => ({ ...prev, [param]: val }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const startValParsed = parseFloat(knob.dataset.value);
|
|
||||||
updateUI(startValParsed);
|
|
||||||
|
|
||||||
knob.addEventListener('pointerdown', e => {
|
|
||||||
isDragging = true;
|
|
||||||
startY = e.clientY;
|
|
||||||
startVal = ozStateRef.current[param] !== undefined ? ozStateRef.current[param] : startValParsed;
|
|
||||||
knob.setPointerCapture(e.pointerId);
|
|
||||||
});
|
|
||||||
knob.addEventListener('pointermove', e => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
const deltaY = startY - e.clientY;
|
|
||||||
let newVal = startVal + (deltaY / 100) * (max - min);
|
|
||||||
newVal = Math.min(max, Math.max(min, newVal));
|
|
||||||
updateUI(newVal);
|
|
||||||
});
|
|
||||||
knob.addEventListener('pointerup', e => {
|
|
||||||
isDragging = false;
|
|
||||||
knob.releasePointerCapture(e.pointerId);
|
|
||||||
});
|
|
||||||
knob.addEventListener('pointercancel', e => {
|
|
||||||
isDragging = false;
|
|
||||||
knob.releasePointerCapture(e.pointerId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', resizeAll);
|
window.removeEventListener('resize', resizeAll);
|
||||||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||||||
@@ -7298,22 +7516,23 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));
|
const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));
|
||||||
|
|
||||||
const bandKnob = (param, min, max, val, unit, label, freq, color, filterType) => (
|
const bandKnob = (param, min, max, val, unit, label, freq, color, filterType) => (
|
||||||
<div className="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
|
<div className="bg-slate-900/80 border border-slate-800 p-2 flex flex-col justify-between items-center rounded-lg">
|
||||||
<div className="flex items-center justify-between text-xs font-bold" style={{color}}>
|
<div className="flex items-center justify-between text-[10px] font-bold w-full" style={{color}}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span className="text-[10px] font-mono text-slate-400">{freq}</span>
|
<span className="text-[9px] font-mono text-slate-400">{freq}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-around my-2">
|
<div className="my-1">
|
||||||
<div className="knob-container" data-param={param} data-min={min} data-max={max} data-value={val} data-unit={unit}>
|
<MasteringKnob
|
||||||
<div className="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 relative flex items-center justify-center shadow-md" style={{borderColor: color}}>
|
param={param}
|
||||||
<div className="knob-pointer w-0.5 h-3 rounded absolute top-1" style={{backgroundColor: color}}></div>
|
min={min}
|
||||||
</div>
|
max={max}
|
||||||
</div>
|
value={ozState[param] !== undefined ? ozState[param] : val}
|
||||||
<div className="text-center font-mono">
|
unit={unit}
|
||||||
<span className="knob-val text-xs text-slate-200 font-bold">{val > 0 && unit === 'dB' ? '+' : ''}{val.toFixed(1)} {unit}</span>
|
color={color}
|
||||||
</div>
|
onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-slate-500 font-mono text-center">{filterType}</div>
|
<div className="text-[9px] text-slate-500 font-mono text-center mt-1">{filterType}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -7489,49 +7708,156 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
<div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
|
<div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
|
||||||
<div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
|
<div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
|
||||||
<span className="text-xs font-bold text-slate-400 uppercase oz-font-mono mb-3">Maximizer Gain Boost</span>
|
<span className="text-xs font-bold text-slate-400 uppercase oz-font-mono mb-3">Maximizer Gain Boost</span>
|
||||||
<div className="knob-container my-2" data-param="maxGain" data-min="0" data-max="12" data-value="5.4" data-unit="dB">
|
<div className="my-2">
|
||||||
<div className="knob-dial w-24 h-24 rounded-full bg-slate-900 border-4 border-cyan-500 relative flex items-center justify-center shadow-2xl">
|
<MasteringKnob
|
||||||
<div className="knob-pointer w-1 h-8 bg-cyan-400 rounded absolute top-2"></div>
|
param="maxGain"
|
||||||
<span className="knob-val text-sm font-bold oz-font-mono text-cyan-300 z-10">+5.4 dB</span>
|
min={0}
|
||||||
</div>
|
max={12}
|
||||||
|
value={ozState.maxGain}
|
||||||
|
unit="dB"
|
||||||
|
color="#22d3ee"
|
||||||
|
size="large"
|
||||||
|
onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs oz-font-mono flex justify-between items-center">
|
<div className="w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs oz-font-mono flex flex-col gap-1.5">
|
||||||
<span className="text-slate-400">Ceiling Level:</span>
|
<div className="flex justify-between items-center w-full">
|
||||||
<span className="text-cyan-400 font-bold">{ozState.ceiling.toFixed(2)} dB</span>
|
<span className="text-slate-400">Ceiling Level:</span>
|
||||||
|
<span className="text-cyan-400 font-bold">{ozState.ceiling.toFixed(2)} dB</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-12"
|
||||||
|
max="0"
|
||||||
|
step="0.1"
|
||||||
|
value={ozState.ceiling}
|
||||||
|
onChange={e => setOzState(prev => ({ ...prev, ceiling: parseFloat(e.target.value) }))}
|
||||||
|
className="w-full h-1 cursor-pointer accent-cyan-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-8 grid grid-cols-3 gap-4">
|
<div className="col-span-8 grid grid-cols-3 gap-4">
|
||||||
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
|
||||||
<span className="text-[10px] font-bold text-slate-400 oz-font-mono mb-2">UPWARD COMPRESS</span>
|
<MasteringKnob
|
||||||
<div className="knob-container" data-param="maxUpward" data-min="0" data-max="10" data-value="2.0" data-unit="dB">
|
param="maxUpward"
|
||||||
<div className="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-cyan-500 relative flex items-center justify-center">
|
min={0}
|
||||||
<div className="knob-pointer w-0.5 h-4 bg-cyan-400 rounded absolute top-1"></div>
|
max={10}
|
||||||
</div>
|
value={ozState.maxUpward}
|
||||||
</div>
|
unit="dB"
|
||||||
<span className="knob-val text-xs font-mono text-slate-200 mt-2">+2.0 dB</span>
|
label="UPWARD COMPRESS"
|
||||||
|
color="#22d3ee"
|
||||||
|
onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
|
||||||
<span className="text-[10px] font-bold text-slate-400 oz-font-mono mb-2">SOFT CLIPPER</span>
|
<MasteringKnob
|
||||||
<div className="knob-container" data-param="maxSoftClip" data-min="0" data-max="100" data-value="15" data-unit="%">
|
param="maxSoftClip"
|
||||||
<div className="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-amber-500 relative flex items-center justify-center">
|
min={0}
|
||||||
<div className="knob-pointer w-0.5 h-4 bg-amber-400 rounded absolute top-1"></div>
|
max={100}
|
||||||
</div>
|
value={ozState.maxSoftClip}
|
||||||
</div>
|
unit="%"
|
||||||
<span className="knob-val text-xs font-mono text-slate-200 mt-2">15%</span>
|
label="SOFT CLIPPER"
|
||||||
|
color="#fbbf24"
|
||||||
|
onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
<div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
|
||||||
<span className="text-[10px] font-bold text-slate-400 oz-font-mono mb-2">TRANSIENT EMPHASIS</span>
|
<MasteringKnob
|
||||||
<div className="knob-container" data-param="maxTransient" data-min="0" data-max="100" data-value="25" data-unit="%">
|
param="maxTransient"
|
||||||
<div className="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-emerald-500 relative flex items-center justify-center">
|
min={0}
|
||||||
<div className="knob-pointer w-0.5 h-4 bg-emerald-400 rounded absolute top-1"></div>
|
max={100}
|
||||||
</div>
|
value={ozState.maxTransient}
|
||||||
</div>
|
unit="%"
|
||||||
<span className="knob-val text-xs font-mono text-slate-200 mt-2">25%</span>
|
label="TRANSIENT EMPHASIS"
|
||||||
|
color="#34d399"
|
||||||
|
onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* WAVE OBSERVER INTEGRATION */}
|
||||||
|
<div className="border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between pb-1.5 border-b border-slate-800/80 mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[11px] font-bold text-white uppercase tracking-wider">Wave Observer</span>
|
||||||
|
<span className="text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/60 px-1.5 py-0.2 rounded font-mono">Real-time Oscilloscope</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['Scope', 'Settings', 'Help', 'About'].map(tab => (
|
||||||
|
<button key={tab} className={`px-2 py-0.5 rounded text-[10px] font-bold ${tab === 'Scope' ? 'bg-cyan-950 text-cyan-300 border border-cyan-800/60' : 'text-slate-400 hover:text-slate-200'}`}>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scope Canvas */}
|
||||||
|
<div className="relative w-full h-32 bg-[#090d16] border border-slate-800/80 rounded-lg overflow-hidden mb-2">
|
||||||
|
<canvas ref={woCanvasRef} className="w-full h-full block"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls bar */}
|
||||||
|
<div className="flex items-center justify-between gap-4 text-[10px] oz-font-mono text-slate-400 mt-1 select-none">
|
||||||
|
{/* Input level meters */}
|
||||||
|
<div className="flex items-center gap-2 bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800/80 shrink-0">
|
||||||
|
<span className="text-[10px] font-bold text-slate-300">Input</span>
|
||||||
|
<div className="flex flex-col gap-1 w-20">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[8px] text-slate-500 w-2">L</span>
|
||||||
|
<div className="w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center">
|
||||||
|
<div ref={woLeftMeterRef} className="h-full bg-cyan-400 transition-all duration-75" style={{ width: '0%' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[8px] text-slate-500 w-2">R</span>
|
||||||
|
<div className="w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center">
|
||||||
|
<div ref={woRightMeterRef} className="h-full bg-teal-500 transition-all duration-75" style={{ width: '0%' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scope Controls */}
|
||||||
|
<div className="flex items-center gap-4 bg-slate-950 px-3 py-1 rounded border border-slate-800/80 flex-1 justify-around">
|
||||||
|
<span className="font-bold text-slate-300 uppercase tracking-widest text-[9px]">Scope</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>Channel</span>
|
||||||
|
<select value={woChannel} onChange={e => setWoChannel(e.target.value)} className="bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none">
|
||||||
|
<option value="stereo">Stereo</option>
|
||||||
|
<option value="left">Left Only</option>
|
||||||
|
<option value="right">Right Only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>Mode</span>
|
||||||
|
<select value={woMode} onChange={e => setWoMode(e.target.value)} className="bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none">
|
||||||
|
<option value="waveform">Waveform</option>
|
||||||
|
<option value="envelope">Envelope</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>Duration:</span>
|
||||||
|
<span className="text-cyan-400 font-bold w-10 text-right">{woDuration.toFixed(3)}s</span>
|
||||||
|
<input type="range" min="0.5" max="5.0" step="0.1" value={woDuration} onChange={e => setWoDuration(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer accent-cyan-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>V.Zoom:</span>
|
||||||
|
<span className="text-cyan-400 font-bold w-12 text-right">{woZoom.toFixed(1)} dB</span>
|
||||||
|
<input type="range" min="-12" max="24" step="0.5" value={woZoom} onChange={e => setWoZoom(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer accent-cyan-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={() => setWoPaused(!woPaused)} className={`px-3 py-1 rounded font-bold border transition shadow-md shrink-0 flex items-center gap-1 ${woPaused ? 'bg-amber-800 hover:bg-amber-700 border-amber-500 text-amber-100' : 'bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-200'}`}>
|
||||||
|
{woPaused ? 'Resume' : 'Pause'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT SIDEBAR: I/O METERS */}
|
{/* RIGHT SIDEBAR: I/O METERS */}
|
||||||
@@ -7989,7 +8315,10 @@ const App = () => {
|
|||||||
var asCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(asTrk, allTracks) : (asTrk ? asTrk.midiChannel : 0);
|
var asCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(asTrk, allTracks) : (asTrk ? asTrk.midiChannel : 0);
|
||||||
var asProg = as.instrumentProgram;
|
var asProg = as.instrumentProgram;
|
||||||
var asSe = as.synth_engine;
|
var asSe = as.synth_engine;
|
||||||
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(as.trackId, scaledVel);
|
||||||
|
}
|
||||||
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Route to ALL armed tracks (not just the first one)
|
// Route to ALL armed tracks (not just the first one)
|
||||||
@@ -7998,7 +8327,10 @@ const App = () => {
|
|||||||
var atProg = at.instrumentProgram;
|
var atProg = at.instrumentProgram;
|
||||||
var atSe = at.synth_engine;
|
var atSe = at.synth_engine;
|
||||||
var atDest = activeTrackNodesRef.current[at.id]?.gainNode || null;
|
var atDest = activeTrackNodesRef.current[at.id]?.gainNode || null;
|
||||||
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(at.id, scaledVel);
|
||||||
|
}
|
||||||
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) {
|
} else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) {
|
||||||
@@ -8581,6 +8913,15 @@ const App = () => {
|
|||||||
sessionTabsRef.current = sessionTabs;
|
sessionTabsRef.current = sessionTabs;
|
||||||
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
|
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
|
||||||
|
|
||||||
|
const midiVuActivityRef = useRef({});
|
||||||
|
const triggerMidiVuActivity = (trackId, velocity) => {
|
||||||
|
if (!trackId) return;
|
||||||
|
const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||||
|
const peak = Math.min(1.0, Math.max(0.15, velFactor));
|
||||||
|
midiVuActivityRef.current[trackId] = peak;
|
||||||
|
};
|
||||||
|
window.triggerMidiVuActivity = triggerMidiVuActivity;
|
||||||
|
|
||||||
|
|
||||||
// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ──
|
// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ──
|
||||||
const [tempTabActive, setTempTabActive] = useState(false);
|
const [tempTabActive, setTempTabActive] = useState(false);
|
||||||
@@ -11715,6 +12056,10 @@ const App = () => {
|
|||||||
const midiItems = track.midiItems || [];
|
const midiItems = track.midiItems || [];
|
||||||
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
|
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
|
||||||
var trkCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(track, activeTracks) : (track.midiChannel !== undefined ? track.midiChannel : 0);
|
var trkCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(track, activeTracks) : (track.midiChannel !== undefined ? track.midiChannel : 0);
|
||||||
|
// Ensure instrument is loaded in FluidSynth
|
||||||
|
if (track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
|
||||||
|
window.SonicSF.selectInstrument(trkCh, track.synth_engine.soundfont_bank || 0, track.synth_engine.soundfont_program || 0, track.synth_engine.soundfont_id);
|
||||||
|
}
|
||||||
const bpmVal = parseInt(bpm) || 120;
|
const bpmVal = parseInt(bpm) || 120;
|
||||||
const secondsPerBeat = 60.0 / bpmVal;
|
const secondsPerBeat = 60.0 / bpmVal;
|
||||||
midiItems.forEach(item => {
|
midiItems.forEach(item => {
|
||||||
@@ -11742,6 +12087,12 @@ const App = () => {
|
|||||||
trkCh,
|
trkCh,
|
||||||
track.synth_engine
|
track.synth_engine
|
||||||
);
|
);
|
||||||
|
// Trigger VU meter flash when the note starts playing
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
|
||||||
|
}
|
||||||
|
}, delay * 1000);
|
||||||
} else {
|
} else {
|
||||||
const playOffset = offsetTime - noteStartSec;
|
const playOffset = offsetTime - noteStartSec;
|
||||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||||
@@ -11755,6 +12106,10 @@ const App = () => {
|
|||||||
trkCh,
|
trkCh,
|
||||||
track.synth_engine
|
track.synth_engine
|
||||||
);
|
);
|
||||||
|
// Trigger VU meter flash instantly
|
||||||
|
if (window.triggerMidiVuActivity) {
|
||||||
|
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -16525,17 +16880,29 @@ const App = () => {
|
|||||||
const canvas = trackVuRefs.current[key];
|
const canvas = trackVuRefs.current[key];
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
|
let audioPeak = 0;
|
||||||
if (node && node.analyserNode && isPlaying) {
|
if (node && node.analyserNode && isPlaying) {
|
||||||
const analyser = node.analyserNode;
|
const analyser = node.analyserNode;
|
||||||
const data = new Uint8Array(128);
|
const data = new Uint8Array(128);
|
||||||
analyser.getByteTimeDomainData(data);
|
analyser.getByteTimeDomainData(data);
|
||||||
let peak = 0;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const v = Math.abs(data[i] - 128) / 128;
|
const v = Math.abs(data[i] - 128) / 128;
|
||||||
if (v > peak) peak = v;
|
if (v > audioPeak) audioPeak = v;
|
||||||
}
|
}
|
||||||
const db = peak > 0 ? 20 * Math.log10(peak) : -60;
|
}
|
||||||
|
|
||||||
|
let midiPeak = midiVuActivityRef.current[trackId] || 0;
|
||||||
|
if (midiPeak > 0) {
|
||||||
|
midiVuActivityRef.current[trackId] = midiPeak * 0.90;
|
||||||
|
if (midiVuActivityRef.current[trackId] < 0.01) {
|
||||||
|
midiVuActivityRef.current[trackId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const peak = Math.max(audioPeak, midiPeak);
|
||||||
|
const db = peak > 0 ? 20 * Math.log10(peak) : -60;
|
||||||
|
|
||||||
|
if (peak > 0.001) {
|
||||||
if (key.endsWith('_mixer')) {
|
if (key.endsWith('_mixer')) {
|
||||||
drawMixerVuMeter(canvas, peak);
|
drawMixerVuMeter(canvas, peak);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"ai_configs": {
|
||||||
|
"anonymous": [
|
||||||
|
{
|
||||||
|
"id": "openai_default",
|
||||||
|
"name": "OpenAI Official",
|
||||||
|
"provider_type": "openai",
|
||||||
|
"api_base_url": "https://api.openai.com/v1",
|
||||||
|
"api_key": "test-sk-key-123",
|
||||||
|
"model_name": "gpt-4o",
|
||||||
|
"temperature": 0.7,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "openai_compat_default",
|
||||||
|
"name": "OpenAI Compatible (Ollama/LocalAI/DeepSeek)",
|
||||||
|
"provider_type": "openai_compatible",
|
||||||
|
"api_base_url": "http://localhost:11434/v1",
|
||||||
|
"api_key": "ollama",
|
||||||
|
"model_name": "deepseek-r1",
|
||||||
|
"temperature": 0.7,
|
||||||
|
"is_active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "anthropic_default",
|
||||||
|
"name": "Anthropic Claude",
|
||||||
|
"provider_type": "anthropic",
|
||||||
|
"api_base_url": "https://api.anthropic.com/v1",
|
||||||
|
"api_key": "",
|
||||||
|
"model_name": "claude-3-5-sonnet",
|
||||||
|
"temperature": 0.7,
|
||||||
|
"is_active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gemini_default",
|
||||||
|
"name": "Google Gemini",
|
||||||
|
"provider_type": "gemini",
|
||||||
|
"api_base_url": "https://generativelanguage.googleapis.com",
|
||||||
|
"api_key": "",
|
||||||
|
"model_name": "gemini-1.5-pro",
|
||||||
|
"temperature": 0.7,
|
||||||
|
"is_active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preferences": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user