feat: update
This commit is contained in:
+24
-24
@@ -1,24 +1,24 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"tabWidth": 2
|
"tabWidth": 2
|
||||||
}
|
}
|
||||||
Vendored
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Tauri + React + Typescript
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
||||||
- Check render to improve perfoormances
|
- Check render to improve perfoormances
|
||||||
+14
-14
@@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + React + Typescript</title>
|
<title>Tauri + React + Typescript</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+3131
-3131
File diff suppressed because it is too large
Load Diff
+31
-31
@@ -1,31 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "thomann-dsp-app",
|
"name": "thomann-dsp-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css}\"",
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css}\"",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css}\""
|
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
# Generated by Tauri
|
# Generated by Tauri
|
||||||
# will have schema files for capabilities auto-completion
|
# will have schema files for capabilities auto-completion
|
||||||
/gen/schemas
|
/gen/schemas
|
||||||
|
|||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
thomann_dsp_app_lib::run()
|
thomann_dsp_app_lib::run()
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-37
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "thomann-dsp-app",
|
"productName": "thomann-dsp-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.lucas.thomann-dsp-app",
|
"identifier": "com.lucas.thomann-dsp-app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "npm run build",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "thomann-dsp-app",
|
"title": "thomann-dsp-app",
|
||||||
"width": 1600,
|
"width": 1600,
|
||||||
"height": 950,
|
"height": 950,
|
||||||
"minWidth": 1200,
|
"minWidth": 1200,
|
||||||
"minHeight": 600
|
"minHeight": 600
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--header-height);
|
|
||||||
background: var(--bg-void);
|
|
||||||
border-bottom: 1px solid var(--border-hairline);
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
padding-inline: var(--space-3);
|
|
||||||
gap: var(--space-3);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-slot {
|
|
||||||
display: flex;
|
|
||||||
height: 85%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabstrip {
|
|
||||||
display: flex;
|
|
||||||
height: var(--header-height);
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
min-width: 0;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
border-left: 1px solid var(--border-hairline);
|
|
||||||
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--border-hairline) transparent;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
scroll-padding-inline: var(--space-3);
|
|
||||||
scroll-snap-type: x proximity;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-4);
|
|
||||||
padding-left: var(--space-4);
|
|
||||||
|
|
||||||
container-type: inline-size;
|
|
||||||
container-name: tabstrip;
|
|
||||||
|
|
||||||
-webkit-mask-image: linear-gradient(to right, transparent, black 0);
|
|
||||||
mask-image: linear-gradient(to right, transparent, black 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabstrip-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: var(--header-height);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
font: inherit;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: var(--border-width-active) solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-action:hover {
|
|
||||||
background: var(--bg-raised);
|
|
||||||
border-color: var(--accent-brand);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-add {
|
|
||||||
height: 70%;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--text-muted);
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crown-action {
|
|
||||||
height: 70%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #facc15;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crown-action:hover {
|
|
||||||
color: #fde047;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-crown,
|
|
||||||
.icon-tab-add,
|
|
||||||
.icon-tab-add-empty {
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
height: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-add-empty {
|
|
||||||
height: 70%;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
border: var(--border-width-active) dashed var(--border-hairline);
|
|
||||||
color: var(--text-muted);
|
|
||||||
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding-inline: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-add-empty:hover {
|
|
||||||
border-color: var(--accent-brand);
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab */
|
|
||||||
.tab {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
height: var(--tab-height);
|
|
||||||
padding: 0 var(--space-3);
|
|
||||||
border: var(--border-width-active) solid var(--border-hairline);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
scroll-snap-align: start;
|
|
||||||
|
|
||||||
flex: 0 0 25%;
|
|
||||||
max-width: 25%;
|
|
||||||
min-width: calc(
|
|
||||||
(var(--space-3) * 2) + (var(--tab-height) * 0.5) + (var(--space-2) * 2) + 4ch +
|
|
||||||
var(--tab-close-size)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab:hover {
|
|
||||||
background: var(--bg-raised);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--accent-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-name {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-led {
|
|
||||||
height: 50%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-close {
|
|
||||||
height: 50%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 0;
|
|
||||||
font-size: var(--tab-close-font);
|
|
||||||
color: var(--text-muted);
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab--active {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border-color: var(--accent-brand);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab--dimmed {
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-close:hover {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabstrip[data-fade-start='true'] {
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
black 24px,
|
|
||||||
black calc(100% - 24px),
|
|
||||||
black
|
|
||||||
);
|
|
||||||
mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), black);
|
|
||||||
}
|
|
||||||
.tabstrip[data-fade-end='true'] {
|
|
||||||
-webkit-mask-image: linear-gradient(to right, black, black calc(100% - 24px), transparent);
|
|
||||||
mask-image: linear-gradient(to right, black, black calc(100% - 24px), transparent);
|
|
||||||
}
|
|
||||||
.tabstrip[data-fade-start='true'][data-fade-end='true'] {
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
black 24px,
|
|
||||||
black calc(100% - 24px),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
black 24px,
|
|
||||||
black calc(100% - 24px),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab[data-step='6'],
|
|
||||||
.tab[data-step='8'],
|
|
||||||
.tab[data-step='10'] {
|
|
||||||
min-width: calc(
|
|
||||||
(var(--space-3) * 2) + (var(--tab-height) * 0.5) + (var(--space-2) * 2) + 4ch +
|
|
||||||
var(--tab-close-size)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import { DSP, AppPage } from '../../types';
|
|
||||||
import AnimatedLogo from '../../assets/icons/AnimatedLogo';
|
|
||||||
import { PlusIcon } from '../../assets/icons/PlusIcon';
|
|
||||||
import { CrownIcon } from '../../assets/icons/CrownIcon';
|
|
||||||
import Led from '../Led/Led';
|
|
||||||
import { CloseIcon } from '../../assets/icons/CloseIcon';
|
|
||||||
|
|
||||||
import './Topbar.css';
|
|
||||||
|
|
||||||
function getStep(count: number) {
|
|
||||||
if (count <= 4) return 4;
|
|
||||||
if (count <= 6) return 6;
|
|
||||||
if (count <= 8) return 8;
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tab({
|
|
||||||
dsp,
|
|
||||||
active,
|
|
||||||
dimmed,
|
|
||||||
step,
|
|
||||||
onSelect,
|
|
||||||
onRemove,
|
|
||||||
onConnect,
|
|
||||||
onDisconnect,
|
|
||||||
}: {
|
|
||||||
dsp: DSP;
|
|
||||||
active: boolean;
|
|
||||||
dimmed: boolean;
|
|
||||||
step: number;
|
|
||||||
onSelect: () => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
onConnect: (dsp: DSP) => void;
|
|
||||||
onDisconnect: (dsp: DSP) => void;
|
|
||||||
}) {
|
|
||||||
const isConnected = dsp.status === 'connected';
|
|
||||||
|
|
||||||
const handleTabKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
onSelect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={['tab', active && 'tab--active', dimmed && 'tab--dimmed']
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
data-step={step}
|
|
||||||
tabIndex={0}
|
|
||||||
role="tab"
|
|
||||||
aria-selected={active}
|
|
||||||
onClick={onSelect}
|
|
||||||
onKeyDown={handleTabKeyDown}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="tab-led"
|
|
||||||
aria-label={isConnected ? `Disconnect ${dsp.name}` : `Connect ${dsp.name}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
onDisconnect(dsp);
|
|
||||||
} else {
|
|
||||||
onConnect(dsp);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Led status={dsp.status} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span className="tab-name" title={dsp.name}>
|
|
||||||
{dsp.name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="tab-close"
|
|
||||||
aria-label={`Remove ${dsp.name}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TopBar({
|
|
||||||
dsps,
|
|
||||||
selected,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
setSelect,
|
|
||||||
onSelect,
|
|
||||||
onRemove,
|
|
||||||
onAddClick,
|
|
||||||
onConnect,
|
|
||||||
onDisconnect,
|
|
||||||
}: {
|
|
||||||
dsps: DSP[];
|
|
||||||
selected: number | null;
|
|
||||||
page: AppPage;
|
|
||||||
setPage: (page: AppPage) => void;
|
|
||||||
setSelect: (select: number | null) => void;
|
|
||||||
onSelect: (id: number) => void;
|
|
||||||
onRemove: (dsp: DSP) => void;
|
|
||||||
onAddClick: () => void;
|
|
||||||
onConnect: (dsp: DSP) => void;
|
|
||||||
onDisconnect: (dsp: DSP) => void;
|
|
||||||
}) {
|
|
||||||
const hasTabs = dsps.length > 0;
|
|
||||||
const tabstripRef = useRef<HTMLDivElement>(null);
|
|
||||||
const prevCount = useRef(dsps.length);
|
|
||||||
const step = getStep(dsps.length);
|
|
||||||
|
|
||||||
const updateFade = () => {
|
|
||||||
const el = tabstripRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const atStart = el.scrollLeft <= 1;
|
|
||||||
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
||||||
el.dataset.fadeStart = String(!atStart);
|
|
||||||
el.dataset.fadeEnd = String(!atEnd && el.scrollWidth > el.clientWidth);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = tabstripRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const onWheel = (e: WheelEvent) => {
|
|
||||||
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
||||||
el.scrollLeft += e.deltaY;
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
el.addEventListener('wheel', onWheel, { passive: false });
|
|
||||||
return () => el.removeEventListener('wheel', onWheel);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFade();
|
|
||||||
const el = tabstripRef.current;
|
|
||||||
el?.addEventListener('scroll', updateFade);
|
|
||||||
window.addEventListener('resize', updateFade);
|
|
||||||
return () => {
|
|
||||||
el?.removeEventListener('scroll', updateFade);
|
|
||||||
window.removeEventListener('resize', updateFade);
|
|
||||||
};
|
|
||||||
}, [dsps.length]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dsps.length > prevCount.current && tabstripRef.current) {
|
|
||||||
tabstripRef.current.scrollTo({
|
|
||||||
left: tabstripRef.current.scrollWidth,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
prevCount.current = dsps.length;
|
|
||||||
}, [dsps.length]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="topbar">
|
|
||||||
<button
|
|
||||||
className="logo-slot"
|
|
||||||
onClick={() => {
|
|
||||||
setSelect(null);
|
|
||||||
setPage('home');
|
|
||||||
}}
|
|
||||||
aria-label="Go to home"
|
|
||||||
>
|
|
||||||
<AnimatedLogo />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="tabstrip" ref={tabstripRef}>
|
|
||||||
{dsps.map((dsp) => (
|
|
||||||
<Tab
|
|
||||||
key={dsp.id}
|
|
||||||
dsp={dsp}
|
|
||||||
step={step}
|
|
||||||
active={selected === dsp.id && !(page == 'add-dsp')}
|
|
||||||
dimmed={page === 'add-dsp'}
|
|
||||||
onSelect={() => onSelect(dsp.id)}
|
|
||||||
onRemove={() => onRemove(dsp)}
|
|
||||||
onConnect={onConnect}
|
|
||||||
onDisconnect={onDisconnect}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="tabstrip-actions">
|
|
||||||
{hasTabs ? (
|
|
||||||
<button className="tab-add tab-action" aria-label="Add DSP" onClick={onAddClick}>
|
|
||||||
<PlusIcon className="icon-tab-add" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button className="tab-add-empty tab-action" onClick={onAddClick}>
|
|
||||||
<PlusIcon className="icon-tab-add-empty" />
|
|
||||||
<span>Add DSP</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="crown-action tab-action"
|
|
||||||
title="Credits & Support"
|
|
||||||
aria-label="Credits & Support"
|
|
||||||
onClick={() => {
|
|
||||||
setPage('credits');
|
|
||||||
setSelect(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CrownIcon className="icon-crown" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TopBar;
|
|
||||||
@@ -3,11 +3,9 @@ import { InputChannel, Channel, OutputChannel } from '../../types';
|
|||||||
import { DSP408 as DSP } from '../../../../types';
|
import { DSP408 as DSP } from '../../../../types';
|
||||||
import Button from '../../../../components/Button/Button';
|
import Button from '../../../../components/Button/Button';
|
||||||
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
||||||
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
|
|
||||||
import './GainSection.css';
|
import './GainSection.css';
|
||||||
import { DiscreteFrequency } from '../../types';
|
|
||||||
import {
|
import {
|
||||||
DISCRETE_FREQUENCIES,
|
GEQ_BANDS_LABEL,
|
||||||
GRAPH_X_MAX,
|
GRAPH_X_MAX,
|
||||||
GRAPH_X_MIN,
|
GRAPH_X_MIN,
|
||||||
GRAPH_Y_MAX,
|
GRAPH_Y_MAX,
|
||||||
@@ -16,7 +14,13 @@ import {
|
|||||||
OUTPUT_ORDER,
|
OUTPUT_ORDER,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
// import { OverlayChannelSpec, buildInputMagnitudeOverlayDraw } from '../../utils/overlayCurve';
|
import LinearGraph from '../../../../components/LinearGraph/LinearGraph';
|
||||||
|
import { drawCurves } from '../../utils/graph/draw';
|
||||||
|
import { Curve, CurvePoint, sumCurvePoints } from '../../utils/graph/curve';
|
||||||
|
import { getGeqCurvePoints } from '../../utils/graph/geq';
|
||||||
|
import { getPeqCurvePoints } from '../../utils/graph/peq';
|
||||||
|
import { getCrossoverCurvePoints } from '../../utils/graph/crossover';
|
||||||
|
import { logSpace } from '../../utils/graph/curve';
|
||||||
|
|
||||||
const CHANNEL_COLORS = [
|
const CHANNEL_COLORS = [
|
||||||
'#4ade80',
|
'#4ade80',
|
||||||
@@ -29,6 +33,9 @@ const CHANNEL_COLORS = [
|
|||||||
'#38bdf8',
|
'#38bdf8',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GEQ_CURVE_Q = 5;
|
||||||
|
const Y_AXIS_STEP = 3; // must match the graph's yStep
|
||||||
|
|
||||||
function buildColorMap<T extends string>(channels: T[]): Record<T, string> {
|
function buildColorMap<T extends string>(channels: T[]): Record<T, string> {
|
||||||
const map = {} as Record<T, string>;
|
const map = {} as Record<T, string>;
|
||||||
channels.forEach((ch, i) => {
|
channels.forEach((ch, i) => {
|
||||||
@@ -37,6 +44,127 @@ function buildColorMap<T extends string>(channels: T[]): Record<T, string> {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Curve construction -----------------------------------------------
|
||||||
|
|
||||||
|
type ChannelCurves = {
|
||||||
|
geq: CurvePoint[];
|
||||||
|
peqCrossover: CurvePoint[];
|
||||||
|
total: CurvePoint[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getGainCurvePoints(freqs: number[], gainDb: number): CurvePoint[] {
|
||||||
|
return freqs.map((x) => ({ x, y: gainDb }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeqCrossoverPoints(
|
||||||
|
freqs: number[],
|
||||||
|
peqChain: { bands: Record<string, unknown> },
|
||||||
|
crossover: {
|
||||||
|
high_pass: { frequency: number; slope: unknown };
|
||||||
|
low_pass: { frequency: number; slope: unknown };
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return sumCurvePoints([
|
||||||
|
getPeqCurvePoints({ freqs, bands: peqChain.bands as any }),
|
||||||
|
getCrossoverCurvePoints({
|
||||||
|
freqs,
|
||||||
|
highPass: { frequency: crossover.high_pass.frequency, filter_type: crossover.high_pass.slope as any },
|
||||||
|
lowPass: { frequency: crossover.low_pass.frequency, filter_type: crossover.low_pass.slope as any },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputChannelCurves(
|
||||||
|
state: DSP['state']['current_config']['input_states'][InputChannel],
|
||||||
|
freqs: number[],
|
||||||
|
liveGain: number | undefined
|
||||||
|
): ChannelCurves {
|
||||||
|
const gain = liveGain ?? state.gain;
|
||||||
|
const geqValues = GEQ_BANDS_LABEL.map(({ frequency }) => state.geq.gains[frequency] ?? 0);
|
||||||
|
const geq = getGeqCurvePoints({ freqs, values: geqValues, q: GEQ_CURVE_Q });
|
||||||
|
const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
|
||||||
|
const total = sumCurvePoints([geq, peqCrossover, getGainCurvePoints(freqs, gain)]);
|
||||||
|
|
||||||
|
return { geq, peqCrossover, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOutputChannelCurves(
|
||||||
|
state: DSP['state']['current_config']['output_states'][OutputChannel],
|
||||||
|
freqs: number[],
|
||||||
|
liveGain: number | undefined
|
||||||
|
): ChannelCurves {
|
||||||
|
const gain = liveGain ?? state.gain;
|
||||||
|
const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
|
||||||
|
const total = sumCurvePoints([peqCrossover, getGainCurvePoints(freqs, gain)]);
|
||||||
|
|
||||||
|
return { geq: [], peqCrossover, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill:false everywhere — with multiple overlapping curves the fill/fade
|
||||||
|
// just muddies things; stroke-only makes each channel/stage legible.
|
||||||
|
function buildCurvesForChannels<T extends string>(
|
||||||
|
channels: T[],
|
||||||
|
selected: Set<T>,
|
||||||
|
colors: Record<T, string>,
|
||||||
|
showStages: boolean,
|
||||||
|
getCurves: (channel: T) => ChannelCurves
|
||||||
|
): Curve[] {
|
||||||
|
const curves: Curve[] = [];
|
||||||
|
|
||||||
|
channels.forEach((channel) => {
|
||||||
|
if (!selected.has(channel)) return;
|
||||||
|
const { geq, peqCrossover, total } = getCurves(channel);
|
||||||
|
const color = colors[channel];
|
||||||
|
|
||||||
|
if (showStages) {
|
||||||
|
if (geq.length > 0) {
|
||||||
|
curves.push({
|
||||||
|
points: geq,
|
||||||
|
style: { strokeColor: color, fill: false, dimmed: true, lineWidth: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (peqCrossover.length > 0) {
|
||||||
|
curves.push({
|
||||||
|
points: peqCrossover,
|
||||||
|
style: { strokeColor: color, fill: false, dimmed: true, lineWidth: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curves.push({ points: total, style: { strokeColor: color, fill: false, lineWidth: 2 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
return curves;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expands [baseMin, baseMax] outward to fit whatever the curves actually
|
||||||
|
// contain (e.g. gain + geq + peq stacking past the nominal graph range),
|
||||||
|
// snapped to the axis step with one step of headroom so nothing touches
|
||||||
|
// the plot edge. Never shrinks below the base range.
|
||||||
|
function computeYAxisBounds(
|
||||||
|
curves: Curve[],
|
||||||
|
baseMin: number,
|
||||||
|
baseMax: number,
|
||||||
|
step: number
|
||||||
|
): { yMin: number; yMax: number } {
|
||||||
|
let dataMax = baseMax;
|
||||||
|
let dataMin = baseMin;
|
||||||
|
|
||||||
|
for (const curve of curves) {
|
||||||
|
for (const { y } of curve.points) {
|
||||||
|
if (y > dataMax) dataMax = y;
|
||||||
|
if (y < dataMin) dataMin = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const yMax = Math.ceil((dataMax + step) / step) * step;
|
||||||
|
const yMin = Math.floor((dataMin - step) / step) * step;
|
||||||
|
|
||||||
|
return { yMin, yMax };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Panels -------------------------------------------------------------
|
||||||
|
|
||||||
function GainPanel({
|
function GainPanel({
|
||||||
dsp,
|
dsp,
|
||||||
channel,
|
channel,
|
||||||
@@ -44,6 +172,7 @@ function GainPanel({
|
|||||||
setGain,
|
setGain,
|
||||||
setMute,
|
setMute,
|
||||||
setInverse,
|
setInverse,
|
||||||
|
onLiveGain,
|
||||||
}: {
|
}: {
|
||||||
dsp: DSP;
|
dsp: DSP;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
@@ -51,6 +180,7 @@ function GainPanel({
|
|||||||
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
||||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||||
|
onLiveGain: (value: number | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const channelState =
|
const channelState =
|
||||||
'Input' in channel
|
'Input' in channel
|
||||||
@@ -69,6 +199,18 @@ function GainPanel({
|
|||||||
await setInverse(channel, !inverted);
|
await setInverse(channel, !inverted);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLiveValue = (value: number) => {
|
||||||
|
onLiveGain(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitGain = async (value: number) => {
|
||||||
|
const success = await setGain(channel, value);
|
||||||
|
if (success) {
|
||||||
|
onLiveGain(null);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gain-panel">
|
<div className="gain-panel">
|
||||||
<div className="gain-panel__title">{title}</div>
|
<div className="gain-panel__title">{title}</div>
|
||||||
@@ -80,7 +222,8 @@ function GainPanel({
|
|||||||
unit=" dB"
|
unit=" dB"
|
||||||
value={gain}
|
value={gain}
|
||||||
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
|
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
|
||||||
onChange={(value) => setGain(channel, value)}
|
onInput={handleLiveValue}
|
||||||
|
onChange={commitGain}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -164,14 +307,40 @@ function GainSection({
|
|||||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||||
}) {
|
}) {
|
||||||
const currentChannel = dsp.state.current_config.input_states[InputChannel.InA];
|
|
||||||
|
|
||||||
const inputColors = buildColorMap(INPUT_ORDER);
|
const inputColors = buildColorMap(INPUT_ORDER);
|
||||||
const outputColors = buildColorMap(OUTPUT_ORDER);
|
const outputColors = buildColorMap(OUTPUT_ORDER);
|
||||||
|
|
||||||
const [selectedInputs, setSelectedInputs] = useState<Set<InputChannel>>(new Set(INPUT_ORDER));
|
const [selectedInputs, setSelectedInputs] = useState<Set<InputChannel>>(new Set(INPUT_ORDER));
|
||||||
const [selectedOutputs, setSelectedOutputs] = useState<Set<OutputChannel>>(new Set(OUTPUT_ORDER));
|
const [selectedOutputs, setSelectedOutputs] = useState<Set<OutputChannel>>(new Set(OUTPUT_ORDER));
|
||||||
|
|
||||||
|
const [showInputStages, setShowInputStages] = useState(false);
|
||||||
|
const [showOutputStages, setShowOutputStages] = useState(false);
|
||||||
|
|
||||||
|
const [liveInputGains, setLiveInputGains] = useState<Partial<Record<InputChannel, number>>>({});
|
||||||
|
const [liveOutputGains, setLiveOutputGains] = useState<Partial<Record<OutputChannel, number>>>({});
|
||||||
|
|
||||||
|
const setLiveInputGain = (channel: InputChannel, value: number | null) => {
|
||||||
|
setLiveInputGains((prev) => {
|
||||||
|
if (value === null) {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[channel];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return { ...prev, [channel]: value };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLiveOutputGain = (channel: OutputChannel, value: number | null) => {
|
||||||
|
setLiveOutputGains((prev) => {
|
||||||
|
if (value === null) {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[channel];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return { ...prev, [channel]: value };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const inputLabels: Record<InputChannel, string> = INPUT_ORDER.reduce(
|
const inputLabels: Record<InputChannel, string> = INPUT_ORDER.reduce(
|
||||||
(acc, channel) => ({
|
(acc, channel) => ({
|
||||||
...acc,
|
...acc,
|
||||||
@@ -180,6 +349,14 @@ function GainSection({
|
|||||||
{} as Record<InputChannel, string>
|
{} as Record<InputChannel, string>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const outputLabels: Record<OutputChannel, string> = OUTPUT_ORDER.reduce(
|
||||||
|
(acc, channel) => ({
|
||||||
|
...acc,
|
||||||
|
[channel]: dsp.state.current_config.output_states[channel].name,
|
||||||
|
}),
|
||||||
|
{} as Record<OutputChannel, string>
|
||||||
|
);
|
||||||
|
|
||||||
const toggleInput = (channel: InputChannel) => {
|
const toggleInput = (channel: InputChannel) => {
|
||||||
setSelectedInputs((prev) => {
|
setSelectedInputs((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -196,20 +373,52 @@ function GainSection({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const inputOverlaySpecs: OverlayChannelSpec<InputChannel>[] = INPUT_ORDER.filter((c) =>
|
const freqs = logSpace(GRAPH_X_MIN, GRAPH_X_MAX, 1000);
|
||||||
// selectedInputs.has(c)
|
|
||||||
// ).map((channel) => ({ channel, color: inputColors[channel] }));
|
|
||||||
|
|
||||||
// const outputOverlaySpecs: OverlayChannelSpec<OutputChannel>[] = OUTPUT_ORDER.filter((c) =>
|
const inputCurves = buildCurvesForChannels(
|
||||||
// selectedOutputs.has(c)
|
INPUT_ORDER,
|
||||||
// ).map((channel) => ({ channel, color: outputColors[channel] }));
|
selectedInputs,
|
||||||
|
inputColors,
|
||||||
|
showInputStages,
|
||||||
|
(channel) =>
|
||||||
|
getInputChannelCurves(dsp.state.current_config.input_states[channel], freqs, liveInputGains[channel])
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputCurves = buildCurvesForChannels(
|
||||||
|
OUTPUT_ORDER,
|
||||||
|
selectedOutputs,
|
||||||
|
outputColors,
|
||||||
|
showOutputStages,
|
||||||
|
(channel) =>
|
||||||
|
getOutputChannelCurves(dsp.state.current_config.output_states[channel], freqs, liveOutputGains[channel])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { yMin: inputYMin, yMax: inputYMax } = computeYAxisBounds(
|
||||||
|
inputCurves,
|
||||||
|
GRAPH_Y_MIN,
|
||||||
|
GRAPH_Y_MAX,
|
||||||
|
Y_AXIS_STEP
|
||||||
|
);
|
||||||
|
const { yMin: outputYMin, yMax: outputYMax } = computeYAxisBounds(
|
||||||
|
outputCurves,
|
||||||
|
GRAPH_Y_MIN,
|
||||||
|
GRAPH_Y_MAX,
|
||||||
|
Y_AXIS_STEP
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gain-linear-graph-section">
|
<div className="gain-linear-graph-section">
|
||||||
<div className="gain-graph-panel">
|
<div className="gain-graph-panel">
|
||||||
<div className="gain-graph-title-row">
|
<div className="gain-graph-title-row">
|
||||||
<div className="gain-graph-title">Input Magnitude</div>
|
<div className="gain-graph-title">Input Response</div>
|
||||||
|
<Button
|
||||||
|
variant="toggle"
|
||||||
|
className={showInputStages ? 'active' : ''}
|
||||||
|
onClick={() => setShowInputStages((v) => !v)}
|
||||||
|
>
|
||||||
|
Show stages
|
||||||
|
</Button>
|
||||||
<ChannelSelector
|
<ChannelSelector
|
||||||
channels={INPUT_ORDER}
|
channels={INPUT_ORDER}
|
||||||
labels={inputLabels}
|
labels={inputLabels}
|
||||||
@@ -218,48 +427,75 @@ function GainSection({
|
|||||||
onToggle={toggleInput}
|
onToggle={toggleInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* <LinearGraph
|
<LinearGraph
|
||||||
xMin={GRAPH_X_MIN}
|
xMin={GRAPH_X_MIN}
|
||||||
xMax={GRAPH_X_MAX}
|
xMax={GRAPH_X_MAX}
|
||||||
xScale="log"
|
xScale="log"
|
||||||
yMin={GRAPH_Y_MIN}
|
yMin={inputYMin}
|
||||||
yMax={GRAPH_Y_MAX}
|
yMax={inputYMax}
|
||||||
yStep={3}
|
yStep={Y_AXIS_STEP}
|
||||||
xLabel="Hz"
|
xLabel="Hz"
|
||||||
yLabel="dB"
|
yLabel="dB"
|
||||||
showGrid={true}
|
showGrid={true}
|
||||||
formatX={(v) => Math.round(v).toString()}
|
formatX={(v) => Math.round(v).toString()}
|
||||||
formatY={(v) => Math.round(v).toString()}
|
formatY={(v) => Math.round(v).toString()}
|
||||||
draw={buildInputMagnitudeOverlayDraw({
|
|
||||||
channels: inputOverlaySpecs,
|
|
||||||
getGains: (channel) => dsp.state.current_config.input_states[channel].geq.gains,
|
|
||||||
getBypass: (channel) => dsp.state.current_config.input_states[channel].geq.bypass,
|
|
||||||
})}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className="gain-graph-panel">
|
|
||||||
<div className="gain-graph-title">Phase Response</div>
|
|
||||||
<LinearGraph
|
|
||||||
xMin={20}
|
|
||||||
xMax={20000}
|
|
||||||
yMin={-180}
|
|
||||||
yMax={180}
|
|
||||||
yStep={45}
|
|
||||||
xLabel="Hz"
|
|
||||||
yLabel="deg"
|
|
||||||
draw={({ ctx, toPx }) => {
|
draw={({ ctx, toPx }) => {
|
||||||
ctx.strokeStyle = '#4ade80';
|
drawCurves({
|
||||||
ctx.lineWidth = 2;
|
ctx,
|
||||||
const [x0, y0] = toPx(20, 0);
|
toPx,
|
||||||
const [x1, y1] = toPx(20000, 0);
|
xMin: GRAPH_X_MIN,
|
||||||
ctx.beginPath();
|
xMax: GRAPH_X_MAX,
|
||||||
ctx.moveTo(x0, y0);
|
yMin: inputYMin,
|
||||||
ctx.lineTo(x1, y1);
|
yMax: inputYMax,
|
||||||
ctx.stroke();
|
curves: inputCurves,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
|
<div className="gain-graph-panel">
|
||||||
|
<div className="gain-graph-title-row">
|
||||||
|
<div className="gain-graph-title">Output Response</div>
|
||||||
|
<Button
|
||||||
|
variant="toggle"
|
||||||
|
className={showOutputStages ? 'active' : ''}
|
||||||
|
onClick={() => setShowOutputStages((v) => !v)}
|
||||||
|
>
|
||||||
|
Show stages
|
||||||
|
</Button>
|
||||||
|
<ChannelSelector
|
||||||
|
channels={OUTPUT_ORDER}
|
||||||
|
labels={outputLabels}
|
||||||
|
colors={outputColors}
|
||||||
|
selected={selectedOutputs}
|
||||||
|
onToggle={toggleOutput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LinearGraph
|
||||||
|
xMin={GRAPH_X_MIN}
|
||||||
|
xMax={GRAPH_X_MAX}
|
||||||
|
xScale="log"
|
||||||
|
yMin={outputYMin}
|
||||||
|
yMax={outputYMax}
|
||||||
|
yStep={Y_AXIS_STEP}
|
||||||
|
xLabel="Hz"
|
||||||
|
yLabel="dB"
|
||||||
|
showGrid={true}
|
||||||
|
formatX={(v) => Math.round(v).toString()}
|
||||||
|
formatY={(v) => Math.round(v).toString()}
|
||||||
|
draw={({ ctx, toPx }) => {
|
||||||
|
drawCurves({
|
||||||
|
ctx,
|
||||||
|
toPx,
|
||||||
|
xMin: GRAPH_X_MIN,
|
||||||
|
xMax: GRAPH_X_MAX,
|
||||||
|
yMin: outputYMin,
|
||||||
|
yMax: outputYMax,
|
||||||
|
curves: outputCurves,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gain-section">
|
<div className="gain-section">
|
||||||
@@ -274,6 +510,7 @@ function GainSection({
|
|||||||
setGain={setGain}
|
setGain={setGain}
|
||||||
setMute={setMute}
|
setMute={setMute}
|
||||||
setInverse={setInverse}
|
setInverse={setInverse}
|
||||||
|
onLiveGain={(value) => setLiveInputGain(channel, value)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ChannelGroup>
|
</ChannelGroup>
|
||||||
@@ -288,6 +525,7 @@ function GainSection({
|
|||||||
setGain={setGain}
|
setGain={setGain}
|
||||||
setMute={setMute}
|
setMute={setMute}
|
||||||
setInverse={setInverse}
|
setInverse={setInverse}
|
||||||
|
onLiveGain={(value) => setLiveOutputGain(channel, value)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ChannelGroup>
|
</ChannelGroup>
|
||||||
@@ -297,4 +535,4 @@ function GainSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GainSection;
|
export default GainSection;
|
||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
CurveStyle,
|
CurveStyle,
|
||||||
Curve,
|
Curve,
|
||||||
DEFAULT_CURVE_COLORS,
|
DEFAULT_CURVE_COLORS,
|
||||||
logSpace,
|
|
||||||
sumCurvePoints,
|
sumCurvePoints,
|
||||||
} from './curve';
|
} from './curve';
|
||||||
|
|
||||||
|
|||||||
@@ -319,115 +319,6 @@ function peqTotalPhaseDeg(
|
|||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unwrapPhaseDeg(phases: number[]): number[] {
|
|
||||||
if (phases.length === 0) return [];
|
|
||||||
|
|
||||||
const result = [phases[0]];
|
|
||||||
|
|
||||||
for (let i = 1; i < phases.length; i++) {
|
|
||||||
let phase = phases[i];
|
|
||||||
const previous = result[i - 1];
|
|
||||||
|
|
||||||
while (phase - previous > 180) {
|
|
||||||
phase -= 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (phase - previous < -180) {
|
|
||||||
phase += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export function getPeqPhaseSegments({
|
|
||||||
// freqs,
|
|
||||||
// bands,
|
|
||||||
// sampleRate = DEFAULT_SAMPLE_RATE,
|
|
||||||
// }: {
|
|
||||||
// freqs: number[];
|
|
||||||
// bands: Record<PEQBand, PEQ>;
|
|
||||||
// sampleRate?: number;
|
|
||||||
// }): CurvePoint[][] {
|
|
||||||
// if (freqs.length === 0) return [];
|
|
||||||
|
|
||||||
// const rawPhases = freqs.map((freq) => peqTotalPhaseDeg(freq, bands, sampleRate));
|
|
||||||
// const phases = unwrapPhaseDeg(rawPhases);
|
|
||||||
|
|
||||||
// const wrapToDisplay = (p: number) => ((((p + 180) % 360) + 360) % 360) - 180;
|
|
||||||
|
|
||||||
// const segments: CurvePoint[][] = [[]];
|
|
||||||
|
|
||||||
// const pushPoint = (x: number, y: number) => {
|
|
||||||
// segments[segments.length - 1].push({ x, y });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let previousFreq = freqs[0];
|
|
||||||
// let previousPhase = phases[0];
|
|
||||||
|
|
||||||
// pushPoint(previousFreq, wrapToDisplay(previousPhase));
|
|
||||||
|
|
||||||
// for (let i = 1; i < freqs.length; i++) {
|
|
||||||
// const freq = freqs[i];
|
|
||||||
// const phase = phases[i];
|
|
||||||
|
|
||||||
// const logPrev = Math.log10(previousFreq);
|
|
||||||
// const logCurr = Math.log10(freq);
|
|
||||||
|
|
||||||
// const direction = phase >= previousPhase ? 1 : -1;
|
|
||||||
|
|
||||||
// const boundaries: number[] = [];
|
|
||||||
|
|
||||||
// if (direction > 0) {
|
|
||||||
// let b = Math.floor((previousPhase - 180) / 360) * 360 + 180;
|
|
||||||
|
|
||||||
// while (b <= previousPhase) {
|
|
||||||
// b += 360;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// while (b < phase) {
|
|
||||||
// boundaries.push(b);
|
|
||||||
// b += 360;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// let b = Math.ceil((previousPhase - 180) / 360) * 360 + 180;
|
|
||||||
|
|
||||||
// while (b >= previousPhase) {
|
|
||||||
// b -= 360;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// while (b > phase) {
|
|
||||||
// boundaries.push(b);
|
|
||||||
// b -= 360;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const boundary of boundaries) {
|
|
||||||
// const t = (boundary - previousPhase) / (phase - previousPhase);
|
|
||||||
// const crossingFreq = Math.pow(10, logPrev + t * (logCurr - logPrev));
|
|
||||||
|
|
||||||
// const approach = direction > 0 ? 180 : -180;
|
|
||||||
// const depart = direction > 0 ? -180 : 180;
|
|
||||||
|
|
||||||
// // End current segment at ±180°.
|
|
||||||
// pushPoint(crossingFreq, approach);
|
|
||||||
|
|
||||||
// // Start a new segment at the opposite side of the display.
|
|
||||||
// segments.push([]);
|
|
||||||
// pushPoint(crossingFreq, depart);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pushPoint(freq, wrapToDisplay(phase));
|
|
||||||
|
|
||||||
// previousFreq = freq;
|
|
||||||
// previousPhase = phase;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return segments.filter((segment) => segment.length >= 2);
|
|
||||||
// }
|
|
||||||
|
|
||||||
export function getPeqPhasePoints({
|
export function getPeqPhasePoints({
|
||||||
freqs,
|
freqs,
|
||||||
bands,
|
bands,
|
||||||
|
|||||||
+25
-25
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-32
@@ -1,32 +1,32 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent Vite from obscuring rust errors
|
// 1. prevent Vite from obscuring rust errors
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
// 2. tauri expects a fixed port, fail if that port is not available
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
host: host || false,
|
host: host || false,
|
||||||
hmr: host
|
hmr: host
|
||||||
? {
|
? {
|
||||||
protocol: "ws",
|
protocol: "ws",
|
||||||
host,
|
host,
|
||||||
port: 1421,
|
port: 1421,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
// 3. tell Vite to ignore watching `src-tauri`
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
ignored: ["**/src-tauri/**"],
|
ignored: ["**/src-tauri/**"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user