FIX: sửa UNDO/REDO bị lỗi màn hình đen
This commit is contained in:
+11
-12
@@ -12285,14 +12285,13 @@ const App = () => {
|
||||
});
|
||||
setDraggedSectionItem({ trackId, itemType, itemId: rearrangeNewId, clickOffset, isDuplicate: false, originalPositions: { [rearrangeNewId]: itemType === 'section' ? item.start : item.startTime } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
var curTrk = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null;
|
||||
var its = itemType === 'section' ? (curTrk?.sections || []) : (curTrk?.midiItems || []);
|
||||
var it = its.find(function(x) { return x.id === itemId; });
|
||||
var origPos = it ? (itemType === 'section' ? it.start : it.startTime) : 0;
|
||||
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, originalPositions: { [itemId]: origPos } });
|
||||
};
|
||||
} else {
|
||||
var curTrk = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null;
|
||||
var its = itemType === 'section' ? (curTrk?.sections || []) : (curTrk?.midiItems || []);
|
||||
var it = its.find(function(x) { return x.id === itemId; });
|
||||
var origPos = it ? (itemType === 'section' ? it.start : it.startTime) : 0;
|
||||
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, originalPositions: { [itemId]: origPos } });
|
||||
}
|
||||
};
|
||||
handleSectionItemDragStartRef.current = handleSectionItemDragStart;
|
||||
|
||||
@@ -17099,7 +17098,7 @@ setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, origin
|
||||
className: "w-12 bg-zinc-800 border border-zinc-700 rounded text-xs text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500",
|
||||
min: "40",
|
||||
max: "300"
|
||||
}), /*#__PURE__*/React.createElement("span",
|
||||
}), /*#__PURE__*/React.createElement("span", {
|
||||
className: "text-xs text-zinc-500"
|
||||
}, "BPM")))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"
|
||||
@@ -18691,7 +18690,7 @@ setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, origin
|
||||
/*#__PURE__*/React.createElement("div", { className: "flex gap-4", style: { height: "420px" } },
|
||||
/*#__PURE__*/React.createElement("div", { className: "w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2" },
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => setSelectedSoundFontId(null); setSynthCategory(null); },
|
||||
onClick: () => { setSelectedSoundFontId(null); setSynthCategory(null); },
|
||||
className: "w-full text-left px-3 py-2 text-sm rounded " + (!selectedSoundFontId ? "bg-amber-700 text-white" : "bg-zinc-800 hover:bg-zinc-700 text-zinc-300")
|
||||
}, "All Instruments"),
|
||||
(instrumentSelectorData?.soundfonts || []).map(sf => {
|
||||
@@ -18744,7 +18743,7 @@ setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, origin
|
||||
)
|
||||
)
|
||||
)
|
||||
)), fxSelectorTrackId && /*#__PURE__*/React.createElement("div", {
|
||||
), fxSelectorTrackId && /*#__PURE__*/React.createElement("div", {
|
||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
||||
onClick: () => setFxSelectorTrackId(null)
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
@@ -18754,7 +18753,7 @@ setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, origin
|
||||
/*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, null), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400" }, "None"),
|
||||
/*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, 'chorus'), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300" }, "Chorus"),
|
||||
/*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, 'reverb'), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300" }, "Reverb")
|
||||
), instrumentDropdownTrackId && instrumentDropdownBtnRect && /*#__PURE__*/React.createElement("div", {
|
||||
))), instrumentDropdownTrackId && instrumentDropdownBtnRect && /*#__PURE__*/React.createElement("div", {
|
||||
"data-instr-dropdown": "",
|
||||
className: "fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",
|
||||
style: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -22,7 +22,8 @@
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></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/app.precompiled.js?v=202607271245" defer></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607290941" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
@@ -0,0 +1,887 @@
|
||||
# TECHNICAL SPECIFICATION & OPERATIONAL WORKFLOW MANUAL: OZONE WEB MASTERING SUITE
|
||||
|
||||
This document compiles the complete architectural layout, module inventory, control parameter specifications, dynamic re-routing execution matrices, visual metering systems, and full single-file implementation code for the Ozone-Style Web Mastering Engine.
|
||||
|
||||
---
|
||||
|
||||
## 1. AUDIO PROCESSING MODULES SUMMARY
|
||||
|
||||
| Module Name | Core Function | Web Audio API Nodes / Class | Key Control Parameters | Inherited Acoustic Impact |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Dynamic EQ** | Dynamic frequency filtering and equalization | `BiquadFilterNode` (`lowshelf`, `peaking`, `highshelf`) | 4-Band Gain ($-12 \text{ to } +12\text{ dB}$), Frequency, Q Factor | Balances frequency spectrum, carves out harsh resonances, or boosts air/warmth dynamically without introducing harsh clipping. |
|
||||
| **Stereo Imager** | Mid/Side spatial expansion and balance | `ChannelSplitterNode`, `ChannelMergerNode`, `GainNode` | 4-Band Width ($-100\% \text{ to } +100\%$), Mid/Side Gain | Expands stereo width across High/Mid bands while collapsing Low/Sub-bass bands to Mono for maximum low-end punch. |
|
||||
| **IRC IV Maximizer** | True-Peak Limiter & Peak Gain Boosting | `WaveShaperNode` (Soft Clip), `DynamicsCompressorNode` | Boost Gain ($+0 \text{ to } +12\text{ dB}$), Soft Clip %, Upward Comp, Ceiling | Maximizes perceived loudness (LUFS) to commercial distribution standards while suppressing transient spikes below $0\text{ dBFS}$. |
|
||||
| **Master Utilities** | I/O Management, Metering & Codec Emulation | `AnalyserNode` (FFT 2048), `GainNode` | Delta Listen, Gain Match, Codec Preview, Dither | Enables objective A/B listening tests between pre/post-mastered signals, emulates lossy MP3/AAC compression, and manages bit-depth reduction. |
|
||||
|
||||
---
|
||||
|
||||
## 2. PARAMETER CONTROL SPECIFICATIONS MATRIX
|
||||
|
||||
| Module Group | Parameter Name | Range | Unit | Node Handler / Formula | Technical Purpose |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **Dynamic EQ** | `eqLowGain` | $-12.0 \to +12.0$ | dB | Lowshelf @ $100\text{Hz}$ | Controls low-end punch and sub-bass weight. |
|
||||
| **Dynamic EQ** | `eqMid1Gain` | $-12.0 \to +12.0$ | dB | Peaking @ $822\text{Hz}$, $Q=0.7$ | Cleans up muddy or boxy low-mid frequencies. |
|
||||
| **Dynamic EQ** | `eqMid2Gain` | $-12.0 \to +12.0$ | dB | Peaking @ $3.2\text{kHz}$, $Q=1.2$ | Adjusts vocal clarity and instrument presence. |
|
||||
| **Dynamic EQ** | `eqHighGain` | $-12.0 \to +12.0$ | dB | Highshelf @ $10\text{kHz}$ | Adds high-frequency air, sheen, and sparkle. |
|
||||
| **Stereo Imager** | `w1` (Band 1) | $-100 \to +100$ | % | Mid/Side Gain ($0-100\text{Hz}$) | Collapses sub-bass to mono ($0\%$) to ensure club system compatibility. |
|
||||
| **Stereo Imager** | `w2` (Band 2) | $-100 \to +100$ | % | Mid/Side Gain ($100\text{Hz}-1\text{kHz}$) | Balances body and width for core instruments. |
|
||||
| **Stereo Imager** | `w3` (Band 3) | $-100 \to +100$ | % | Mid/Side Gain ($1\text{kHz}-6\text{kHz}$) | Expands upper-mid imaging for a wider soundstage. |
|
||||
| **Stereo Imager** | `w4` (Band 4) | $-100 \to +100$ | % | Mid/Side Gain ($6\text{kHz}-20\text{kHz}$) | Widens top-end spatial atmosphere. |
|
||||
| **Maximizer** | `maxGain` | $0.0 \to +12.0$ | dB | Gain Boost Node | Drives the signal into the limiter to increase overall LUFS. |
|
||||
| **Maximizer** | `maxUpward` | $0.0 \to +10.0$ | dB | Dynamic Range Expander | Brings up low-level details for a denser mix. |
|
||||
| **Maximizer** | `maxSoftClip` | $0 \to 100$ | % | Arctan WaveShaper Curve | Rounds off sharp transient peaks to add analog-style saturation. |
|
||||
| **Maximizer** | `ceiling` | $-6.0 \to 0.0$ | dBFS | Brickwall Limiter Threshold | Absolute ceiling threshold (Default: $-0.10\text{ dBFS}$). |
|
||||
|
||||
---
|
||||
|
||||
## 3. AUDIONODE GRAPH RE-ROUTING EVENT MATRIX
|
||||
|
||||
| UI Action | Triggered Event | Audio Engine Method (`OzoneAudioChainManager`) | Node Graph Execution | Perceived Acoustic Effect |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Add Module (`[+] Add`)** | Click Add Module button | `addModuleToChain(moduleName)` | Appends module to `moduleChain`, disconnects existing routing, and re-connects all nodes in sequence. | Introduces additional processing step to the signal chain prior to Master Out. |
|
||||
| **Remove Module (`[x] Remove`)** | Click Remove Module button | `removeModuleFromChain(moduleName)` | Filters module out of `moduleChain`, disconnects old connections, and connects remaining nodes directly. | Bypasses the removed module's processing, reducing latency and CPU load. |
|
||||
| **Reorder Chain** | Drag & Drop Module Card | `reorderChain(newArray)` | Disconnects the entire graph and rebuilds connections from scratch based on the new array order. | Alters processing order (e.g., Stereo Imager widening prior to EQ vs. post-EQ). |
|
||||
| **Toggle Module Power** | Click Module Power button | `state.moduleActive = !active; updateAudioParameters();` | Sets target band gains to $0\text{ dB}$ or bypasses signal around module. | Passes signal through the target module without tonal modification (flat response). |
|
||||
| **Master Bypass** | Click Master Bypass button | `toggleMasterBypass()` | Reroutes input directly: `inputAnalyser` $\to$ `masterBypassGain` $\to$ `outputAnalyser`. | Instantly returns signal to raw, unprocessed dry audio for A/B comparison. |
|
||||
|
||||
---
|
||||
|
||||
## 4. GRAPHICAL METERS & VISUALIZER CANVAS SYSTEMS
|
||||
|
||||
| Visualizer Canvas | Source Audio Node | Rendering Math / Algorithm | Graphical Characteristics & Refresh Rate |
|
||||
| --- | --- | --- | --- |
|
||||
| **Dynamic EQ Spectrum** | `outputAnalyser.getByteFrequencyData()` | Logarithmic Frequency Scaling ($20\text{Hz} - 20\text{kHz}$) | Cyan composite EQ curve overlaid across a live 60 FPS FFT frequency spectrum fill. |
|
||||
| **Polar Vectorscope** | Channel Splitter (Independent L/R Signals) | Polar Coordinate Calculation ($\theta = \arctan(R/L)$, radius $r$) | 180° radial particle scatter plot depicting phase correlation and stereo width. |
|
||||
| **I/O Stereo VU Meters** | `inputAnalyser` & `outputAnalyser` | Peak Calculation ($dB = 20 \log_{10}(\text{level})$) using Time-Domain data | Vertical Green $\to$ Yellow $\to$ Red gradient meters with real-time numeric dBFS readouts. |
|
||||
|
||||
---
|
||||
|
||||
## 5. MASTER UTILITIES COMPARISON
|
||||
|
||||
| Utility Feature | DSP Mechanism / Formula | Operational Purpose in Mastering |
|
||||
| --- | --- | --- |
|
||||
| **Delta Listen (`[i] Delta`)** | $S_{\text{Delta}}(t) = S_{\text{Input}}(t) - S_{\text{Output}}(t)$ | Isolates the exact signal difference being removed or compressed to prevent over-processing. |
|
||||
| **Gain Match** | $G_{\text{Match}} = \text{LUFS}_{\text{Input}} - \text{LUFS}_{\text{Output}}$ | Automatically matches output loudness to input level to enable objective A/B evaluations free from "louder is better" bias. |
|
||||
| **Codec Preview** | Low-pass Biquad Filter @ $16\text{kHz}$ | Simulates lossy high-frequency roll-off typical of 128 kbps MP3/AAC encoding. |
|
||||
| **Dither Processor** | Triangular PDF Noise Injection | Prevents quantization distortion when converting bit-depth from 32-bit Float down to 16-bit PCM. |
|
||||
|
||||
---
|
||||
|
||||
## 6. FULL SINGLE-FILE IMPLEMENTATION CODE
|
||||
|
||||
Below is the complete single-file source code incorporating HTML5, Tailwind CSS, FontAwesome, HTML5 Canvas, and Web Audio API DSP engines modeling the iZotope Ozone mastering interface:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-slate-950 text-slate-100 select-none">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ozone Mastering Studio Engine</title>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: #080b12; }
|
||||
::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #334155; }
|
||||
|
||||
.knob-container { touch-action: none; user-select: none; }
|
||||
.knob-dial {
|
||||
transition: transform 0.05s ease-out;
|
||||
box-shadow: inset 0 1px 3px rgba(255,255,255,0.15), 0 3px 8px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
/* Ozone Theme Dark Gradient Panels */
|
||||
.oz-panel {
|
||||
background: linear-gradient(180deg, #131822 0%, #0d1118 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
.oz-card {
|
||||
background: #18202c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.oz-card-active {
|
||||
background: #1e2a3a;
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full flex flex-col overflow-hidden bg-slate-950 text-slate-200">
|
||||
|
||||
<!-- TOP TRANSPORT & SESSION BAR -->
|
||||
<header class="h-12 bg-slate-900/90 border-b border-slate-800/80 flex items-center justify-between px-4 z-30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-7 h-7 rounded-lg bg-cyan-600 flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-950">
|
||||
<i class="fa-solid fa-atom text-xs"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xs font-bold tracking-wider text-white flex items-center gap-2">
|
||||
OZONE ADVANCED <span class="text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded font-mono">WEB MASTERING V10.5</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Central Signal Generator & Transport -->
|
||||
<div class="flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800/80">
|
||||
<button id="playAudioBtn" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 transition-all shadow-md shadow-cyan-950">
|
||||
<i class="fa-solid fa-play text-[10px]"></i> <span>Play Reference Loop</span>
|
||||
</button>
|
||||
<button id="stopAudioBtn" class="px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs font-semibold flex items-center gap-1.5 transition-all">
|
||||
<i class="fa-solid fa-stop text-[10px]"></i> <span>Stop</span>
|
||||
</button>
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-800 mx-1"></div>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs font-mono">
|
||||
<span class="text-slate-400 text-[11px]">Preset:</span>
|
||||
<select id="masterPresetSelect" class="bg-slate-900 border border-slate-700 rounded px-2 py-0.5 text-xs text-cyan-300 outline-none focus:border-cyan-500">
|
||||
<option value="adaptive">Adaptive Dynamic Master</option>
|
||||
<option value="edm_club">EDM / Club Punch Maximizer</option>
|
||||
<option value="wide_space">Cinematic Stereo Expansion</option>
|
||||
<option value="transparent">Transparent High-Clarity Limiter</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Target Meter Info -->
|
||||
<div class="flex items-center gap-3 text-xs font-mono">
|
||||
<span class="text-slate-400 text-[11px]">Target LUFS:</span>
|
||||
<span id="targetLufsText" class="text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded">-11.0 LUFS</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MODULE RACK STRIP (OZONE UPPER MODULE BAR) -->
|
||||
<div class="h-16 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-2 overflow-x-auto">
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 font-mono">MODULE CHAIN:</span>
|
||||
|
||||
<!-- Module 1: Dynamic EQ Card -->
|
||||
<div id="cardDynamicEq" class="oz-card oz-card-active w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="toggleEqBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
|
||||
<i class="fa-solid fa-power-off"></i>
|
||||
</button>
|
||||
<div>
|
||||
<div class="text-[11px] font-bold text-slate-200">Dynamic EQ</div>
|
||||
<div class="text-[9px] text-cyan-400 font-mono">4-Band Peak</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa-solid fa-chart-area text-slate-500 text-xs"></i>
|
||||
</div>
|
||||
|
||||
<!-- Module 2: Stereo Imager Card -->
|
||||
<div id="cardImager" class="oz-card w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="toggleImagerBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
|
||||
<i class="fa-solid fa-power-off"></i>
|
||||
</button>
|
||||
<div>
|
||||
<div class="text-[11px] font-bold text-slate-200">Imager</div>
|
||||
<div class="text-[9px] text-slate-400 font-mono">4-Band Width</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa-solid fa-circle-nodes text-slate-500 text-xs"></i>
|
||||
</div>
|
||||
|
||||
<!-- Module 3: Maximizer Card -->
|
||||
<div id="cardMaximizer" class="oz-card w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="toggleMaximizerBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
|
||||
<i class="fa-solid fa-power-off"></i>
|
||||
</button>
|
||||
<div>
|
||||
<div class="text-[11px] font-bold text-slate-200">Maximizer</div>
|
||||
<div class="text-[9px] text-slate-400 font-mono">IRC IV True Peak</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa-solid fa-gauge-high text-slate-500 text-xs"></i>
|
||||
</div>
|
||||
|
||||
<!-- Empty Add Module Slot -->
|
||||
<div class="w-24 h-12 rounded-lg border border-dashed border-slate-800 flex items-center justify-center text-slate-600 hover:text-slate-400 hover:border-slate-600 cursor-pointer transition-all">
|
||||
<i class="fa-solid fa-plus text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN WORKSPACE CONTAINER -->
|
||||
<main class="flex-1 flex overflow-hidden">
|
||||
|
||||
<!-- LEFT MODULE WORKSPACE (DYNAMIC VISUALIZER & CONTROLS) -->
|
||||
<div class="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto">
|
||||
|
||||
<!-- SUB HEADER TOOLBAR -->
|
||||
<div class="h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs font-mono">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-slate-400 flex items-center gap-1.5"><i class="fa-solid fa-headphones text-cyan-400"></i> Delta Listen</span>
|
||||
<select id="modeSelect" class="bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none">
|
||||
<option value="irc4">IRC IV - Classic</option>
|
||||
<option value="irc3">IRC III - Balanced</option>
|
||||
<option value="irc2">IRC II - Crisp</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-[11px]">
|
||||
<span class="text-slate-400">Learn Input Gain:</span>
|
||||
<button id="learnGainBtn" class="bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors">
|
||||
-11.0 LUFS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIEW 1: DYNAMIC EQ VISUALIZER & BAND SETTINGS -->
|
||||
<div id="viewDynamicEq" class="flex-1 flex flex-col p-4 gap-4">
|
||||
<!-- EQ Canvas Display -->
|
||||
<div class="relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair">
|
||||
<canvas id="eqCanvas" class="w-full h-full block"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- EQ Band Control Panel (Ozone Band Settings Window) -->
|
||||
<div class="oz-panel p-3 rounded-xl grid grid-cols-4 gap-3">
|
||||
<!-- Band 1 (Low) -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
|
||||
<div class="flex items-center justify-between text-xs font-bold text-cyan-400">
|
||||
<span>BAND 1 (LOW)</span>
|
||||
<span class="text-[10px] font-mono text-slate-400">100 Hz</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-around my-2">
|
||||
<div class="knob-container" data-param="eqLowGain" data-min="-12" data-max="12" data-value="1.5" data-unit="dB">
|
||||
<div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-cyan-500 relative flex items-center justify-center shadow-md">
|
||||
<div class="knob-pointer w-0.5 h-3 bg-cyan-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center font-mono">
|
||||
<span class="knob-val text-xs text-slate-200 font-bold">+1.5 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[9px] text-slate-500 font-mono text-center">Shelf Filter</div>
|
||||
</div>
|
||||
|
||||
<!-- Band 2 (Mid Low) -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
|
||||
<div class="flex items-center justify-between text-xs font-bold text-amber-400">
|
||||
<span>BAND 2 (MID LOW)</span>
|
||||
<span class="text-[10px] font-mono text-slate-400">822 Hz</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-around my-2">
|
||||
<div class="knob-container" data-param="eqMid1Gain" data-min="-12" data-max="12" data-value="-1.0" data-unit="dB">
|
||||
<div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-amber-500 relative flex items-center justify-center shadow-md">
|
||||
<div class="knob-pointer w-0.5 h-3 bg-amber-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center font-mono">
|
||||
<span class="knob-val text-xs text-slate-200 font-bold">-1.0 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[9px] text-slate-500 font-mono text-center">Dynamic Bell (Q: 0.7)</div>
|
||||
</div>
|
||||
|
||||
<!-- Band 3 (Mid High) -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
|
||||
<div class="flex items-center justify-between text-xs font-bold text-purple-400">
|
||||
<span>BAND 3 (MID HIGH)</span>
|
||||
<span class="text-[10px] font-mono text-slate-400">3.2 kHz</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-around my-2">
|
||||
<div class="knob-container" data-param="eqMid2Gain" data-min="-12" data-max="12" data-value="2.0" data-unit="dB">
|
||||
<div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-purple-500 relative flex items-center justify-center shadow-md">
|
||||
<div class="knob-pointer w-0.5 h-3 bg-purple-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center font-mono">
|
||||
<span class="knob-val text-xs text-slate-200 font-bold">+2.0 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[9px] text-slate-500 font-mono text-center">Dynamic Bell (Q: 1.2)</div>
|
||||
</div>
|
||||
|
||||
<!-- Band 4 (High) -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
|
||||
<div class="flex items-center justify-between text-xs font-bold text-emerald-400">
|
||||
<span>BAND 4 (HIGH)</span>
|
||||
<span class="text-[10px] font-mono text-slate-400">10 kHz</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-around my-2">
|
||||
<div class="knob-container" data-param="eqHighGain" data-min="-12" data-max="12" data-value="1.8" data-unit="dB">
|
||||
<div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-emerald-500 relative flex items-center justify-center shadow-md">
|
||||
<div class="knob-pointer w-0.5 h-3 bg-emerald-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center font-mono">
|
||||
<span class="knob-val text-xs text-slate-200 font-bold">+1.8 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[9px] text-slate-500 font-mono text-center">High Shelf</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIEW 2: STEREO IMAGER VISUALIZER & CONTROLS (HIDDEN BY DEFAULT) -->
|
||||
<div id="viewImager" class="flex-1 flex flex-col p-4 gap-4 hidden">
|
||||
<div class="grid grid-cols-12 gap-4 flex-1">
|
||||
<!-- Polar Vectorscope Canvas (8 Cols) -->
|
||||
<div class="col-span-8 bg-slate-950 border border-slate-800/90 rounded-xl p-3 flex flex-col relative shadow-inner">
|
||||
<span class="text-xs font-bold text-cyan-400 uppercase font-mono mb-2">
|
||||
<i class="fa-solid fa-circle-nodes"></i> Polar Vectorscope & Correlation Meter
|
||||
</span>
|
||||
<div class="flex-1 relative w-full h-56 flex items-center justify-center">
|
||||
<canvas id="imagerCanvas" class="w-full h-full block"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Band Width Controls (4 Cols) -->
|
||||
<div class="col-span-4 oz-panel p-3 rounded-xl flex flex-col justify-between">
|
||||
<span class="text-xs font-bold text-slate-300 uppercase font-mono">4-Band Stereo Width</span>
|
||||
|
||||
<div class="space-y-3 my-auto">
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-cyan-400 font-bold">Band 1 (0-100Hz)</span><span id="w1Val">0%</span></div>
|
||||
<input id="w1Slider" type="range" min="-100" max="100" value="0" class="w-full accent-cyan-500 h-1 cursor-pointer">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-amber-400 font-bold">Band 2 (100-1kHz)</span><span id="w2Val">+15%</span></div>
|
||||
<input id="w2Slider" type="range" min="-100" max="100" value="15" class="w-full accent-amber-500 h-1 cursor-pointer">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-purple-400 font-bold">Band 3 (1k-6kHz)</span><span id="w3Val">+35%</span></div>
|
||||
<input id="w3Slider" type="range" min="-100" max="100" value="35" class="w-full accent-purple-500 h-1 cursor-pointer">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-emerald-400 font-bold">Band 4 (6k-20kHz)</span><span id="w4Val">+50%</span></div>
|
||||
<input id="w4Slider" type="range" min="-100" max="100" value="50" class="w-full accent-emerald-500 h-1 cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIEW 3: MAXIMIZER / LIMITER VISUALIZER & CONTROLS (HIDDEN BY DEFAULT) -->
|
||||
<div id="viewMaximizer" class="flex-1 flex flex-col p-4 gap-4 hidden">
|
||||
<div class="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
|
||||
|
||||
<!-- Left Gain & Output Level Panel -->
|
||||
<div class="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
|
||||
<span class="text-xs font-bold text-slate-400 uppercase font-mono mb-3">Maximizer Gain Boost</span>
|
||||
|
||||
<div class="knob-container my-2" data-param="maxGain" data-min="0" data-max="12" data-value="5.4" data-unit="dB">
|
||||
<div class="knob-dial w-24 h-24 rounded-full bg-slate-900 border-4 border-cyan-500 relative flex items-center justify-center shadow-2xl">
|
||||
<div class="knob-pointer w-1 h-8 bg-cyan-400 rounded absolute top-2"></div>
|
||||
<span class="knob-val text-sm font-bold font-mono text-cyan-300 z-10">+5.4 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs font-mono flex justify-between items-center">
|
||||
<span class="text-slate-400">Ceiling Level:</span>
|
||||
<span class="text-cyan-400 font-bold">-0.10 dB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Upward Comp & Soft Clip Controls -->
|
||||
<div class="col-span-8 grid grid-cols-3 gap-4">
|
||||
<div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 font-mono mb-2">UPWARD COMPRESS</span>
|
||||
<div class="knob-container" data-param="maxUpward" data-min="0" data-max="10" data-value="2.0" data-unit="dB">
|
||||
<div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-cyan-500 relative flex items-center justify-center">
|
||||
<div class="knob-pointer w-0.5 h-4 bg-cyan-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="knob-val text-xs font-mono text-slate-200 mt-2">+2.0 dB</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 font-mono mb-2">SOFT CLIPPER</span>
|
||||
<div class="knob-container" data-param="maxSoftClip" data-min="0" data-max="100" data-value="15" data-unit="%">
|
||||
<div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-amber-500 relative flex items-center justify-center">
|
||||
<div class="knob-pointer w-0.5 h-4 bg-amber-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="knob-val text-xs font-mono text-slate-200 mt-2">15%</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 font-mono mb-2">TRANSIENT EMPHASIS</span>
|
||||
<div class="knob-container" data-param="maxTransient" data-min="0" data-max="100" data-value="25" data-unit="%">
|
||||
<div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-emerald-500 relative flex items-center justify-center">
|
||||
<div class="knob-pointer w-0.5 h-4 bg-emerald-400 rounded absolute top-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="knob-val text-xs font-mono text-slate-200 mt-2">25%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDEBAR: MASTER INPUT / OUTPUT STEREO METER TOWER -->
|
||||
<div class="w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl font-mono text-xs z-20">
|
||||
<div class="flex items-center justify-between border-b border-slate-800 pb-2 mb-2">
|
||||
<span class="text-xs font-bold text-white tracking-wider uppercase flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-sliders text-cyan-400"></i> I/O METERS
|
||||
</span>
|
||||
<span class="text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-300 px-1 py-0.5 rounded">TRUE PEAK</span>
|
||||
</div>
|
||||
|
||||
<!-- Peak / RMS Numeric Display -->
|
||||
<div class="grid grid-cols-2 gap-2 text-[10px] text-center mb-2">
|
||||
<div class="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
|
||||
<div class="text-slate-500 font-bold">IN PEAK</div>
|
||||
<div id="inPeakText" class="text-cyan-400 font-bold font-mono">-14.2 dB</div>
|
||||
</div>
|
||||
<div class="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
|
||||
<div class="text-slate-500 font-bold">OUT PEAK</div>
|
||||
<div id="outPeakText" class="text-emerald-400 font-bold font-mono">-0.1 dB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Level Meters Graphic Container -->
|
||||
<div class="flex-1 bg-slate-950 border border-slate-800/90 rounded-xl p-2 flex justify-around relative overflow-hidden my-1">
|
||||
<!-- Input Meters (L/R) -->
|
||||
<div class="flex flex-col items-center h-full">
|
||||
<span class="text-[9px] text-slate-500 mb-1">IN</span>
|
||||
<div class="flex-1 w-5 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
|
||||
<canvas id="inMeterCanvas" class="w-full h-full block"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Meters (L/R) -->
|
||||
<div class="flex flex-col items-center h-full">
|
||||
<span class="text-[9px] text-slate-500 mb-1">OUT</span>
|
||||
<div class="flex-1 w-6 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
|
||||
<canvas id="outMeterCanvas" class="w-full h-full block"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Utility Buttons (Ozone Right Side Action Bar) -->
|
||||
<div class="grid grid-cols-2 gap-1.5 mt-2">
|
||||
<button id="masterBypassBtn" class="bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors">
|
||||
Bypass
|
||||
</button>
|
||||
<button class="bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors">
|
||||
Gain Match
|
||||
</button>
|
||||
<button class="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] font-sans border border-slate-700">
|
||||
Codec
|
||||
</button>
|
||||
<button class="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] font-sans border border-slate-700">
|
||||
Dither
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- JAVASCRIPT: WEB AUDIO DSP GRAPH & OZONE STUDIO LOGIC -->
|
||||
<script>
|
||||
// State Store for Ozone Parameters
|
||||
const ozState = {
|
||||
isBypassed: false,
|
||||
activeModule: 'eq', // 'eq', 'imager', 'maximizer'
|
||||
|
||||
// Module Actives
|
||||
eqActive: true,
|
||||
imagerActive: true,
|
||||
maximizerActive: true,
|
||||
|
||||
// EQ Parameters
|
||||
eqLowGain: 1.5,
|
||||
eqMid1Gain: -1.0,
|
||||
eqMid2Gain: 2.0,
|
||||
eqHighGain: 1.8,
|
||||
|
||||
// Imager Band Widths
|
||||
w1: 0,
|
||||
w2: 15,
|
||||
w3: 35,
|
||||
w4: 50,
|
||||
|
||||
// Maximizer Parameters
|
||||
maxGain: 5.4,
|
||||
maxUpward: 2.0,
|
||||
maxSoftClip: 15,
|
||||
maxTransient: 25
|
||||
};
|
||||
|
||||
// Web Audio Node Graph References
|
||||
let audioCtx = null;
|
||||
let sourceNode = null;
|
||||
let inputAnalyser = null;
|
||||
let eqLowFilter = null;
|
||||
let eqMid1Filter = null;
|
||||
let eqMid2Filter = null;
|
||||
let eqHighFilter = null;
|
||||
let maximizerBoostGain = null;
|
||||
let maximizerCompressor = null;
|
||||
let outputAnalyser = null;
|
||||
let isPlaying = false;
|
||||
|
||||
// Web Audio Initialization
|
||||
function initOzoneAudioEngine() {
|
||||
if (audioCtx) return;
|
||||
|
||||
const AudioCtxClass = window.AudioContext || window.webkitAudioContext;
|
||||
audioCtx = new AudioCtxClass();
|
||||
|
||||
// Input Analyser
|
||||
inputAnalyser = audioCtx.createAnalyser();
|
||||
inputAnalyser.fftSize = 2048;
|
||||
|
||||
// Dynamic EQ Filters
|
||||
eqLowFilter = audioCtx.createBiquadFilter();
|
||||
eqLowFilter.type = 'lowshelf';
|
||||
eqLowFilter.frequency.value = 100;
|
||||
|
||||
eqMid1Filter = audioCtx.createBiquadFilter();
|
||||
eqMid1Filter.type = 'peaking';
|
||||
eqMid1Filter.frequency.value = 822;
|
||||
eqMid1Filter.Q.value = 0.7;
|
||||
|
||||
eqMid2Filter = audioCtx.createBiquadFilter();
|
||||
eqMid2Filter.type = 'peaking';
|
||||
eqMid2Filter.frequency.value = 3200;
|
||||
eqMid2Filter.Q.value = 1.2;
|
||||
|
||||
eqHighFilter = audioCtx.createBiquadFilter();
|
||||
eqHighFilter.type = 'highshelf';
|
||||
eqHighFilter.frequency.value = 10000;
|
||||
|
||||
// Maximizer Boost Gain
|
||||
maximizerBoostGain = audioCtx.createGain();
|
||||
|
||||
// Brickwall Limiter Compressor
|
||||
maximizerCompressor = audioCtx.createDynamicsCompressor();
|
||||
maximizerCompressor.threshold.value = -0.1;
|
||||
maximizerCompressor.knee.value = 0.0;
|
||||
maximizerCompressor.ratio.value = 20.0;
|
||||
maximizerCompressor.attack.value = 0.001;
|
||||
maximizerCompressor.release.value = 0.05;
|
||||
|
||||
// Output Analyser
|
||||
outputAnalyser = audioCtx.createAnalyser();
|
||||
outputAnalyser.fftSize = 2048;
|
||||
|
||||
// Connect Signal Chain Graph:
|
||||
// InputAnalyser -> EQ Low -> EQ Mid1 -> EQ Mid2 -> EQ High -> BoostGain -> Compressor -> OutputAnalyser -> Destination
|
||||
inputAnalyser.connect(eqLowFilter);
|
||||
eqLowFilter.connect(eqMid1Filter);
|
||||
eqMid1Filter.connect(eqMid2Filter);
|
||||
eqMid2Filter.connect(eqHighFilter);
|
||||
eqHighFilter.connect(maximizerBoostGain);
|
||||
maximizerBoostGain.connect(maximizerCompressor);
|
||||
maximizerCompressor.connect(outputAnalyser);
|
||||
outputAnalyser.connect(audioCtx.destination);
|
||||
|
||||
updateAudioGraphValues();
|
||||
}
|
||||
|
||||
function updateAudioGraphValues() {
|
||||
if (!audioCtx) return;
|
||||
const now = audioCtx.currentTime;
|
||||
|
||||
eqLowFilter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqLowGain : 0, now, 0.01);
|
||||
eqMid1Filter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqMid1Gain : 0, now, 0.01);
|
||||
eqMid2Filter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqMid2Gain : 0, now, 0.01);
|
||||
eqHighFilter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqHighGain : 0, now, 0.01);
|
||||
|
||||
const boostLinear = ozState.maximizerActive ? Math.pow(10, ozState.maxGain / 20) : 1.0;
|
||||
maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);
|
||||
}
|
||||
|
||||
// Synthesize EDM / Pop Reference Audio Loop
|
||||
function startAudioDemo() {
|
||||
initOzoneAudioEngine();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
stopAudioDemo();
|
||||
|
||||
const sampleRate = audioCtx.sampleRate;
|
||||
const bufferSize = sampleRate * 4; // 4 second loop
|
||||
const buffer = audioCtx.createBuffer(2, bufferSize, sampleRate);
|
||||
const left = buffer.getChannelData(0);
|
||||
const right = buffer.getChannelData(1);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
const t = i / sampleRate;
|
||||
// Kick Drum
|
||||
const kickEnv = Math.max(0, 1 - (t % 0.5) * 8);
|
||||
const kick = Math.sin(2 * Math.PI * (55 * Math.exp(-(t % 0.5) * 18))) * kickEnv;
|
||||
|
||||
// Snare / Claps
|
||||
const snareEnv = Math.max(0, 1 - ((t + 0.25) % 0.5) * 6);
|
||||
const snare = (Math.random() * 2 - 1) * snareEnv * 0.3;
|
||||
|
||||
// EDM Chords (C min, Ab, Eb, Bb)
|
||||
const synth = (Math.sin(2 * Math.PI * 261.63 * t) + Math.sin(2 * Math.PI * 311.13 * t) + Math.sin(2 * Math.PI * 392.0 * t)) * 0.12;
|
||||
|
||||
left[i] = kick * 0.6 + snare + synth;
|
||||
right[i] = kick * 0.6 + snare * 0.9 + synth * 0.95;
|
||||
}
|
||||
|
||||
sourceNode = audioCtx.createBufferSource();
|
||||
sourceNode.buffer = buffer;
|
||||
sourceNode.loop = true;
|
||||
sourceNode.connect(inputAnalyser);
|
||||
sourceNode.start();
|
||||
|
||||
isPlaying = true;
|
||||
document.getElementById('playAudioBtn').innerHTML = '<i class="fa-solid fa-pause text-[10px]"></i> <span>Pause Reference</span>';
|
||||
}
|
||||
|
||||
function stopAudioDemo() {
|
||||
if (sourceNode) {
|
||||
try { sourceNode.stop(); } catch(e){}
|
||||
sourceNode.disconnect();
|
||||
sourceNode = null;
|
||||
}
|
||||
isPlaying = false;
|
||||
document.getElementById('playAudioBtn').innerHTML = '<i class="fa-solid fa-play text-[10px]"></i> <span>Play Reference Loop</span>';
|
||||
}
|
||||
|
||||
// Canvas Render Loops
|
||||
function startCanvasRenderers() {
|
||||
const eqCanvas = document.getElementById('eqCanvas');
|
||||
const imagerCanvas = document.getElementById('imagerCanvas');
|
||||
const inMeterCanvas = document.getElementById('inMeterCanvas');
|
||||
const outMeterCanvas = document.getElementById('outMeterCanvas');
|
||||
|
||||
const eqCtx = eqCanvas.getContext('2d');
|
||||
const imagerCtx = imagerCanvas.getContext('2d');
|
||||
const inCtx = inMeterCanvas.getContext('2d');
|
||||
const outCtx = outMeterCanvas.getContext('2d');
|
||||
|
||||
function resizeAll() {
|
||||
eqCanvas.width = eqCanvas.clientWidth;
|
||||
eqCanvas.height = eqCanvas.clientHeight;
|
||||
imagerCanvas.width = imagerCanvas.clientWidth;
|
||||
imagerCanvas.height = imagerCanvas.clientHeight;
|
||||
inMeterCanvas.width = inMeterCanvas.clientWidth;
|
||||
inMeterCanvas.height = inMeterCanvas.clientHeight;
|
||||
outMeterCanvas.width = outMeterCanvas.clientWidth;
|
||||
outMeterCanvas.height = outMeterCanvas.clientHeight;
|
||||
}
|
||||
resizeAll();
|
||||
window.addEventListener('resize', resizeAll);
|
||||
|
||||
const fftData = new Uint8Array(1024);
|
||||
|
||||
function renderFrame() {
|
||||
requestAnimationFrame(renderFrame);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 1. RENDER DYNAMIC EQ & SPECTRUM
|
||||
// -------------------------------------------------------------
|
||||
const w = eqCanvas.width;
|
||||
const h = eqCanvas.height;
|
||||
eqCtx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw Ozone-style Frequency Grid Lines
|
||||
eqCtx.strokeStyle = 'rgba(51, 65, 85, 0.3)';
|
||||
eqCtx.lineWidth = 1;
|
||||
eqCtx.font = '9px JetBrains Mono';
|
||||
eqCtx.fillStyle = '#475569';
|
||||
|
||||
const freqs = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
|
||||
freqs.forEach(f => {
|
||||
const x = (Math.log10(f / 20) / Math.log10(20000 / 20)) * w;
|
||||
eqCtx.beginPath();
|
||||
eqCtx.moveTo(x, 0);
|
||||
eqCtx.lineTo(x, h);
|
||||
eqCtx.stroke();
|
||||
if (f >= 1000) eqCtx.fillText(`${f/1000}k`, x + 3, h - 6);
|
||||
else eqCtx.fillText(`${f}`, x + 3, h - 6);
|
||||
});
|
||||
|
||||
// Draw FFT Spectrum Fill
|
||||
if (outputAnalyser && isPlaying) {
|
||||
outputAnalyser.getByteFrequencyData(fftData);
|
||||
eqCtx.fillStyle = 'rgba(56, 189, 248, 0.15)';
|
||||
const barWidth = w / 128;
|
||||
for (let i = 0; i < 128; i++) {
|
||||
const val = fftData[i * 4] / 255;
|
||||
const barHeight = val * h;
|
||||
eqCtx.fillRect(i * barWidth, h - barHeight, barWidth - 1, barHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Composite EQ Response Curve
|
||||
eqCtx.strokeStyle = '#38bdf8';
|
||||
eqCtx.lineWidth = 2.5;
|
||||
eqCtx.beginPath();
|
||||
|
||||
for (let x = 0; x < w; x++) {
|
||||
const freq = 20 * Math.pow(20000 / 20, x / w);
|
||||
let gainDb = 0;
|
||||
|
||||
if (ozState.eqActive) {
|
||||
// Low Shelf
|
||||
gainDb += ozState.eqLowGain / (1 + Math.pow(freq / 100, 2));
|
||||
// Mid 1 Bell
|
||||
gainDb += ozState.eqMid1Gain * Math.exp(-Math.pow(Math.log(freq / 822), 2) * 2);
|
||||
// Mid 2 Bell
|
||||
gainDb += ozState.eqMid2Gain * Math.exp(-Math.pow(Math.log(freq / 3200), 2) * 2);
|
||||
// High Shelf
|
||||
gainDb += ozState.eqHighGain / (1 + Math.pow(10000 / freq, 2));
|
||||
}
|
||||
|
||||
const y = h / 2 - (gainDb / 18) * (h / 2);
|
||||
if (x === 0) eqCtx.moveTo(x, y);
|
||||
else eqCtx.lineTo(x, y);
|
||||
}
|
||||
eqCtx.stroke();
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 2. RENDER IMAGER POLAR VECTORSCOPE
|
||||
// -------------------------------------------------------------
|
||||
const iw = imagerCanvas.width;
|
||||
const ih = imagerCanvas.height;
|
||||
imagerCtx.clearRect(0, 0, iw, ih);
|
||||
|
||||
// Scope Polar Circle Guides
|
||||
imagerCtx.strokeStyle = 'rgba(51, 65, 85, 0.4)';
|
||||
imagerCtx.lineWidth = 1;
|
||||
imagerCtx.beginPath();
|
||||
imagerCtx.arc(iw / 2, ih / 2, ih / 3, 0, Math.PI * 2);
|
||||
imagerCtx.stroke();
|
||||
|
||||
// Scope Dynamic Stereo Dots Animation
|
||||
if (isPlaying) {
|
||||
imagerCtx.fillStyle = '#38bdf8';
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const angle = (Math.random() - 0.5) * (Math.PI / 2) + (-Math.PI / 2);
|
||||
const radius = (Math.random() * (ih / 3.2));
|
||||
const x = iw / 2 + Math.cos(angle) * radius * (1 + ozState.w3 / 100);
|
||||
const y = ih / 2 + Math.sin(angle) * radius;
|
||||
imagerCtx.fillRect(x, y, 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 3. RENDER I/O STEREO METER TOWERS
|
||||
// -------------------------------------------------------------
|
||||
renderMeterBar(inCtx, inMeterCanvas.width, inMeterCanvas.height, isPlaying ? 0.55 : 0.05, 'inPeakText');
|
||||
renderMeterBar(outCtx, outMeterCanvas.width, outMeterCanvas.height, isPlaying ? 0.85 : 0.05, 'outPeakText');
|
||||
}
|
||||
|
||||
function renderMeterBar(ctx, w, h, level, textId) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const barH = level * h;
|
||||
|
||||
const grad = ctx.createLinearGradient(0, h, 0, 0);
|
||||
grad.addColorStop(0, '#38bdf8');
|
||||
grad.addColorStop(0.7, '#f59e0b');
|
||||
grad.addColorStop(1, '#ef4444');
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(2, h - barH, w - 4, barH);
|
||||
|
||||
const dbVal = (20 * Math.log10(level)).toFixed(1);
|
||||
document.getElementById(textId).innerText = isPlaying ? `${dbVal} dB` : '-inf dB';
|
||||
}
|
||||
|
||||
renderFrame();
|
||||
}
|
||||
|
||||
// Rotary Knob Drag Interactions
|
||||
function setupKnobs() {
|
||||
document.querySelectorAll('.knob-container').forEach(knob => {
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startVal = parseFloat(knob.dataset.value);
|
||||
|
||||
const param = knob.dataset.param;
|
||||
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.parentElement.querySelector('.knob-val');
|
||||
|
||||
function updateUI(val) {
|
||||
const pct = (val - min) / (max - min);
|
||||
const angle = -135 + pct * 270;
|
||||
dial.style.transform = `rotate(${angle}deg)`;
|
||||
|
||||
if (valText) {
|
||||
valText.innerText = `${val > 0 && unit==='dB' ? '+' : ''}${val.toFixed(1)} ${unit}`;
|
||||
}
|
||||
|
||||
ozState[param] = val;
|
||||
updateAudioGraphValues();
|
||||
}
|
||||
|
||||
updateUI(startVal);
|
||||
|
||||
knob.addEventListener('pointerdown', e => {
|
||||
isDragging = true;
|
||||
startY = e.clientY;
|
||||
startVal = ozState[param];
|
||||
knob.setPointerCapture(e.pointerId);
|
||||
});
|
||||
|
||||
knob.addEventListener('pointermove', e => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.clientY;
|
||||
const range = max - min;
|
||||
let newVal = startVal + (deltaY / 100) * range;
|
||||
newVal = Math.min(max, Math.max(min, newVal));
|
||||
updateUI(newVal);
|
||||
});
|
||||
|
||||
knob.addEventListener('pointerup', e => {
|
||||
isDragging = false;
|
||||
knob.releasePointerCapture(e.pointerId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Module Switcher Navigation Logic
|
||||
function setupModuleNavigation() {
|
||||
const cardEq = document.getElementById('cardDynamicEq');
|
||||
const cardImager = document.getElementById('cardImager');
|
||||
const cardMaximizer = document.getElementById('cardMaximizer');
|
||||
|
||||
const viewEq = document.getElementById('viewDynamicEq');
|
||||
const viewImager = document.getElementById('viewImager');
|
||||
const viewMaximizer = document.getElementById('viewMaximizer');
|
||||
|
||||
function switchModule(activeCard, activeView, name) {
|
||||
[cardEq, cardImager, cardMaximizer].forEach(c => c.classList.remove('oz-card-active'));
|
||||
[viewEq, viewImager, viewMaximizer].forEach(v => v.classList.add('hidden'));
|
||||
|
||||
activeCard.classList.add('oz-card-active');
|
||||
activeView.classList.remove('hidden');
|
||||
ozState.activeModule = name;
|
||||
}
|
||||
|
||||
cardEq.addEventListener('click', () => switchModule(cardEq, viewEq, 'eq'));
|
||||
cardImager.addEventListener('click', () => switchModule(cardImager, viewImager, 'imager'));
|
||||
cardMaximizer.addEventListener('click', () => switchModule(cardMaximizer, viewMaximizer, 'maximizer'));
|
||||
}
|
||||
|
||||
// DOM Load Init
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('playAudioBtn').addEventListener('click', () => {
|
||||
if (isPlaying) stopAudioDemo();
|
||||
else startAudioDemo();
|
||||
});
|
||||
|
||||
document.getElementById('stopAudioBtn').addEventListener('click', stopAudioDemo);
|
||||
|
||||
setupKnobs();
|
||||
setupModuleNavigation();
|
||||
startCanvasRenderers();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
Generated
+523
-1
@@ -8,7 +8,41 @@
|
||||
"dependencies": {
|
||||
"@babel/cli": "^8.0.4",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-react": "^8.0.1"
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"jsdom": "^30.0.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz",
|
||||
"integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.2.1",
|
||||
"@csstools/css-color-parser": "^4.1.9",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz",
|
||||
"integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
@@ -366,6 +400,169 @@
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
|
||||
"integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz",
|
||||
"integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.0",
|
||||
"@csstools/css-calc": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
|
||||
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -434,6 +631,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
@@ -529,6 +735,52 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.393",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz",
|
||||
@@ -544,6 +796,18 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -579,6 +843,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-meta-resolve": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
|
||||
@@ -589,12 +865,58 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz",
|
||||
"integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^6.0.5",
|
||||
"@asamuzakjp/dom-selector": "^8.3.0",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.7",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.9.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.2.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -628,6 +950,12 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
@@ -674,6 +1002,18 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
@@ -696,6 +1036,36 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
@@ -709,6 +1079,33 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
@@ -733,6 +1130,72 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz",
|
||||
"integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.9"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz",
|
||||
"integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
@@ -762,6 +1225,65 @@
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -7,6 +7,9 @@
|
||||
"dependencies": {
|
||||
"@babel/cli": "^8.0.4",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-react": "^8.0.1"
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"jsdom": "^30.0.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
import os
|
||||
import app.api.v1.user_config as user_config
|
||||
user_config.DATA_FILE = os.path.join(os.path.dirname(__file__), "user_configs_test.json")
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
|
||||
Reference in New Issue
Block a user