Compare commits
1 commit
main
...
windows-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f33adb3d7 |
6 changed files with 1616 additions and 3501 deletions
66
WINDOWS.md
Normal file
66
WINDOWS.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Strudel (fork hacklab) + KISUM en Windows
|
||||||
|
|
||||||
|
> ⚠ **Estado: a probar.** Estos scripts están escritos y revisados pero
|
||||||
|
> todavía no se han ejecutado en un Windows real. Si algo falla, abre una
|
||||||
|
> incidencia en el Gitea con el error.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
Instálalos una vez (desde PowerShell o `winget`):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install OpenJS.NodeJS.LTS # Node.js (>=20) + npm/corepack
|
||||||
|
winget install Python.Python.3.12 # Python (para KISUM)
|
||||||
|
winget install Gyan.FFmpeg # ffmpeg (para leer vídeo en KISUM)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cierra y reabre la terminal después, para que el `PATH` se actualice.
|
||||||
|
|
||||||
|
## Instalación (un comando)
|
||||||
|
|
||||||
|
Descarga el repo y ejecuta el instalador:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://gitea.laenre.net/hacklab/strudel.git
|
||||||
|
cd strudel
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\install.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Instala las dependencias de Node (`pnpm`), construye la web y crea el entorno
|
||||||
|
de KISUM. **No** guarda paquetes en el repo (todo va a carpetas ignoradas).
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\strudel-launcher.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Abre <http://127.0.0.1:3009> en el navegador y arranca el servidor de KISUM
|
||||||
|
(<http://127.0.0.1:3010>). Para KISUM desde la terminal:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd kisum
|
||||||
|
.\kisum.cmd tu_cancion.mp3
|
||||||
|
.\kisum.cmd clip.mp4 --bars 8 --bass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acceso directo en el escritorio
|
||||||
|
|
||||||
|
Crea un acceso directo cuyo destino sea:
|
||||||
|
|
||||||
|
```
|
||||||
|
powershell -ExecutionPolicy Bypass -File "C:\ruta\a\strudel\strudel-launcher.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- Si PowerShell bloquea los scripts, ejecútalos con `-ExecutionPolicy Bypass`
|
||||||
|
como en los ejemplos (no cambia la política global del sistema).
|
||||||
|
- El resto (funciones, pestañas KISUM/Visor, temas…) es idéntico a Linux; solo
|
||||||
|
cambian los lanzadores. Ver [`README.md`](./README.md).
|
||||||
|
|
||||||
|
## Fase 2 (futuro): instalador .exe
|
||||||
|
|
||||||
|
Empaquetar todo en un instalador `.exe` con **Inno Setup** o **NSIS** (que
|
||||||
|
instale Node/Python/ffmpeg embebidos y cree los accesos directos) es posible,
|
||||||
|
pero hay que compilarlo y probarlo en Windows. Queda propuesto.
|
||||||
55
install.ps1
Normal file
55
install.ps1
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Instalador de Strudel (fork hacklab) + KISUM para Windows (PowerShell).
|
||||||
|
# Ejecuta desde una terminal PowerShell en la carpeta del repo:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\install.ps1
|
||||||
|
#
|
||||||
|
# NO sube paquetes al repo: usa pnpm (Node) y un venv de Python para KISUM.
|
||||||
|
# ⚠ Escrito y revisado, pero pendiente de probar en un Windows real.
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
Set-Location $DIR
|
||||||
|
|
||||||
|
Write-Host "============================================"
|
||||||
|
Write-Host " Instalador Strudel (fork hacklab) + KISUM"
|
||||||
|
Write-Host "============================================"
|
||||||
|
|
||||||
|
# --- Node / pnpm ---
|
||||||
|
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Error "Falta Node.js (>= 20). Instálalo: winget install OpenJS.NodeJS.LTS (y reabre la terminal)"
|
||||||
|
}
|
||||||
|
if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Host "==> activando pnpm con corepack"
|
||||||
|
corepack enable pnpm
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Strudel: instalando dependencias de Node (pnpm i)..."
|
||||||
|
pnpm i
|
||||||
|
Write-Host "==> Strudel: construyendo la web (pnpm build)..."
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# --- KISUM (opcional) ---
|
||||||
|
$kisum = Join-Path $DIR "kisum"
|
||||||
|
if (Test-Path (Join-Path $kisum "requirements.txt")) {
|
||||||
|
Write-Host "==> Configurando KISUM (audio -> codigo Strudel)..."
|
||||||
|
$venv = Join-Path $kisum ".venv"
|
||||||
|
if (Get-Command py -ErrorAction SilentlyContinue) {
|
||||||
|
py -3 -m venv $venv
|
||||||
|
} elseif (Get-Command python -ErrorAction SilentlyContinue) {
|
||||||
|
python -m venv $venv
|
||||||
|
} else {
|
||||||
|
Write-Warning "No encuentro Python. Instálalo: winget install Python.Python.3.12 (KISUM no se instalará; Strudel funciona igual)"
|
||||||
|
$venv = $null
|
||||||
|
}
|
||||||
|
if ($venv) {
|
||||||
|
$pip = Join-Path $venv "Scripts\pip.exe"
|
||||||
|
& $pip install --upgrade pip
|
||||||
|
& $pip install -r (Join-Path $kisum "requirements.txt")
|
||||||
|
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Warning "ffmpeg no está en el PATH. KISUM lo necesita para vídeo. Instálalo: winget install Gyan.FFmpeg (y reabre la terminal)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "OK. Arranca con: powershell -ExecutionPolicy Bypass -File .\strudel-launcher.ps1"
|
||||||
|
Write-Host " Strudel: http://127.0.0.1:3009"
|
||||||
|
Write-Host " KISUM server: http://127.0.0.1:3010 (arranca solo con el lanzador)"
|
||||||
12
kisum/kisum.cmd
Normal file
12
kisum/kisum.cmd
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
@echo off
|
||||||
|
rem Lanzador de KISUM para Windows: usa el venv local de esta carpeta.
|
||||||
|
rem kisum tu_cancion.mp3
|
||||||
|
rem kisum clip.mp4 --bars 8 --bass
|
||||||
|
setlocal
|
||||||
|
set "DIR=%~dp0"
|
||||||
|
if not exist "%DIR%.venv\Scripts\python.exe" (
|
||||||
|
echo KISUM: no encuentro el entorno .venv
|
||||||
|
echo Instalalo con: powershell -ExecutionPolicy Bypass -File ..\install.ps1
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
"%DIR%.venv\Scripts\python.exe" "%DIR%kisum.py" %*
|
||||||
34
strudel-launcher.ps1
Normal file
34
strudel-launcher.ps1
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Lanzador de Strudel (fork hacklab) + KISUM para Windows (PowerShell).
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\strudel-launcher.ps1
|
||||||
|
# Sirve la build estática, arranca el servidor de KISUM y abre el navegador.
|
||||||
|
# ⚠ Escrito y revisado, pero pendiente de probar en un Windows real.
|
||||||
|
$DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$PORT = if ($env:STRUDEL_PORT) { $env:STRUDEL_PORT } else { 3009 }
|
||||||
|
$URL = "http://127.0.0.1:$PORT/"
|
||||||
|
$ASTRO = Join-Path $DIR "website\node_modules\.bin\astro.cmd"
|
||||||
|
|
||||||
|
function Test-Up($u) {
|
||||||
|
try { Invoke-WebRequest -UseBasicParsing -Uri $u -TimeoutSec 2 | Out-Null; return $true } catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $ASTRO)) {
|
||||||
|
Write-Error "Falta la instalación. Ejecuta antes: powershell -ExecutionPolicy Bypass -File .\install.ps1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Servidor web (astro preview) ---
|
||||||
|
if (-not (Test-Up $URL)) {
|
||||||
|
Start-Process -WindowStyle Hidden -WorkingDirectory (Join-Path $DIR "website") `
|
||||||
|
-FilePath $ASTRO -ArgumentList "preview","--port","$PORT","--host","127.0.0.1"
|
||||||
|
for ($i = 0; $i -lt 30; $i++) { if (Test-Up $URL) { break }; Start-Sleep -Seconds 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Servidor KISUM (si tiene venv) ---
|
||||||
|
$KPY = Join-Path $DIR "kisum\.venv\Scripts\python.exe"
|
||||||
|
if (Test-Path $KPY) {
|
||||||
|
if (-not (Test-Up "http://127.0.0.1:3010/health")) {
|
||||||
|
Start-Process -WindowStyle Hidden -FilePath $KPY -ArgumentList (Join-Path $DIR "kisum\kisum_server.py")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Abrir navegador ---
|
||||||
|
Start-Process $URL
|
||||||
|
|
@ -3,122 +3,23 @@ import cx from '@src/cx.mjs';
|
||||||
import { useSettings } from '@src/settings.mjs';
|
import { useSettings } from '@src/settings.mjs';
|
||||||
import { __pianoroll } from '@strudel/draw';
|
import { __pianoroll } from '@strudel/draw';
|
||||||
import {
|
import {
|
||||||
|
drawTimeScope,
|
||||||
|
drawFrequencyScope,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
getAnalyserById,
|
getAnalyserById,
|
||||||
getAnalyzerData,
|
|
||||||
getSuperdoughAudioController,
|
getSuperdoughAudioController,
|
||||||
|
analysers,
|
||||||
} from '@strudel/webaudio';
|
} from '@strudel/webaudio';
|
||||||
|
|
||||||
// Visor: dibuja EN VIVO lo que suena, sin escribir nada en el código.
|
// Visor: dibuja EN VIVO lo que suena, sin tener que escribir nada en el codigo.
|
||||||
// - PENTAGRAMA (piano-roll): notas de lo que se está tocando ahora.
|
// - PENTAGRAMA (piano-roll): rejilla de notas de lo que se esta tocando ahora.
|
||||||
// - ONDA: osciloscopio estable (disparo por cruce por cero + auto-ganancia).
|
// - ONDA (osciloscopio): forma de onda del sonido en tiempo real.
|
||||||
// - ESPECTRO: bandas logarítmicas (graves→agudos) con peak-hold.
|
// - ESPECTRO: reparto de frecuencias (graves -> agudos).
|
||||||
// Para que funcione con CUALQUIER código (sin .scope()) pinchamos dos
|
// El truco para que funcione con CUALQUIER codigo (sin .scope()) es pinchar un
|
||||||
// AnalyserNode al bus maestro de superdough (destinationGain).
|
// AnalyserNode al bus maestro de superdough (destinationGain).
|
||||||
|
|
||||||
const CKEY = 'strudel-visor-cycles'; // zoom del pentagrama (compases visibles)
|
const CKEY = 'strudel-visor-cycles'; // zoom del pentagrama (compases visibles)
|
||||||
const ID_T = 'visorTime'; // analizador de forma de onda
|
const SCOPE_ID = 'visor';
|
||||||
const ID_F = 'visorFreq'; // analizador de espectro
|
|
||||||
const FFT = 2048;
|
|
||||||
|
|
||||||
const CYAN = '#00e5ff';
|
|
||||||
const ORANGE = '#ff7a18';
|
|
||||||
|
|
||||||
// ── osciloscopio estable ────────────────────────────────────────────
|
|
||||||
function drawOnda(ctx, data, gainRef) {
|
|
||||||
const w = ctx.canvas.width;
|
|
||||||
const h = ctx.canvas.height;
|
|
||||||
const n = data.length;
|
|
||||||
const mid = h / 2;
|
|
||||||
|
|
||||||
// línea central tenue
|
|
||||||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, mid);
|
|
||||||
ctx.lineTo(w, mid);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// disparo: primer cruce por cero ascendente (estabiliza la imagen)
|
|
||||||
let start = 0;
|
|
||||||
const search = Math.floor(n / 2);
|
|
||||||
for (let i = 1; i < search; i++) {
|
|
||||||
if (data[i - 1] <= 0 && data[i] > 0) {
|
|
||||||
start = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// auto-ganancia suave: normaliza al pico reciente para que se vea igual de
|
|
||||||
// bien con el tema flojo o fuerte (con límite para no ampliar el silencio).
|
|
||||||
let peak = 0;
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const a = Math.abs(data[i]);
|
|
||||||
if (a > peak) peak = a;
|
|
||||||
}
|
|
||||||
const amp = mid * 0.92; // altura útil (deja margen arriba/abajo)
|
|
||||||
const norm = 1 / Math.max(peak, 0.02); // multiplicador para que el pico llene amp
|
|
||||||
gainRef.g += (norm - gainRef.g) * 0.15; // suavizado entre frames
|
|
||||||
const k = amp * Math.min(gainRef.g, 6); // tope de amplificación x6
|
|
||||||
|
|
||||||
const len = n - start;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.strokeStyle = CYAN;
|
|
||||||
ctx.shadowBlur = 8;
|
|
||||||
ctx.shadowColor = CYAN;
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const x = (i / (len - 1)) * w;
|
|
||||||
let s = data[start + i] * k;
|
|
||||||
if (s > amp) s = amp;
|
|
||||||
else if (s < -amp) s = -amp;
|
|
||||||
const y = mid - s;
|
|
||||||
if (i === 0) ctx.moveTo(x, y);
|
|
||||||
else ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── espectro con bandas logarítmicas + peak-hold ────────────────────
|
|
||||||
function drawEspectro(ctx, data, sampleRate, peaks) {
|
|
||||||
const w = ctx.canvas.width;
|
|
||||||
const h = ctx.canvas.height;
|
|
||||||
const bins = data.length; // dB, lineal de 0..nyquist
|
|
||||||
const nyquist = sampleRate / 2;
|
|
||||||
const BANDS = peaks.length;
|
|
||||||
const fmin = 30;
|
|
||||||
const fmax = Math.min(16000, nyquist);
|
|
||||||
const minDb = -90;
|
|
||||||
const maxDb = -10;
|
|
||||||
const bw = w / BANDS;
|
|
||||||
|
|
||||||
for (let b = 0; b < BANDS; b++) {
|
|
||||||
const f0 = fmin * Math.pow(fmax / fmin, b / BANDS);
|
|
||||||
const f1 = fmin * Math.pow(fmax / fmin, (b + 1) / BANDS);
|
|
||||||
let i0 = Math.floor((f0 / nyquist) * bins);
|
|
||||||
let i1 = Math.floor((f1 / nyquist) * bins);
|
|
||||||
if (i1 <= i0) i1 = i0 + 1;
|
|
||||||
let m = -Infinity;
|
|
||||||
for (let i = i0; i < i1 && i < bins; i++) if (data[i] > m) m = data[i];
|
|
||||||
if (m === -Infinity) m = minDb;
|
|
||||||
let v = (m - minDb) / (maxDb - minDb);
|
|
||||||
v = v < 0 ? 0 : v > 1 ? 1 : v;
|
|
||||||
|
|
||||||
const x = b * bw;
|
|
||||||
const bh = v * h;
|
|
||||||
// color por frecuencia: graves naranjas → agudos cian (tema neón)
|
|
||||||
const hue = 25 + (b / BANDS) * 165;
|
|
||||||
ctx.fillStyle = `hsl(${hue}, 90%, 55%)`;
|
|
||||||
ctx.fillRect(x, h - bh, Math.max(1, bw * 0.8), bh);
|
|
||||||
|
|
||||||
// peak-hold: cae despacio
|
|
||||||
peaks[b] = Math.max(v, peaks[b] - 0.015);
|
|
||||||
const py = h - peaks[b] * h;
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.85)';
|
|
||||||
ctx.fillRect(x, py - 1.5, Math.max(1, bw * 0.8), 1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VisorTab({ context }) {
|
export function VisorTab({ context }) {
|
||||||
const { fontFamily } = useSettings();
|
const { fontFamily } = useSettings();
|
||||||
|
|
@ -136,6 +37,7 @@ export function VisorTab({ context }) {
|
||||||
const [show, setShow] = useState({ roll: true, scope: true, spec: true });
|
const [show, setShow] = useState({ roll: true, scope: true, spec: true });
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
|
|
||||||
|
// refs para que el bucle lea los valores actuales sin reiniciarse
|
||||||
const cyclesRef = useRef(cycles);
|
const cyclesRef = useRef(cycles);
|
||||||
const showRef = useRef(show);
|
const showRef = useRef(show);
|
||||||
cyclesRef.current = cycles;
|
cyclesRef.current = cycles;
|
||||||
|
|
@ -157,39 +59,32 @@ export function VisorTab({ context }) {
|
||||||
const sctx = scope?.getContext('2d');
|
const sctx = scope?.getContext('2d');
|
||||||
const fctx = spec?.getContext('2d');
|
const fctx = spec?.getContext('2d');
|
||||||
let raf;
|
let raf;
|
||||||
let lastT = null;
|
let lastNode = null;
|
||||||
let lastF = null;
|
|
||||||
const gainRef = { g: 1 };
|
|
||||||
const peaks = new Float32Array(64); // bandas del espectro
|
|
||||||
|
|
||||||
// el piano-roll usa tamaños de texto en píxeles → dpr 1; onda/espectro
|
// ajusta el tamaño interno del canvas al que ocupa en pantalla
|
||||||
// los dibujo yo, así que ahí sí uso dpr para que se vean nítidos.
|
const fit = (cv) => {
|
||||||
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
||||||
const fit = (cv, scale) => {
|
|
||||||
if (!cv) return;
|
if (!cv) return;
|
||||||
const w = Math.round(cv.clientWidth * scale);
|
const w = cv.clientWidth;
|
||||||
const h = Math.round(cv.clientHeight * scale);
|
const h = cv.clientHeight;
|
||||||
if (w && h && (cv.width !== w || cv.height !== h)) {
|
if (w && h && (cv.width !== w || cv.height !== h)) {
|
||||||
cv.width = w;
|
cv.width = w;
|
||||||
cv.height = h;
|
cv.height = h;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureAnalysers = () => {
|
// pincha un analizador al bus maestro para poder leer onda/espectro de
|
||||||
|
// TODO lo que suena (sin que el usuario escriba .scope()). Se reengancha
|
||||||
|
// solo si el nodo cambia (p.ej. tras reiniciar el motor de audio).
|
||||||
|
const ensureAnalyser = () => {
|
||||||
try {
|
try {
|
||||||
getAudioContext();
|
getAudioContext(); // fuerza el contexto si aún no existe
|
||||||
const nt = getAnalyserById(ID_T, FFT, 0);
|
const node = getAnalyserById(SCOPE_ID, 1024);
|
||||||
if (nt !== lastT) {
|
if (node !== lastNode) {
|
||||||
getSuperdoughAudioController().output.destinationGain.connect(nt);
|
getSuperdoughAudioController().output.destinationGain.connect(node);
|
||||||
lastT = nt;
|
lastNode = node;
|
||||||
}
|
|
||||||
const nf = getAnalyserById(ID_F, FFT, 0.7);
|
|
||||||
if (nf !== lastF) {
|
|
||||||
getSuperdoughAudioController().output.destinationGain.connect(nf);
|
|
||||||
lastF = nf;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* audio aún no listo */
|
/* el audio aún no está listo: se reintenta el siguiente frame */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -199,23 +94,25 @@ export function VisorTab({ context }) {
|
||||||
setPlaying((p) => (p !== started ? started : p));
|
setPlaying((p) => (p !== started ? started : p));
|
||||||
|
|
||||||
const S = showRef.current;
|
const S = showRef.current;
|
||||||
fit(roll, 1);
|
fit(roll);
|
||||||
fit(scope, dpr);
|
fit(scope);
|
||||||
fit(spec, dpr);
|
fit(spec);
|
||||||
rctx && rctx.clearRect(0, 0, roll.width, roll.height);
|
rctx && rctx.clearRect(0, 0, roll.width, roll.height);
|
||||||
sctx && sctx.clearRect(0, 0, scope.width, scope.height);
|
sctx && sctx.clearRect(0, 0, scope.width, scope.height);
|
||||||
fctx && fctx.clearRect(0, 0, spec.width, spec.height);
|
fctx && fctx.clearRect(0, 0, spec.width, spec.height);
|
||||||
|
|
||||||
if (started && scheduler.pattern) {
|
if (started && scheduler.pattern) {
|
||||||
// PENTAGRAMA
|
const time = scheduler.now();
|
||||||
if (S.roll && rctx) {
|
|
||||||
const cyc = cyclesRef.current;
|
const cyc = cyclesRef.current;
|
||||||
|
|
||||||
|
// PENTAGRAMA (piano-roll) de lo que suena en esta ventana de tiempo
|
||||||
|
if (S.roll && rctx) {
|
||||||
const playhead = 0.5;
|
const playhead = 0.5;
|
||||||
const from = -cyc * playhead;
|
const from = -cyc * playhead;
|
||||||
const to = cyc * (1 - playhead);
|
const to = cyc * (1 - playhead);
|
||||||
let haps = [];
|
let haps = [];
|
||||||
try {
|
try {
|
||||||
haps = scheduler.pattern.queryArc(scheduler.now() + from, scheduler.now() + to).filter((h) => h.hasOnset());
|
haps = scheduler.pattern.queryArc(time + from, time + to).filter((h) => h.hasOnset());
|
||||||
} catch {
|
} catch {
|
||||||
haps = [];
|
haps = [];
|
||||||
}
|
}
|
||||||
|
|
@ -223,33 +120,32 @@ export function VisorTab({ context }) {
|
||||||
__pianoroll({
|
__pianoroll({
|
||||||
ctx: rctx,
|
ctx: rctx,
|
||||||
haps,
|
haps,
|
||||||
time: scheduler.now(),
|
time,
|
||||||
cycles: cyc,
|
cycles: cyc,
|
||||||
playhead,
|
playhead,
|
||||||
fold: 1,
|
fold: 1,
|
||||||
labels: 1,
|
labels: 1,
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
playheadColor: ORANGE,
|
playheadColor: '#ff7a18',
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONDA + ESPECTRO
|
// ONDA + ESPECTRO del bus maestro
|
||||||
if (S.scope || S.spec) {
|
if ((S.scope || S.spec) && (sctx || fctx)) {
|
||||||
ensureAnalysers();
|
ensureAnalyser();
|
||||||
if (S.scope && sctx) {
|
if (S.scope && sctx && analysers[SCOPE_ID]) {
|
||||||
try {
|
try {
|
||||||
drawOnda(sctx, getAnalyzerData('time', ID_T), gainRef);
|
drawTimeScope(analysers[SCOPE_ID], { ctx: sctx, id: SCOPE_ID, color: '#00e5ff', thickness: 2 });
|
||||||
} catch {
|
} catch {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (S.spec && fctx) {
|
if (S.spec && fctx && analysers[SCOPE_ID]) {
|
||||||
try {
|
try {
|
||||||
const sr = getAudioContext().sampleRate || 44100;
|
drawFrequencyScope(analysers[SCOPE_ID], { ctx: fctx, id: SCOPE_ID, color: '#ff7a18' });
|
||||||
drawEspectro(fctx, getAnalyzerData('frequency', ID_F), sr, peaks);
|
|
||||||
} catch {
|
} catch {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +167,7 @@ export function VisorTab({ context }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full text-foreground flex flex-col overflow-hidden" style={{ fontFamily }}>
|
<div className="w-full h-full text-foreground flex flex-col overflow-hidden" style={{ fontFamily }}>
|
||||||
|
{/* controles */}
|
||||||
<div className="flex items-center gap-3 px-2 py-2 border-b border-muted shrink-0 flex-wrap">
|
<div className="flex items-center gap-3 px-2 py-2 border-b border-muted shrink-0 flex-wrap">
|
||||||
<span className="text-xs opacity-70">zoom</span>
|
<span className="text-xs opacity-70">zoom</span>
|
||||||
<input
|
<input
|
||||||
|
|
@ -297,6 +194,7 @@ export function VisorTab({ context }) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* lienzos */}
|
||||||
<div className="relative grow min-h-0 flex flex-col">
|
<div className="relative grow min-h-0 flex flex-col">
|
||||||
{!playing && (
|
{!playing && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
||||||
|
|
@ -318,7 +216,7 @@ export function VisorTab({ context }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{show.spec && (
|
{show.spec && (
|
||||||
<div className="relative shrink-0" style={{ height: 120 }}>
|
<div className="relative shrink-0" style={{ height: 110 }}>
|
||||||
<span className="absolute top-1 left-2 text-[10px] uppercase tracking-widest opacity-40 z-10">
|
<span className="absolute top-1 left-2 text-[10px] uppercase tracking-widest opacity-40 z-10">
|
||||||
espectro
|
espectro
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue