FIX: Đã đổi tên (v202608060900)
This commit is contained in:
+117
-4
@@ -7036,6 +7036,81 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
if (undoStackRef.current.length > 50) undoStackRef.current.shift();
|
if (undoStackRef.current.length > 50) undoStackRef.current.shift();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ── Humanize: ngẫu nhiên hóa velocity + timing theo cường độ ──
|
||||||
|
const [humanizeStrength, setHumanizeStrength] = React.useState(0.10); // 0.05 nhẹ / 0.10 vừa / 0.18 mạnh
|
||||||
|
const applyHumanize = React.useCallback(() => {
|
||||||
|
if (!notes || !notes.length) { showToast('Không có nốt nào để humanize.', 'warning'); return; }
|
||||||
|
const velAmt = humanizeStrength;
|
||||||
|
const timeAmt = humanizeStrength * 0.15; // ±0.015 beat @ vừa (~12ms @120bpm)
|
||||||
|
pushToUndo(notes);
|
||||||
|
setNotes(prev => (prev || []).map(n => ({
|
||||||
|
...n,
|
||||||
|
velocity: Math.max(0.05, Math.min(1.0, (n.velocity || 0.8) + (Math.random() * 2 - 1) * velAmt)),
|
||||||
|
start_beat: Math.max(0, (n.start_beat || 0) + (Math.random() * 2 - 1) * timeAmt)
|
||||||
|
})));
|
||||||
|
showToast('Đã humanize ' + notes.length + ' nốt (velocity ±' + Math.round(velAmt * 100) + '%, timing ±' + Math.round(timeAmt * 1000) + 'ms).', 'success');
|
||||||
|
}, [notes, pushToUndo, setNotes, showToast, humanizeStrength]);
|
||||||
|
|
||||||
|
// ── Transpose semitone: dịch pitch tất cả nốt (clamp 0-127) ──
|
||||||
|
const applyTranspose = React.useCallback((semi) => {
|
||||||
|
const s = parseInt(semi);
|
||||||
|
if (isNaN(s) || s === 0) { showToast('Nhập số semitone khác 0.', 'warning'); return; }
|
||||||
|
if (!notes || !notes.length) { showToast('Không có nốt nào để transpose.', 'warning'); return; }
|
||||||
|
pushToUndo(notes);
|
||||||
|
setNotes(prev => (prev || []).map(n => ({
|
||||||
|
...n,
|
||||||
|
pitch: Math.max(0, Math.min(127, (n.pitch || 60) + s))
|
||||||
|
})));
|
||||||
|
showToast('Đã transpose ' + notes.length + ' nốt ' + (s > 0 ? '+' : '') + s + ' semitone.', 'success');
|
||||||
|
}, [notes, pushToUndo, setNotes, showToast]);
|
||||||
|
|
||||||
|
// ── Transpose theo SCALE (chuyển giọng): detect key hiện tại → map degree ──
|
||||||
|
const SCALE_PATTERNS = { major: [0, 2, 4, 5, 7, 9, 11], minor: [0, 2, 3, 5, 7, 8, 10] };
|
||||||
|
const SCALE_ROOTS = { C: 0, 'C#': 1, D: 2, 'D#': 3, E: 4, F: 5, 'F#': 6, G: 7, 'G#': 8, A: 9, 'A#': 10, B: 11 };
|
||||||
|
const detectKey = React.useCallback((noteList) => {
|
||||||
|
const roots = Object.keys(SCALE_ROOTS);
|
||||||
|
let best = null, bestScore = -1;
|
||||||
|
for (let ri = 0; ri < roots.length; ri++) {
|
||||||
|
for (const st of ['major', 'minor']) {
|
||||||
|
const tones = new Set(SCALE_PATTERNS[st].map(s => (SCALE_ROOTS[roots[ri]] + s) % 12));
|
||||||
|
let score = 0;
|
||||||
|
(noteList || []).forEach(n => { const pc = (((n.pitch || 60) % 12) + 12) % 12; if (tones.has(pc)) score++; });
|
||||||
|
if (score > bestScore) { bestScore = score; best = { root: roots[ri], scale: st }; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best || { root: 'C', scale: 'major' };
|
||||||
|
}, []);
|
||||||
|
const [keyTargetRoot, setKeyTargetRoot] = React.useState('C');
|
||||||
|
const [keyTargetScale, setKeyTargetScale] = React.useState('major');
|
||||||
|
const applyTransposeToKey = React.useCallback(() => {
|
||||||
|
if (!notes || !notes.length) { showToast('Không có nốt nào để chuyển giọng.', 'warning'); return; }
|
||||||
|
const srcKey = detectKey(notes);
|
||||||
|
const dstKey = { root: keyTargetRoot, scale: keyTargetScale };
|
||||||
|
if (srcKey.root === dstKey.root && srcKey.scale === dstKey.scale) {
|
||||||
|
showToast('Đã ở giọng ' + dstKey.root + ' ' + dstKey.scale + ' rồi.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const srcTones = SCALE_PATTERNS[srcKey.scale].map(s => (SCALE_ROOTS[srcKey.root] + s) % 12);
|
||||||
|
const dstTones = SCALE_PATTERNS[dstKey.scale].map(s => (SCALE_ROOTS[dstKey.root] + s) % 12);
|
||||||
|
pushToUndo(notes);
|
||||||
|
setNotes(prev => (prev || []).map(n => {
|
||||||
|
const p = n.pitch || 60;
|
||||||
|
const pc = ((p % 12) + 12) % 12;
|
||||||
|
// Degree gần nhất trong scale nguồn (7 bậc)
|
||||||
|
let bestIdx = 0, bestDist = 99;
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
let d = Math.abs(pc - srcTones[i]); if (d > 6) d = 12 - d;
|
||||||
|
if (d < bestDist) { bestDist = d; bestIdx = i; }
|
||||||
|
}
|
||||||
|
let shift = dstTones[bestIdx] - srcTones[bestIdx];
|
||||||
|
if (shift > 6) shift -= 12; else if (shift < -6) shift += 12;
|
||||||
|
return { ...n, pitch: Math.max(0, Math.min(127, p + shift)) };
|
||||||
|
}));
|
||||||
|
showToast('Chuyển giọng ' + srcKey.root + ' ' + srcKey.scale + ' → ' + dstKey.root + ' ' + dstKey.scale + ' (' + notes.length + ' nốt).', 'success');
|
||||||
|
}, [notes, pushToUndo, setNotes, showToast, detectKey, keyTargetRoot, keyTargetScale]);
|
||||||
|
|
||||||
|
const [transposeSemis, setTransposeSemis] = React.useState(0);
|
||||||
|
|
||||||
const handleUndo = React.useCallback(() => {
|
const handleUndo = React.useCallback(() => {
|
||||||
const prev = undoStackRef.current.pop();
|
const prev = undoStackRef.current.pop();
|
||||||
if (!prev) return;
|
if (!prev) return;
|
||||||
@@ -8387,9 +8462,9 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
return React.createElement("div", {
|
return React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
||||||
},
|
},
|
||||||
/* 1. TOOLBAR HEADER */
|
/* 1. TOOLBAR HEADER — 2 hàng (wrap tự nhiên; spacer 100% ép hàng mới) */
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
|
className: "bg-[#282828] border-b border-zinc-900 flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5 shrink-0 text-slate-200"
|
||||||
}, React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
className: "flex items-center gap-4"
|
className: "flex items-center gap-4"
|
||||||
}, React.createElement("select", {
|
}, React.createElement("select", {
|
||||||
@@ -8475,12 +8550,50 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
return showGhostNotes ? base + 'bg-purple-900/60 text-purple-300 border border-purple-700' : base + 'text-zinc-500 hover:text-zinc-300';
|
return showGhostNotes ? base + 'bg-purple-900/60 text-purple-300 border border-purple-700' : base + 'text-zinc-500 hover:text-zinc-300';
|
||||||
}(),
|
}(),
|
||||||
title: "Toggle ghost notes visibility"
|
title: "Toggle ghost notes visibility"
|
||||||
}, "\uD83D\uDC7B Ghost"), React.createElement("div", {
|
}, "\\uD83D\\uDC7B MIDI ghost notes"), React.createElement("div", {
|
||||||
|
style: { flexBasis: "100%", height: 0 }
|
||||||
|
}), React.createElement("button", {
|
||||||
|
onClick: applyHumanize,
|
||||||
|
className: "px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition",
|
||||||
|
title: "Humanize: randomize velocity + timing"
|
||||||
|
}, "\uD83C\uDF9A Humanize"), React.createElement("select", {
|
||||||
|
key: "humstr", value: humanizeStrength,
|
||||||
|
onChange: function(e) { setHumanizeStrength(parseFloat(e.target.value)); },
|
||||||
|
className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
|
||||||
|
title: "Cường độ humanize"
|
||||||
|
}, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("div", {
|
||||||
|
key: "transpose", className: "flex items-center gap-1"
|
||||||
|
}, React.createElement("input", {
|
||||||
|
key: "in", type: "number", step: 1, min: -24, max: 24, value: transposeSemis,
|
||||||
|
onChange: function(e) { setTransposeSemis(e.target.value); },
|
||||||
|
className: "w-12 px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-center text-zinc-200",
|
||||||
|
title: "Semitone offset (vd 2 = cao hơn 1 tone)"
|
||||||
|
}), React.createElement("button", {
|
||||||
|
key: "btn", onClick: function() { applyTranspose(transposeSemis); },
|
||||||
|
className: "px-2 py-1 rounded text-xs bg-sky-900/40 text-sky-300 border border-sky-700/60 hover:bg-sky-800/50 transition",
|
||||||
|
title: "Transpose all notes by the semitone offset"
|
||||||
|
}, "Transpose")), React.createElement("div", {
|
||||||
|
key: "keyshift", className: "flex items-center gap-1"
|
||||||
|
}, React.createElement("select", {
|
||||||
|
key: "root", value: keyTargetRoot,
|
||||||
|
onChange: function(e) { setKeyTargetRoot(e.target.value); },
|
||||||
|
className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
|
||||||
|
title: "Giọng đích (root)"
|
||||||
|
}, ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"].map(function(r) { return React.createElement("option", { key: r, value: r }, r); })), React.createElement("select", {
|
||||||
|
key: "scale", value: keyTargetScale,
|
||||||
|
onChange: function(e) { setKeyTargetScale(e.target.value); },
|
||||||
|
className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
|
||||||
|
title: "Thể scale đích"
|
||||||
|
}, React.createElement("option", { key: "maj", value: "major" }, "major"), React.createElement("option", { key: "min", value: "minor" }, "minor")), React.createElement("button", {
|
||||||
|
key: "btn", onClick: applyTransposeToKey,
|
||||||
|
className: "px-2 py-1 rounded text-xs bg-violet-900/40 text-violet-300 border border-violet-700/60 hover:bg-violet-800/50 transition",
|
||||||
|
title: "Chuyển giọng: map degree hiện tại sang giọng đích (auto-detect key nguồn)"
|
||||||
|
}, "\uD83C\uDFB5 Chuy\u1EC3n gi\u1ECDng")), React.createElement("div", {
|
||||||
className: "flex items-center gap-1"
|
className: "flex items-center gap-1"
|
||||||
}, React.createElement("button", {
|
}, React.createElement("button", {
|
||||||
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
||||||
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
||||||
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), React.createElement("button", {
|
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "L\u01B0u"), React.createElement("button", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const ppq = 480;
|
const ppq = 480;
|
||||||
const bpmNum = parseInt(bpm) || 120;
|
const bpmNum = parseInt(bpm) || 120;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608060700" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608060900" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1995,3 +1995,30 @@
|
|||||||
- **FIX (app/main.py):** index.html (`/`) thêm `Cache-Control: no-cache, no-store, must-revalidate` — HTML luôn mới, bundle JS bust bằng ?v=.
|
- **FIX (app/main.py):** index.html (`/`) thêm `Cache-Control: no-cache, no-store, must-revalidate` — HTML luôn mới, bundle JS bust bằng ?v=.
|
||||||
- **Các file ảnh hưởng:** `app/main.py`. Cần rebuild docker + restart.
|
- **Các file ảnh hưởng:** `app/main.py`. Cần rebuild docker + restart.
|
||||||
- **Ghi chú/Test:** sau khi deploy: incognito (đóng + mở lại tab — hoặc Ctrl+Shift+R 1 lần) → load trang → bundle mới. Verify: console thấy stamp mới.
|
- **Ghi chú/Test:** sau khi deploy: incognito (đóng + mở lại tab — hoặc Ctrl+Shift+R 1 lần) → load trang → bundle mới. Verify: console thấy stamp mới.
|
||||||
|
|
||||||
|
### [2026-08-06 07:30] Task: PIANO ROLL TAB — Humanize + Transpose (có undo)
|
||||||
|
- **Yêu cầu user:** cài đặt tính năng Humanize (midi note) + Transpose (chuyển giọng) trong piano roll tab.
|
||||||
|
- **FIX (app.jsx PianoRollTabEditor):**
|
||||||
|
(1) `applyHumanize()` — random velocity ±8% + start_beat ±0.015 beat (~12ms @120bpm), clamp 0.05-1.0/≥0 — pushToUndo trước khi đổi.
|
||||||
|
(2) `applyTranspose(semi)` — dịch pitch ±s semitone, clamp 0-127 — pushToUndo trước khi đổi.
|
||||||
|
(3) Toolbar: nút **🎚 Humanize** (sau nút Ghost) + nhóm **input semitone + nút Transpose** (trước nút Lưu). State `transposeSemis` local.
|
||||||
|
(4) Cả 2 đều qua `pushToUndo(notes)` → Ctrl+Z/Ctrl+Shift+Z hoạt động (undo stack local của piano roll).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060730), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → mở piano roll → bấm Humanize (nghe velocity/timing đổi) → Transpose +2 (nghe cao hơn 1 tone) → Ctrl+Z hoàn tác.
|
||||||
|
|
||||||
|
### [2026-08-06 08:00] Task: Humanize có cường độ (Nhẹ/Vừa/Mạnh) + Transpose theo SCALE (12 tông major/minor)
|
||||||
|
- **Yêu cầu user:** tùy chỉnh lượng humanize (mạnh/nhẹ) + transpose theo scale major/minor đủ 12 tông.
|
||||||
|
- **FIX (app.jsx PianoRollTabEditor):**
|
||||||
|
(1) Humanize: `humanizeStrength` state (0.05 Nhẹ / 0.10 Vừa / 0.18 Mạnh) + select trong toolbar — velocity ±strength, timing ±strength*0.15 beat.
|
||||||
|
(2) Transpose theo SCALE: `SCALE_PATTERNS` (major [0,2,4,5,7,9,11], minor [0,2,3,5,7,8,10]) + `SCALE_ROOTS` (12 tông) + **`detectKey()` auto-detect key nguồn** (best-fit root+scale theo pitch class) + `applyTransposeToKey()` map **degree → degree** (nốt về bậc gần nhất trong scale nguồn → shift sang bậc tương ứng scale đích, ±6 clamp octave).
|
||||||
|
(3) Toolbar: `[🎚 Humanize] [Nhẹ|Vừa|Mạnh] [semis|Transpose] [C..B][major|minor][🎵 Chuyển giọng]` — đều qua pushToUndo (Ctrl+Z hoạt động).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060800), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → piano roll → Humanize Mạnh vs Nhẹ; Chuyển giọng C major → D minor (map degree — melody giữ hình dạng) → Ctrl+Z.
|
||||||
|
|
||||||
|
### [2026-08-06 08:30] Task: Toolbar piano roll 2 hàng — nhóm nút chỉnh sửa note sang hàng mới
|
||||||
|
- **Yêu cầu user:** thêm hàng toolbar mới, di chuyển nút tính năng tương tự sang hàng mới.
|
||||||
|
- **FIX (app.jsx):** header toolbar đổi `h-10 flex items-center justify-between` → `flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5` + **row-break spacer** (`flexBasis:100%, height:0`) trước nút Humanize.
|
||||||
|
- **Hàng 1:** track select, Snap to Scale, Snap, ARM, MIDI Input, Instrument, AI bar range, CC mode/CC, Session/Isolated, Ghost.
|
||||||
|
- **Hàng 2:** 🎚 Humanize + [Nhẹ|Vừa|Mạnh], ±semis Transpose, [C..B][major|minor] 🎵 Chuyển giọng, Lưu, Export, Đóng.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060830), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → mở piano roll → thấy 2 hàng toolbar; nút edit note (Humanize/Transpose/Chuyển giọng) ở hàng 2.
|
||||||
|
|||||||
Reference in New Issue
Block a user