v15b: fix CC7 to VSTi (legacy tag kHostMidiCC+cc + volume-param fallback), CC lane grid + 0/+- labels
This commit is contained in:
+64
-9
@@ -9643,6 +9643,44 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// ponytail: chi khoi phuc khi project cu co curve; chua hop nhat migration
|
||||
}, [notes]);
|
||||
|
||||
// CC lane grid: vẽ cột beat/bar GIỐNG HỆT piano roll (cùng pixelsPerBeat +
|
||||
// snapBeats + timeSigNum) → không lệch khi zoom. snapBeats copy nguyên từ
|
||||
// notes canvas để hai lane trùng khít.
|
||||
const paintCCLaneGrid = (ctx, h) => {
|
||||
let snapBeats = 0.25;
|
||||
if (snapValue === '1') snapBeats = 1.0;
|
||||
else if (snapValue === '1/2') snapBeats = 0.5;
|
||||
else if (snapValue === '1/4') snapBeats = 0.25;
|
||||
else if (snapValue === '1/8') snapBeats = 0.125;
|
||||
else if (snapValue === '1/16') snapBeats = 0.0625;
|
||||
else if (snapValue === '4') snapBeats = 4.0;
|
||||
else if (snapValue === '1/32') snapBeats = 0.03125;
|
||||
for (let beat = 0; beat <= viewBeats; beat += snapBeats) {
|
||||
const x = beat * pixelsPerBeat;
|
||||
if (x > viewWidth) break;
|
||||
const isBar = beat % timeSigNum === 0;
|
||||
ctx.strokeStyle = isBar ? '#444450' : '#2d2d35';
|
||||
ctx.lineWidth = isBar ? 1.2 : 0.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
// Nhan 0/+-: CC 0=min (velocity, expression...) ghi "0" o day; CC luong cuc
|
||||
// (pan) ghi "+" dinh, "0" giua, "-" day.
|
||||
const paintCCLaneZeroLabel = (ctx, h, bipolar) => {
|
||||
ctx.fillStyle = '#8b8b95';
|
||||
ctx.font = '10px sans-serif';
|
||||
if (bipolar) {
|
||||
ctx.fillText('+', 5, 12);
|
||||
ctx.fillText('0', 5, h / 2 + 3);
|
||||
ctx.fillText('-', 5, h - 5);
|
||||
} else {
|
||||
ctx.fillText('0', 5, h - 5);
|
||||
}
|
||||
};
|
||||
|
||||
// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep
|
||||
// (truoc day setNotes tung mousemove + doc notes tu closure stale -> node
|
||||
// cu/moi xen ke -> flicker). previewNoteId != null: chi ve note do voi
|
||||
@@ -9658,12 +9696,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.fillStyle = '#161616';
|
||||
ctx.fillRect(0, 0, viewWidth, h);
|
||||
ctx.strokeStyle = '#252525';
|
||||
paintCCLaneGrid(ctx, h);
|
||||
// CC11 0=min → ghi "0" o day (khong ve duong giua nua).
|
||||
ctx.strokeStyle = '#2f2f38';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h / 2);
|
||||
ctx.lineTo(viewWidth, h / 2);
|
||||
ctx.moveTo(0, h - 10);
|
||||
ctx.lineTo(viewWidth, h - 10);
|
||||
ctx.stroke();
|
||||
paintCCLaneZeroLabel(ctx, h, false);
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
if (notes.length === 0) return;
|
||||
let minS = Infinity, maxE = -Infinity;
|
||||
@@ -9719,13 +9760,27 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
|
||||
ctx.fillStyle = '#161616';
|
||||
ctx.fillRect(0, 0, viewWidth, h);
|
||||
paintCCLaneGrid(ctx, h);
|
||||
|
||||
ctx.strokeStyle = '#252525';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h / 2);
|
||||
ctx.lineTo(viewWidth, h / 2);
|
||||
ctx.stroke();
|
||||
if (ccMode === 'pan') {
|
||||
// Luong cuc: 0 o giua (+ dinh / - day) — giu nguyen nhu cu.
|
||||
ctx.strokeStyle = '#252525';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h / 2);
|
||||
ctx.lineTo(viewWidth, h / 2);
|
||||
ctx.stroke();
|
||||
paintCCLaneZeroLabel(ctx, h, true);
|
||||
} else {
|
||||
// 0=min: ve duong day tai gia tri 0 + nhan "0".
|
||||
ctx.strokeStyle = '#2f2f38';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h - 10);
|
||||
ctx.lineTo(viewWidth, h - 10);
|
||||
ctx.stroke();
|
||||
paintCCLaneZeroLabel(ctx, h, false);
|
||||
}
|
||||
|
||||
if (ccMode === 'cc11') {
|
||||
// CC11 expression: graph thread cho note dang chon; khi dang drag ve
|
||||
|
||||
@@ -745,12 +745,20 @@ const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.u
|
||||
// sinh lai per-note expression_curve (engine doc per-note) bang cach sample
|
||||
// global tai vung tung note.
|
||||
const[cc11GlobalCurve,setCc11GlobalCurve]=React.useState([]);React.useEffect(()=>{if(cc11GlobalCurve&&cc11GlobalCurve.length)return;const n=notes.find(x=>Array.isArray(x.expression_curve)&&x.expression_curve.length);if(n)setCc11GlobalCurve(n.expression_curve.slice());// ponytail: chi khoi phuc khi project cu co curve; chua hop nhat migration
|
||||
},[notes]);// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep
|
||||
},[notes]);// CC lane grid: vẽ cột beat/bar GIỐNG HỆT piano roll (cùng pixelsPerBeat +
|
||||
// snapBeats + timeSigNum) → không lệch khi zoom. snapBeats copy nguyên từ
|
||||
// notes canvas để hai lane trùng khít.
|
||||
const paintCCLaneGrid=(ctx,h)=>{let snapBeats=0.25;if(snapValue==='1')snapBeats=1.0;else if(snapValue==='1/2')snapBeats=0.5;else if(snapValue==='1/4')snapBeats=0.25;else if(snapValue==='1/8')snapBeats=0.125;else if(snapValue==='1/16')snapBeats=0.0625;else if(snapValue==='4')snapBeats=4.0;else if(snapValue==='1/32')snapBeats=0.03125;for(let beat=0;beat<=viewBeats;beat+=snapBeats){const x=beat*pixelsPerBeat;if(x>viewWidth)break;const isBar=beat%timeSigNum===0;ctx.strokeStyle=isBar?'#444450':'#2d2d35';ctx.lineWidth=isBar?1.2:0.6;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}};// Nhan 0/+-: CC 0=min (velocity, expression...) ghi "0" o day; CC luong cuc
|
||||
// (pan) ghi "+" dinh, "0" giua, "-" day.
|
||||
const paintCCLaneZeroLabel=(ctx,h,bipolar)=>{ctx.fillStyle='#8b8b95';ctx.font='10px sans-serif';if(bipolar){ctx.fillText('+',5,12);ctx.fillText('0',5,h/2+3);ctx.fillText('-',5,h-5);}else{ctx.fillText('0',5,h-5);}};// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep
|
||||
// (truoc day setNotes tung mousemove + doc notes tu closure stale -> node
|
||||
// cu/moi xen ke -> flicker). previewNoteId != null: chi ve note do voi
|
||||
// previewCurve thay cho expression_curve da commit.
|
||||
const paintCC11Lane=previewCurve=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();const yOf=v=>h-(v/127*(h-20)+10);if(notes.length===0)return;let minS=Infinity,maxE=-Infinity;notes.forEach(n=>{minS=Math.min(minS,n.start_beat);maxE=Math.max(maxE,n.start_beat+(n.duration_beats||1));});const x0=(renderBeatOffset+minS)*pixelsPerBeat;const x1=(renderBeatOffset+maxE)*pixelsPerBeat;// Dashed lien tuc tu note dau den note cuoi (default line)
|
||||
ctx.strokeStyle='#2f6b57';ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(x0,yOf(127));ctx.lineTo(x1,yOf(127));ctx.stroke();ctx.setLineDash([]);const curve=(previewCurve||cc11GlobalCurve||[]).slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(!curve.length)return;ctx.strokeStyle='#34d399';ctx.lineWidth=2;ctx.beginPath();if(curve.length===1){ctx.moveTo(x0,yOf(curve[0].value));ctx.lineTo(x1,yOf(curve[0].value));}else{curve.forEach((p,i)=>{const x=x0+p.position_ratio*(x1-x0);const y=yOf(p.value);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});}ctx.stroke();curve.forEach(p=>{const x=x0+p.position_ratio*(x1-x0);ctx.fillStyle='#a7f3d0';ctx.beginPath();ctx.arc(x,yOf(p.value),4,0,2*Math.PI);ctx.fill();});};React.useLayoutEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();if(ccMode==='cc11'){// CC11 expression: graph thread cho note dang chon; khi dang drag ve
|
||||
const paintCC11Lane=previewCurve=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);paintCCLaneGrid(ctx,h);// CC11 0=min → ghi "0" o day (khong ve duong giua nua).
|
||||
ctx.strokeStyle='#2f2f38';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h-10);ctx.lineTo(viewWidth,h-10);ctx.stroke();paintCCLaneZeroLabel(ctx,h,false);const yOf=v=>h-(v/127*(h-20)+10);if(notes.length===0)return;let minS=Infinity,maxE=-Infinity;notes.forEach(n=>{minS=Math.min(minS,n.start_beat);maxE=Math.max(maxE,n.start_beat+(n.duration_beats||1));});const x0=(renderBeatOffset+minS)*pixelsPerBeat;const x1=(renderBeatOffset+maxE)*pixelsPerBeat;// Dashed lien tuc tu note dau den note cuoi (default line)
|
||||
ctx.strokeStyle='#2f6b57';ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(x0,yOf(127));ctx.lineTo(x1,yOf(127));ctx.stroke();ctx.setLineDash([]);const curve=(previewCurve||cc11GlobalCurve||[]).slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(!curve.length)return;ctx.strokeStyle='#34d399';ctx.lineWidth=2;ctx.beginPath();if(curve.length===1){ctx.moveTo(x0,yOf(curve[0].value));ctx.lineTo(x1,yOf(curve[0].value));}else{curve.forEach((p,i)=>{const x=x0+p.position_ratio*(x1-x0);const y=yOf(p.value);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});}ctx.stroke();curve.forEach(p=>{const x=x0+p.position_ratio*(x1-x0);ctx.fillStyle='#a7f3d0';ctx.beginPath();ctx.arc(x,yOf(p.value),4,0,2*Math.PI);ctx.fill();});};React.useLayoutEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);paintCCLaneGrid(ctx,h);if(ccMode==='pan'){// Luong cuc: 0 o giua (+ dinh / - day) — giu nguyen nhu cu.
|
||||
ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();paintCCLaneZeroLabel(ctx,h,true);}else{// 0=min: ve duong day tai gia tri 0 + nhan "0".
|
||||
ctx.strokeStyle='#2f2f38';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h-10);ctx.lineTo(viewWidth,h-10);ctx.stroke();paintCCLaneZeroLabel(ctx,h,false);}if(ccMode==='cc11'){// CC11 expression: graph thread cho note dang chon; khi dang drag ve
|
||||
// preview tu ccDragRef (khong phu thuoc state da commit).
|
||||
const drag=ccDragRef.current;if(drag&&drag.mode==='cc11'&&drag.previewCurve){paintCC11Lane(drag.previewCurve);}else{paintCC11Lane();}return;}else{notes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);let val=note.velocity!==undefined?note.velocity:0.8;if(ccMode==='pan'){val=(note.pan!==undefined?note.pan:0.0)*0.5+0.5;}const stemH=val*(h-20)+10;const y=h-stemH;ctx.strokeStyle=ccMode==='pan'?isSelected?'#60a5fa':'#a78bfa':isSelected?'#3b82f6':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?isSelected?'#3b82f6':'#c084fc':isSelected?'#3b82f6':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});}},[notes,ccMode,rollZoom,viewWidth,selectedNoteIds,renderBeatOffset,ccHeight,showCC]);// Follow playhead: khi PLAY — playhead luôn ở GIỮA view, notes trôi sang
|
||||
// trái (scroll theo st.currentTime); khi STOP — scroll về đầu (playhead ở
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608291100" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608291900" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
@@ -76,6 +76,8 @@ using Steinberg::Vst::ParamID;
|
||||
using Steinberg::Vst::ParamValue;
|
||||
using Steinberg::Vst::IMidiMapping;
|
||||
using Steinberg::Vst::IParamValueQueue;
|
||||
using Steinberg::Vst::ParameterInfo;
|
||||
using Steinberg::Vst::TChar;
|
||||
using Steinberg::Vst::kNoParamId;
|
||||
using Steinberg::Vst::BusInfo;
|
||||
using Steinberg::Vst::kAudio;
|
||||
@@ -154,6 +156,11 @@ struct Vst3HostState {
|
||||
// every overlapping voice gets a unique id and note-off releases them all.
|
||||
int32 nextNoteId = 1;
|
||||
std::vector<int32> noteIdsByPitch[128];
|
||||
// CC → plugin param tag cache for the no-IMidiMapping volume fallback
|
||||
// (scanVolumeParam). 0 = unresolved; kNoParamId = no matching param.
|
||||
bool ccParamResolved[128] = {};
|
||||
bool ccLogged[128] = {};
|
||||
ParamID ccParamTag[128] = {};
|
||||
};
|
||||
|
||||
// Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the
|
||||
@@ -173,6 +180,39 @@ ParamID midiControllerTag(IPtr<IEditController>& controller, int16 channel,
|
||||
return legacyTag;
|
||||
}
|
||||
|
||||
// Case-insensitive substring match of an ASCII token on a UTF-16 SDK string.
|
||||
bool titleContains(const TChar* s, const char* token) {
|
||||
for (int i = 0; i < 127 && s[i] != 0; ++i) {
|
||||
int j = 0;
|
||||
for (; token[j]; ++j) {
|
||||
if (i + j >= 127 || s[i + j] == 0) break;
|
||||
char c = (char)(s[i + j] & 0x7F);
|
||||
if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
||||
if (c != token[j]) break;
|
||||
}
|
||||
if (token[j] == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// No-IMidiMapping fallback: scan the controller's parameter list for the
|
||||
// first volume/gain/output/level/master param (priority order) and use its
|
||||
// tag for CC 7/11. Plugins without mapping ignore the legacy kHostMidiCC
|
||||
// tags, so CC volume curves die there without this scan.
|
||||
ParamID scanVolumeParam(IPtr<IEditController>& controller) {
|
||||
if (!controller) return kNoParamId;
|
||||
static const char* tokens[] = {"volume", "gain", "output", "level", "master"};
|
||||
int32 count = controller->getParameterCount();
|
||||
for (int32 i = 0; i < count; ++i) {
|
||||
ParameterInfo info = {};
|
||||
if (controller->getParameterInfo(i, info) != kResultOk) continue;
|
||||
for (const char* t : tokens) {
|
||||
if (titleContains(info.title, t)) return info.id;
|
||||
}
|
||||
}
|
||||
return kNoParamId;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
@@ -474,7 +514,22 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = midiControllerTag(s->controller, 0, (int16)cc,
|
||||
kHostMidiCC);
|
||||
kHostMidiCC + cc);
|
||||
// IMidiMapping absent (common): plugin cannot resolve kHostMidiCC+cc, so
|
||||
// fall back to a scanned volume param (resolved once per CC).
|
||||
if (cc < 128 && tag == kHostMidiCC + cc && !s->ccParamResolved[cc]) {
|
||||
s->ccParamResolved[cc] = true;
|
||||
s->ccParamTag[cc] = scanVolumeParam(s->controller);
|
||||
std::cerr << "[midi] cc=" << cc << " no IMidiMapping → volume-param tag=0x"
|
||||
<< std::hex << s->ccParamTag[cc] << std::dec << std::endl;
|
||||
}
|
||||
if (cc < 128 && tag == kHostMidiCC + cc) tag = s->ccParamTag[cc];
|
||||
if (cc < 128 && !s->ccLogged[cc]) {
|
||||
s->ccLogged[cc] = true;
|
||||
std::cerr << "[midi] cc=" << cc << " tag=0x" << std::hex << tag
|
||||
<< std::dec << " v=" << value << std::endl;
|
||||
}
|
||||
if (tag == kNoParamId) return;
|
||||
ParamValue v = value / 127.0;
|
||||
// setParamNormalized covers single-component plugins; the param queue
|
||||
// reaches split plugins that read inputParameterChanges inside process().
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user