From caa95177efd2fc0db8905d8d3f41387afecbdfee Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 29 Jul 2026 09:54:21 +0700 Subject: [PATCH] =?UTF-8?q?FEAT:=20th=C3=AAm=20=20Mixer=20Panel=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/storage/sonicforge.db | Bin 77824 -> 77824 bytes mixer_panel_backup_20260729_0941.patch | 564 +++++++++++++++++++++++ soundfont_fix_backup_20260729_0941.patch | 24 + 3 files changed, 588 insertions(+) create mode 100644 mixer_panel_backup_20260729_0941.patch create mode 100644 soundfont_fix_backup_20260729_0941.patch diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index b1d2ec94d1a0c8be003360e1202979abb9d62c6c..a727a59ea52dcc9c5560d49a5e5d7f7edf60031f 100644 GIT binary patch delta 442 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 V5C8!X009sH0T2KI5cswU{0|?#`nCW7 diff --git a/mixer_panel_backup_20260729_0941.patch b/mixer_panel_backup_20260729_0941.patch new file mode 100644 index 0000000..3f985b4 --- /dev/null +++ b/mixer_panel_backup_20260729_0941.patch @@ -0,0 +1,564 @@ +From 46d3778a5d5305ba625318433b7f98c1e4dac932 Mon Sep 17 00:00:00 2001 +From: 3dtours +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`. ++- **Section Bus Map** — `Map`. ++- `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** — `` 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 `` 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%`*hfMN0La&>KGZggL8a&KpHVQnHhZgp&IEFx)cb98cbZDnqBUv6P-WgtK*$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 +zB6N9hWgN0La&>KGZggL8a&KpHVQnHhZgp&IEFx)c +zb98cbZDnqBUv6P-WgtK*$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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 +zG&eOcFflnXUo$WwEFy4ebYo~DIyN>eB6DV>{ +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 +zbYo~DIyEvZB6DV>{Vr5};b0Ru0E;2PN +zB6ekLZ)0h6c_KP6E;TG7aA9sDIxu}Kdm?FMB03^&Z**l}a%Ew1a$hkwI5jdfH#IOY +zF*z|`HXV`bYXIIUt(opbRs%`F)lSMB4l-PVRUJ4ZeL<$VRUmM +zIxsFWH7p`_Wo&O_X>@raIxsFZEFy4WZX!A`eJpz-X=EZgB5rSVWnXe-VRCX`F*i6h +zGBh_eFfcJWF<&<#EFy4ebYo~DIyN*cB6DV>{ +zVr5}}baNs)FfKAREFyMgY;R*}ba^5=FfKGKB5+}DB04aAEPEnpWFk5uZf|sDUvgz( +za&liWH#jviG&eOcFflnXUpOKxB5-MRV`w5eH8m_Eb97;HbYEg+VRRxoI4mM$b#h^J +zX>V>{Vr5};b0Ru0E;2PNB6ekLZ)0h6c_KQ0FfKMMB5+}DB04aAEPEnpWFk5uZf|sD +zUvgz(a&liWH#jviG&eOcFflnXUpXQyB5-MRV`w5eH90IIb97;HbYEg+VRRxoI4&|Z +zEFxrea$$67Z*E^=Wnpx4B04ZGGBqqBc4cgDV`+4GB04ZGH7p`@raIxsFZEFy4WZX!A` +zeJpz-X=EZgB5rSVWnXe-VRCX`F*i7WH8M0eH83zSIWb=`F(ND?aA|a7Xd*f`H!LD^ +zbYXIIUt(opbRs%9E;ltSB4l-PVRUJ4ZeL<$VRUmMIxsFWH7p`_Wo&O_X>@raIxsFY +zEFy4WZX!A`eJpz-X=EZgB5rSVWnXe-VRCX`F*i6hGBh_eFfcJWF<&tV`bYXIIUt(o{VRRxoIW96aEFxrea$$67Z*E^=Wnpx4B04ZGGBqqB +zc4cgDV`+4GB04ZGH7p` + + +- ++ + +