565 lines
29 KiB
Diff
565 lines
29 KiB
Diff
From 46d3778a5d5305ba625318433b7f98c1e4dac932 Mon Sep 17 00:00:00 2001
|
|
From: 3dtours <yeunhiepanh.photo@gmail.com>
|
|
Date: Wed, 29 Jul 2026 09:11:54 +0700
|
|
Subject: [PATCH] =?UTF-8?q?FIX:=20chi=E1=BB=81u=20cao=20c=E1=BB=A7a=20trac?=
|
|
=?UTF-8?q?k=20thu=20nh=E1=BB=8F=20theo=20chi=E1=BB=81u=20cao=20c=E1=BB=A7?=
|
|
=?UTF-8?q?a=20Mixer=20Panel?=
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain; charset=UTF-8
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
---
|
|
.kilo/plans/mixer-equalizer-plan.md | 293 ++++++++++++++++++
|
|
.../js/services/audioMixerGraphManager.js | 20 +-
|
|
app/storage/sonicforge.db | Bin 77824 -> 77824 bytes
|
|
app/templates/index.html | 2 +-
|
|
4 files changed, 310 insertions(+), 5 deletions(-)
|
|
create mode 100644 .kilo/plans/mixer-equalizer-plan.md
|
|
|
|
diff --git a/.kilo/plans/mixer-equalizer-plan.md b/.kilo/plans/mixer-equalizer-plan.md
|
|
new file mode 100644
|
|
index 0000000..2fbf79e
|
|
--- /dev/null
|
|
+++ b/.kilo/plans/mixer-equalizer-plan.md
|
|
@@ -0,0 +1,293 @@
|
|
+# Mixer Console & Equalizer Implementation Plan
|
|
+
|
|
+## Goal
|
|
+Implement a docked Mixer Console panel (similar to REAPER's MCP) at the bottom of the DAW, featuring per-track channel strips with volume fader, pan knob, Mute/Solo, VU meter, and FX controls. Support MAIN SESSION and SECTION-TAB contexts with hierarchical audio routing.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 1: Create `AudioMixerGraphManager`
|
|
+
|
|
+**File:** `app/static/js/services/audioMixerGraphManager.js` (new)
|
|
+
|
|
+Class managing all Web Audio nodes with a proper Master Bus signal chain:
|
|
+
|
|
+```
|
|
+source → trackGain → faderGain → panNode → analyserNode → masterGain → masterPan → masterAnalyser → destination
|
|
+```
|
|
+
|
|
+### Responsibilities
|
|
+
|
|
+- **Singleton pattern** — created once with `audioCtx`, stored on `window.AudioMixerGraphManager`.
|
|
+- **Master Bus** — `masterGain → masterPan → masterAnalyser → audioCtx.destination`.
|
|
+- **Track Node Map** — `Map<trackId, { inputGain, faderGain, panNode, analyserNode, track }>`.
|
|
+- **Section Bus Map** — `Map<sectionId, { subMixGain, subMixPan, hostTrackId }>`.
|
|
+- `createMainTrackNodes(track)` — creates node chain, connects analyser → masterGain. Stores in `trackNodesMap`. Calls `updateTrackVolumePan()`.
|
|
+- `createSectionSubMixBus(sectionId, hostTrackId)` — creates subMixGain→subMixPan, pipes to host track's `inputGain`. Falls back to masterGain if host track not found.
|
|
+- `createSectionTrackNodes(sectionId, secTrack)` — creates nodes for internal section tracks, connects analyser → section's `subMixGain`.
|
|
+- `updateTrackVolumePan(trackId, volumeDb, panValue, isMuted)` — updates gain/pan in real-time. `gain = isMuted ? 0 : Math.pow(10, volumeDb / 20.0)`.
|
|
+- `getAnalyserData(trackId)` — returns `analyserNode` frequency/time-domain data for VU meter rendering.
|
|
+- `removeTrackNodes(trackId)` — disconnects and removes from map.
|
|
+
|
|
+### Signal Equations
|
|
+- `linearGain = isMuted ? 0 : Math.pow(10, volumeDb / 20.0)` — dB to linear scale.
|
|
+- `panValue = track.pan / 100` — existing -100..+100 range maps to -1.0..+1.0 `StereoPanner.pan`.
|
|
+- Use `setTargetAtTime(linearGain, ctx.currentTime, 0.01)` for smooth transitions.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 2: Refactor `app.jsx` Audio Node Creation
|
|
+
|
|
+**File:** `app/static/js/app.jsx` — modify existing audio routing functions.
|
|
+
|
|
+### 2a. Replace `getOrCreateTrackNode()` with `AudioMixerGraphManager`
|
|
+
|
|
+Currently lines 10314-10343 create a local `{ gainNode, pannerNode }` per track and connect `pannerNode → context.destination`. Replace this with:
|
|
+
|
|
+```js
|
|
+function getOrCreateTrackNode(track, context) {
|
|
+ if (!window.AudioMixerGraphManager) return null;
|
|
+ const mgr = window.AudioMixerGraphManager;
|
|
+ const bundle = mgr.trackNodesMap.get(track.id);
|
|
+ if (bundle) return bundle.faderGain;
|
|
+ // Create nodes via AudioMixerGraphManager
|
|
+ mgr.createMainTrackNodes(track);
|
|
+ return mgr.trackNodesMap.get(track.id).faderGain;
|
|
+}
|
|
+```
|
|
+
|
|
+### 2b. Section Sub-Track Audio Routing
|
|
+
|
|
+Replace `getOrCreateSubTrackNode()` (lines 10344-10359) to:
|
|
+1. Create section sub-mix bus via `AudioMixerGraphManager.createSectionSubMixBus()` (first time only, lazy init).
|
|
+2. Create section internal track nodes via `AudioMixerGraphManager.createSectionTrackNodes()`.
|
|
+3. Return `faderGain` node for audio connection.
|
|
+
|
|
+### 2c. SoundFont MIDI Audio Routing Fix
|
|
+
|
|
+**Critical:** FluidSynth renders directly to `audioCtx.destination` via ScriptProcessorNode. SoundFont player (`soundfontPlayer.js` lines 27-49) has its own `_gainNode` connected to destination. This bypasses the track's gain/panner nodes.
|
|
+
|
|
+**Fix in `soundfontPlayer.js`:**
|
|
+- Modify the first `_gainNode` creation so that instead of `_gainNode.connect(ctx.destination)`, it connects to a pass-through that can be dynamically routed to any destination node.
|
|
+- OR: In `startTrackPlayback()` (app.jsx ~line 10382), after calling `window.SonicSF.playNote(...)`, re-route the SF output by calling a new method `window.SonicSF.setOutputNode(destinationNode)` that disconnects the internal `_gainNode` from `destination` and reconnects it to the track's `faderGain`.
|
|
+
|
|
+**Simpler approach:** Add `setOutputNode(destinationNode)` to `soundfontPlayer.js`:
|
|
+```js
|
|
+function setOutputNode(newDest) {
|
|
+ _gainNode.disconnect();
|
|
+ _gainNode.connect(newDest || ctx.destination);
|
|
+}
|
|
+```
|
|
+
|
|
+Call this from `getOrCreateTrackNode()` after creating/retrieving the track's `faderGain`, passing `trackFaderGain` as the destination.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 3: Mixer Console State & Toggle
|
|
+
|
|
+**File:** `app/static/js/app.jsx`
|
|
+
|
|
+### 3a. Add Mixer Visibility State
|
|
+
|
|
+```js
|
|
+const [showMixer, setShowMixer] = useState(false);
|
|
+```
|
|
+
|
|
+### 3b. Mixer Toggle Button
|
|
+
|
|
+Add a toolbar button (e.g., in the TCP header near the "Add Track" button, or in a bottom toolbar). Icon: "mixer" (sliders icon). Toggles `showMixer`.
|
|
+
|
|
+### 3c. Determine Active Context
|
|
+
|
|
+When `activeTab === 'main'`, mixer shows MAIN SESSION tracks. When a SECTION-TAB is active, mixer shows the section's internal tracks (from `sessionTabs.find(s => s.id === activeTab).tracks`).
|
|
+
|
|
+### 3d. Pass to MixerConsole
|
|
+
|
|
+```
|
|
+mixerContext = { isSectionTab, currentTracks, currentTitle, activeTabContext }
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Task 4: Create MixerConsole React Component
|
|
+
|
|
+**File:** `app/static/js/components/MixerConsole.jsx` (new)
|
|
+
|
|
+Docked at the bottom of the timeline (inside the `bottom` dock panel or a fixed container at bottom of the `flex-1 flex flex-col overflow-hidden` area, below the timeline canvas).
|
|
+
|
|
+### 4a. Component Structure
|
|
+
|
|
+```
|
|
+MixerConsole (flex-col h-48 border-t border-zinc-700 bg-slate-900)
|
|
+├── MixerHeader (flex, shows "MAIN SESSION" or "SECTION TAB" label + close button)
|
|
+└── MixerStripsWrapper (flex-1 flex overflow-x-auto p-2 gap-1.5)
|
|
+ ├── MasterChannelStrip (w-28, fixed, left-aligned)
|
|
+ ├── Divider (w-[1px] bg-zinc-700)
|
|
+ └── Track strips (horizontal scroll, each TrackChannelStrip w-24)
|
|
+```
|
|
+
|
|
+### 4b. MasterChannelStrip
|
|
+
|
|
+Props: `isSectionBus, mixerGraphMgr`
|
|
+
|
|
+- Shows "MASTER" or "SEC BUS" header.
|
|
+- VU Meter canvas element (reads `masterAnalyser` frequency data, draws RMS/Peak bar).
|
|
+- Peak/rms numeric readout.
|
|
+- Mono toggle button.
|
|
+- "MAIN OUT" / "SUB-MIX" footer.
|
|
+
|
|
+### 4c. TrackChannelStrip
|
|
+
|
|
+Props: `track, index, mixerGraphMgr`
|
|
+
|
|
+Local state: `volumeDb, pan, isMuted, isSoloed` — initialized from `track` props.
|
|
+
|
|
+Renders:
|
|
+1. **Pan knob** — `<input type="range" min="-1" max="1" step="0.05">` vertical/horizontal slider + numeric label (C / Lxx / Rxx).
|
|
+2. **dB Readout** — `volumeDb <= -60 ? "-inf" : "[+/-]N.NdB"`.
|
|
+3. **Mute / Solo buttons** — `[M]` orange when muted, `[S]` yellow when soloed. Mute dispatches `updateTrackVolumePan(trackId, vol, pan, newMute)`. Solo dispatches `toggleTrackSoloEvaluate(trackId)`.
|
|
+4. **Fader + VU Meter** — vertical fader `<input type="range" min="-60" max="+12" step="0.5">` with CSS `writing-mode: vertical-lr`. VU meter bar (vertical `div` with gradient, height calculated from analyser RMS data). RequestAnimationFrame loop reads `analyserNode.getByteTimeDomainData()` and updates VU height.
|
|
+5. **FX / Power buttons** — `[FX]` stub that shows toast "FX panel coming soon". `[⑈]` bypass toggle (stub).
|
|
+6. **Footer** — `{index}. {track.name}` + `{track.type}`.
|
|
+
|
|
+### 4e. VU Meter Animation
|
|
+
|
|
+Use `requestAnimationFrame` loop inside `TrackChannelStrip`:
|
|
+```js
|
|
+useEffect(() => {
|
|
+ const analyser = mixerGraphMgr?.trackNodesMap.get(track.id)?.analyserNode;
|
|
+ if (!analyser) return;
|
|
+ analyser.fftSize = 128;
|
|
+ const bufferLength = analyser.frequencyBinCount;
|
|
+ const dataArray = new Uint8Array(bufferLength);
|
|
+ let rafId;
|
|
+ const draw = () => {
|
|
+ analyser.getByteTimeDomainData(dataArray);
|
|
+ const rms = Math.sqrt(dataArray.reduce((sum, v) => sum + ((v - 128) / 128) ** 2, 0) / bufferLength);
|
|
+ setMeterLevel(Math.min(1, rms * 2));
|
|
+ rafId = requestAnimationFrame(draw);
|
|
+ };
|
|
+ rafId = requestAnimationFrame(draw);
|
|
+ return () => cancelAnimationFrame(rafId);
|
|
+}, [track.id, mixerGraphMgr]);
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Task 5: Add Mixer Toggle Button (TCP Header)
|
|
+
|
|
+**File:** `app/static/js/app.jsx` — TCP header area (line 16648-16656).
|
|
+
|
|
+Add a mixer toggle button next to the "Add Track" button:
|
|
+
|
|
+```jsx
|
|
+/*#__PURE__*/React.createElement("button", {
|
|
+ onClick: () => setShowMixer(!showMixer),
|
|
+ className: "px-2 py-1 bg-indigo-700 hover:bg-indigo-600 text-white rounded text-xs font-semibold flex items-center gap-1 shadow transition"
|
|
+}, /*#__PURE__*/React.createElement("span", {
|
|
+ className: "inline-flex items-center shrink-0"
|
|
+}, /*#__PURE__*/React.createElement("i", {
|
|
+ "data-lucide": "sliders-horizontal",
|
|
+ className: "w-3.5 h-3.5"
|
|
+})), " Mixer")
|
|
+```
|
|
+
|
|
+## Task 6: Add F7 Keyboard Shortcut for Mixer Toggle
|
|
+
|
|
+**File:** `app/static/js/app.jsx` — modify the global keyboard handler (lines 8037-8335).
|
|
+
|
|
+The keydown handler at line 8334 (`window.addEventListener('keydown', handler, { capture: true })`) already handles hotkeys like Space (play/pause) and function keys for sub-tab operations.
|
|
+
|
|
+Add an `F7` check inside the handler, **before** the sub-tab-only block at line 8054 (so it works in both MAIN SESSION and sub-tabs):
|
|
+
|
|
+```js
|
|
+// F7: Toggle Mixer Console
|
|
+if (e.key === 'F7') {
|
|
+ e.preventDefault();
|
|
+ setShowMixerRef.current(prev => !prev);
|
|
+ return;
|
|
+}
|
|
+```
|
|
+
|
|
+Use a ref (`showMixerRef`) to access the current `showMixer` state without needing it as a dependency of the effect. Create the ref and keep it synced in the component body:
|
|
+
|
|
+```js
|
|
+const showMixerRef = useRef(showMixer);
|
|
+showMixerRef.current = showMixer;
|
|
+```
|
|
+
|
|
+Keep `setShowMixerRef` as a ref to `setShowMixer` to call it from the event listener without stale closures.
|
|
+
|
|
+This avoids adding `showMixer` as a dependency to the keyboard effect, which would cause unnecessary listener re-registrations.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 7: Integrate MixerConsole into App Layout
|
|
+
|
|
+**File:** `app/static/js/app.jsx`
|
|
+
|
|
+### 5a. Placement
|
|
+
|
|
+After the timeline track list (the `div` containing `activeTracks.map(...)` and the add-track row), and before the transport bar, insert:
|
|
+
|
|
+```jsx
|
|
+showMixer && React.createElement(MixerConsole, {
|
|
+ key: 'mixer-console',
|
|
+ mixerContext: { isSectionTab, currentTracks, currentTitle, activeTabContext },
|
|
+ mixerGraphMgr: window.AudioMixerGraphManager,
|
|
+ trackActions: { updateTrackVolumeDb, updateTrackPan, toggleTrackMute, toggleTrackSoloEvaluate }
|
|
+})
|
|
+```
|
|
+
|
|
+### 5b. Panel System Integration
|
|
+
|
|
+Alternatively, register "mixer" as a panel that can be docked to the `bottom` dock panel. The panel content renders `MixerConsole`. This reuses the existing `dockPanels` / `renderDock` system — add a menu item or button that calls `addToDock('bottom', 'mixer')`.
|
|
+
|
|
+Simpler approach: fixed position below the timeline with a toggle button.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 8: Section-Tab Context Switching
|
|
+
|
|
+**File:** `app/static/js/app.jsx`
|
|
+
|
|
+When `activeTab` changes to a SECTION-TAB (`sessionTabs.some(s => s.id === activeTab)`):
|
|
+- MixerConsole's `currentTracks` switches to the section's internal tracks.
|
|
+- The Master strip label changes from "MASTER" to "SEC BUS".
|
|
+- Header shows "MIXER: SECTION TAB ({sectionName})".
|
|
+- Audio routing uses `createSectionSubMixBus` + `createSectionTrackNodes`.
|
|
+
|
|
+When switching back to `activeTab === 'main'`:
|
|
+- MixerConsole reverts to MAIN SESSION tracks and Master strip.
|
|
+
|
|
+This logic lives inside a `useEffect` that watches `activeTab` and re-renders the MixerConsole with the correct context.
|
|
+
|
|
+---
|
|
+
|
|
+## Task 9: Mixer Resize & Toggle Persistence
|
|
+
|
|
+- Add a top drag handle on the MixerConsole (`h-1 cursor-ns-resize hover:bg-cyan-500/50`) to adjust height (range: `h-32` to `h-80`). Store height in a ref.
|
|
+- Store `showMixer` and mixer height in `localStorage` for persistence.
|
|
+- Close button (`[X]`) on the Mixer header sets `showMixer = false`.
|
|
+
|
|
+---
|
|
+
|
|
+## Validation Plan (Additions for F7 + Toggle Button)
|
|
+
|
|
+1. [ ] **Button toggle** — Click "Mixer" button in TCP header → MixerConsole appears. Click again → hides.
|
|
+2. [ ] **F7 hotkey** — Press F7 → MixerConsole toggles. Works in both MAIN SESSION and SECTION-TAB. Does NOT interfere with input/textarea focus.
|
|
+3. [ ] **No stale closure** — Repeated F7 presses reliably toggle state (setShowMixerRef.current used, not a stale function).
|
|
+
|
|
+## Extended Validation Plan
|
|
+
|
|
+1. **Toggle mixer** — Click the Mixer button → MixerConsole appears at bottom. Click again → hides.
|
|
+2. **MAIN SESSION mode** — Shows all tracks in the session. Master strip on left.
|
|
+3. **Volume fader** — Drag fader → track volume changes audibly in real-time. Numeric dB readout updates.
|
|
+4. **Pan knob** — Drag pan → stereo position changes. Label shows C / Lxx / Rxx.
|
|
+5. **Mute** — Click [M] → track mutes, button turns orange. Click again → unmutes.
|
|
+6. **Solo** — Click [S] on track 1 → track 1 plays, all other tracks silence. Click again → all tracks resume.
|
|
+7. **VU Meter** — During playback, meter bars animate with signal level (green/yellow/red gradient).
|
|
+8. **SECTION-TAB** — Open a section → MixerConsole switches to section tracks, Master strip shows "SEC BUS".
|
|
+9. **Section sub-mix volume** — Adjust fader on the SECTION host track → entire section volume changes.
|
|
+10. **MIDI audio routing** — SoundFont MIDI notes play through the track's fader/panner, not bypassing.
|
|
+11. **Resize** — Drag the resize handle on top of MixerConsole → height changes.
|
|
+12. **Persistence** — Reload page → mixer state restored from localStorage.
|
|
diff --git a/app/static/js/services/audioMixerGraphManager.js b/app/static/js/services/audioMixerGraphManager.js
|
|
index 93c530b..60c39c2 100644
|
|
--- a/app/static/js/services/audioMixerGraphManager.js
|
|
+++ b/app/static/js/services/audioMixerGraphManager.js
|
|
@@ -4,10 +4,22 @@
|
|
function AudioMixerGraphManager(audioCtx) {
|
|
this.audioCtx = audioCtx;
|
|
this.trackNodesMap = new Map();
|
|
- this.sectionBusMap = new Map();
|
|
-
|
|
- // Master bus chain: masterGain -> masterPan -> masterAnalyser -> destination
|
|
- this.masterGain = audioCtx.createGain();
|
|
+ this.sectionBusMap = new Map();
|
|
+ this.trackFxBypassGains = new Map();
|
|
+
|
|
+ // Master bus chain: masterGain -> masterPan -> masterBypassGain -> masterAnalyser -> destination
|
|
+ this.masterGain = audioCtx.createGain();
|
|
+ this.masterGain.gain.setValueAtTime(1, audioCtx.currentTime);
|
|
+ this.masterPan = audioCtx.createStereoPanner();
|
|
+ this.masterPan.pan.setValueAtTime(0, audioCtx.currentTime);
|
|
+ this.masterBypassGain = audioCtx.createGain();
|
|
+ this.masterBypassGain.gain.setValueAtTime(1, audioCtx.currentTime);
|
|
+ this.masterAnalyser = audioCtx.createAnalyser();
|
|
+ this.masterAnalyser.fftSize = 128;
|
|
+ this.masterGain.connect(this.masterPan);
|
|
+ this.masterPan.connect(this.masterBypassGain);
|
|
+ this.masterBypassGain.connect(this.masterAnalyser);
|
|
+ this.masterAnalyser.connect(audioCtx.destination);
|
|
this.masterGain.gain.setValueAtTime(1, audioCtx.currentTime);
|
|
this.masterPan = audioCtx.createStereoPanner();
|
|
this.masterPan.pan.setValueAtTime(0, audioCtx.currentTime);
|
|
diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db
|
|
index b1d2ec94d1a0c8be003360e1202979abb9d62c6c..0ffa5a826df5e7beabb1bee853e683414f554125 100644
|
|
GIT binary patch
|
|
delta 1068
|
|
zcmV+{1k?L~-~@o+1dtmIp8x;=6951J5dZ)H29YQ?0iUs85FY^vlW`wHKsjPDIWspl
|
|
zH)J(pH#0ObH8f*2V_{=rW@b1zGG;b2HaIb6GB{#mH8eM3FfleWFl1(AWH2#dGdX5v
|
|
zHDfX{FthU?5nu`f50U^6k`L|=z_Sq`gb$Ozj|n9-F*!6aI5;^iF*rG8Ei`0iG%YwW
|
|
zV=^sdVlZQ2VKp=~VK8L_v%rr;FtY{V{0|Hb3zPr?2gCpjEeFK0fw%`*hfM<sUsXSi
|
|
zH3<R-bY*RDUo<f}G%z?gIV~|bIb|(0WM(uiI5A@~Eo5RaV_{)6G&Er_Wi&B4G%z?g
|
|
zIV~|bIb|(0WM(uiI5A@~Eo5RaV_{)6G&Er_Ws_^sC?j-bZE#<3a&Ky7V{{@cB5h@K
|
|
zVPs)+VIn$vB6MkVY-J)kB9m*;8I#h{<`-0QVPk6`Ffk%5B6N9hWg<EvK~+RaPm{UF
|
|
zLN#u6Y-}tdX>N0La&>KGZggL8a&KpHVQnHhZgp&IEFx)cb98cbZDnqBUv6P-Wg<Fm
|
|
zb!=>tK*$v?Zgp&IEFyDnb#7#4Z*Fv7X=EZgZgp&IEFyDnb#7#4Z*Fv7VqtD;B06q$
|
|
zY;2Pd)D=2zb!==bB58DGZF3?zTU~uDdm?FMB03^6A}k_qVQpn1IwDkZVPk6`Fft-6
|
|
zB6N9hWg<EvK~+RaPm{UFLN#u6Y-}tdX>N0La&>KGZggL8a&KpHVQnHhZgp&IEFx)c
|
|
zb98cbZDnqBUv6P-Wg<Fmb!=>tK*$v?Zgp&IEFyDnb#7#4Z*Fv7X=EZgZgp&IEFyDn
|
|
zb#7#4Z*Fv7VqtD;B06q$Y;2Pd)D;eHb!==bB58DGZF3?zTa(bwCJqBsLE4&AiE|l4
|
|
z0001)k%5&7hfo3uUsXSiJF^?nodFkAa$#d@ATTi^EFyGyaAhJoB0*I|Nl%lx$3iu3
|
|
zb!==bB57`ObaHiVWo~p|aB^>Fa$#*EI&O7rY%C&aZgX^Ub!}yCbYE^^ZDk@lZgp&I
|
|
zlR(H7E^c*fY%C&kZ*^{DW^ZnEUuk3_I&O7rY%C&kZ*^{DW^ZnEUt(cyYa%*sb!=>t
|
|
z5Y!bqZgp&IEFx)iWo>gJI$K?REPEnpWFk5uG9oM@ZeeX@B03^ea$#d@ATTl_EFyGy
|
|
zaAhJoB0*I|Nl%lx$3iu3b!==bB57`ObaHiVWo~p|aB^>Fa$#*EI&O7rY%C&aZgX^U
|
|
zb!}yCbYE^^ZDk@lZgp&IlR(H7E^c*fY%C&kZ*^{DW^ZnEUuk3_I&O7rY%C&kZ*^{D
|
|
zW^ZnEUt(cyYa%*sb!=>t5Y!b8Zgp&IEFx)iWo>gJI$M*_&n5~3S3%mESWSxm%(DUD
|
|
m1|Svy1poj55C8xG6951J5&!@I4*&oF4YL8@1|YK_aNZB((q(7>
|
|
|
|
delta 7203
|
|
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
|
|
|
|
diff --git a/app/templates/index.html b/app/templates/index.html
|
|
index 1207348..5fa01fe 100644
|
|
--- a/app/templates/index.html
|
|
+++ b/app/templates/index.html
|
|
@@ -23,7 +23,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/audioMixerGraphManager.js?v=202607282218"></script>
|
|
- <script src="/static/js/app.precompiled.js?v=202607282359" defer></script>
|
|
+ <script src="/static/js/app.precompiled.js?v=20260729085936" defer></script>
|
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
|
<style>
|
|
:root {
|
|
--
|
|
2.47.3
|
|
|
|
diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx
|
|
index c53adde..8189c12 100644
|
|
--- a/app/static/js/app.jsx
|
|
+++ b/app/static/js/app.jsx
|
|
@@ -6025,7 +6025,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|
keybedMouseDownRef.current = true;
|
|
try {
|
|
if (window.SonicSF) {
|
|
- window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
|
+ window.SonicSF.playNote(pitch, 100, 60000, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
|
}
|
|
} catch (err) {
|
|
console.error('playNote error:', err);
|
|
@@ -6035,12 +6035,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|
if (keybedMouseDownRef.current) {
|
|
try {
|
|
if (window.SonicSF) {
|
|
- window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
|
+ window.SonicSF.playNote(pitch, 100, 60000, undefined, st.instrumentProgram, null, kbCh, kbSynth);
|
|
}
|
|
} catch (err) { console.error('playNote error:', err); }
|
|
}
|
|
},
|
|
- onMouseUp: () => { keybedMouseDownRef.current = false; }
|
|
+ onMouseUp: () => {
|
|
+ keybedMouseDownRef.current = false;
|
|
+ if (window.SonicSF && window.SonicSF.stopNote) {
|
|
+ window.SonicSF.stopNote(kbCh, pitch);
|
|
+ }
|
|
+ }
|
|
}, showLabel && label)
|
|
);
|
|
}
|