diff --git a/agent-ctx/10-presets-ui.md b/agent-ctx/10-presets-ui.md new file mode 100644 index 0000000..6d3a676 --- /dev/null +++ b/agent-ctx/10-presets-ui.md @@ -0,0 +1,86 @@ +# Task 10 — Presets UI (folder/file tree manager + Script tab save targets) + +Task ID: 10 +Agent: full-stack-developer (Presets UI) +Status: COMPLETE — lint clean, tsc clean on touched files, verified end-to-end in a headless browser. + +## What was built + +### 1. `src/components/ava/presets-tab.tsx` — complete rewrite + +**Left column (58% on sm+, full width on mobile): the folder/file tree** +- Built from `store.folders` + `store.presets`. Defensive: presets whose + `folderPath` has no Folder row synthesize their node + ancestors + (`buildTree`). Folders sorted alphabetically per level (segment-wise + compare, parents before children); presets alphabetically inside folders. +- Folder row: chevron button (size-9 hit area, rotates ChevronRight) is the + ONLY expand/collapse control; clicking the row selects the folder + (`selectFolder` — also updates the randomize scope). FolderOpen/Folder + icon swaps with expansion. Subtree preset count badge (font-mono, + tooltip "N presets in this folder and below"). +- Default expansion: `expandedFolders[path] ?? path.split('/').length <= 2` + (same rule as the store) — root, root/embedded, root/user created open. +- Preset row: FileText icon; single click selects, double-click loads. + Running preset (`activePresetId`): emerald Play icon + emerald-300 name + + `bg-emerald-400/10`. Selected row: emerald left border + `bg-zinc-800/70`. +- `originalFile` rows get a .avs download anchor (title "Download original + Winamp .avs"), always visible on mobile, hover-visible on `sm+`. +- Empty folders show an italic "(empty)" hint row. Tree body scrolls + (`max-h-[420px] lg:max-h-[480px]`, thinScrollbar). `role="tree"` / + `treeitem` / `group`, aria-expanded/selected/level throughout. + +**Right column (42%): context-sensitive management panel** +- Header: icon + name + font-mono full path (+ "N presets · M subfolders" + for folders). +- Preset context: LOAD PRESET (emerald; applyScriptText → toast → + setActivePresetId + setEditTarget; compile error → destructive toast + + switch to script tab), RENAME, DELETE. +- Folder context: CREATE PRESET (POST STARTER_TEMPLATE → refresh → + editTarget + editor + engine + select + `setTab('script')` + toast + "created — now edit and Save"), NEW SUBFOLDER (POST → expand parent + + select new), RENAME (PATCH folder / PUT preset; selection, randomScope + and editTarget follow the renamed paths), DELETE (AlertDialog with + client-side blast radius — "removes N presets in M folders", empty-folder + and preset variants), EXPORT .ZIP (asChild anchor to + `/api/presets/export?path=…` with download attr; root exports + everything), IMPORT .ZIP (hidden .zip input → FormData POST → summary + toast with duplicates-skipped / folders-created). +- PROTECTED_FOLDERS (root, root/embedded, root/user created): Rename + + Delete disabled with "Protected folder" tooltip. Import/export allowed. +- All 409/400/404/500 responses → destructive toasts with the server + message. Empty names → inline dialog validation. +- RANDOMIZE section (border-t): Checkbox + delay input (3–60 s; invalid + input shows a red hint and reverts to the committed store value on + blur), scope line ("Random from: ") with amber + "no presets in this folder yet" hint, pulsing emerald "ON · every Ns" + badge while active. The timer runs in the store; the UI only wires it. +- NameDialog is fully controlled (parent owns value/error) — this was + needed to pass the `react-hooks/set-state-in-effect` lint rule. + +### 2. `src/components/ava/script-tab.tsx` — edits +- "Saving to: root/…/Name" chip (middle-truncated path, emerald, X button + clears the target) shown while `editTarget` is set. +- Save button (emerald, editTarget only): PUT `/api/presets/[id]` + {content, author from validateScript meta} → refresh + toast + `Saved to "Name"`; disabled while saving. +- Save As… button (editTarget only): Dialog prefilled with current name → + POST same folder → setEditTarget(new) + refresh + toast; 409 handled. +- New button also clears the edit target. + +## Verification performed (headless browser + curl) +- Tree structure/counts/expansion/selection; Load (toast, chip appears, + Save PUT returns 200); Save As (copy created server-side, chip + retargeted); delete preset + folder (blast-radius + empty wording); + create preset (editor + tab switch); new subfolder; rename folder + (paths follow); import .zip end-to-end; randomize persistence + (localStorage `ava.randomize`), scope line, invalid-delay revert, ON + badge in DOM with pulse animation. Zero page/console errors. +- Library restored to pristine state after testing (95 presets / 7 folders). + +## Notes for future agents +- Do NOT re-add setState-in-effect in NameDialog — the react-hooks v6 rule + forbids it; keep it controlled. +- `setRandomIntervalSec` is typed `(sec: number)`; convert with `Number()` + before calling (the store would tolerate strings at runtime). +- agent-browser `find … dblclick` does not exist — double-click was + verified through the identical Load Preset code path. diff --git a/db/custom.db b/db/custom.db index da8abda..76317ff 100755 Binary files a/db/custom.db and b/db/custom.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 10cfd4b..47a9cdd 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,15 +7,32 @@ datasource db { url = env("DATABASE_URL") } +// A preset script file living somewhere in the folder tree. +// folderPath is the full path from the virtual root, e.g. "root/embedded/Synth-C". +// Names are unique per folder (files in a directory). model Preset { - id String @id @default(cuid()) - name String @unique - author String @default("") - description String @default("") - content String - isBuiltIn Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + name String + author String @default("") + description String @default("") + content String + isBuiltIn Boolean @default(false) + folderPath String @default("root/user created") + // Path to the original Winamp .avs binary in /public (downloadable), if any. + originalFile String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([folderPath, name]) +} + +// A folder in the preset tree. The root ("/") and the two top-level areas +// "root/embedded" and "root/user created" are structural and protected from +// rename/delete; everything below is fully user-manageable. +model Folder { + id String @id @default(cuid()) + path String @unique + createdAt DateTime @default(now()) } model Settings { diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/D&L - Life Is Violated.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/D&L - Life Is Violated.avs new file mode 100644 index 0000000..94b4f15 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/D&L - Life Is Violated.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).avs new file mode 100644 index 0000000..4a9e8c5 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic-Adrealine rush.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic-Adrealine rush.avs new file mode 100644 index 0000000..c0f3335 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Degnic-Adrealine rush.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Alien Heart.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Alien Heart.avs new file mode 100644 index 0000000..3fa7554 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Alien Heart.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Brainstorm.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Brainstorm.avs new file mode 100644 index 0000000..421a551 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Duo - Brainstorm.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).avs new file mode 100644 index 0000000..42c2a0f Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/L1quid - Take the Veil.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/L1quid - Take the Veil.avs new file mode 100644 index 0000000..edeffe9 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/L1quid - Take the Veil.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Nic01 - Rototronik.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Nic01 - Rototronik.avs new file mode 100644 index 0000000..ecce5cf Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Nic01 - Rototronik.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....avs new file mode 100644 index 0000000..ed497e5 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.avs new file mode 100644 index 0000000..5922f76 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Chinook.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Chinook.avs new file mode 100644 index 0000000..59d52d5 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Chinook.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.avs new file mode 100644 index 0000000..2c508f5 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.avs new file mode 100644 index 0000000..448639d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Kingish.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Kingish.avs new file mode 100644 index 0000000..43e5248 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Kingish.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).avs new file mode 100644 index 0000000..03db3ca Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Texering 002.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Texering 002.avs new file mode 100644 index 0000000..bb8a53f Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Texering 002.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Upward Current.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Upward Current.avs new file mode 100644 index 0000000..0a5a4a0 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/S_KuPeRS - Upward Current.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.avs new file mode 100644 index 0000000..ed66c2e Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).avs new file mode 100644 index 0000000..8ba799d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Electro blobs.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Electro blobs.avs new file mode 100644 index 0000000..dca6334 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Electro blobs.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Kaleidofied.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Kaleidofied.avs new file mode 100644 index 0000000..bb5dc04 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Kaleidofied.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Magma Flow II.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Magma Flow II.avs new file mode 100644 index 0000000..ef68862 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Magma Flow II.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.avs new file mode 100644 index 0000000..6e0c355 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Pina Colada.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Pina Colada.avs new file mode 100644 index 0000000..ac47473 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Pina Colada.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.avs new file mode 100644 index 0000000..1d34b82 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Solero Shots.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Solero Shots.avs new file mode 100644 index 0000000..086b3e6 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Solero Shots.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Square Minded.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Square Minded.avs new file mode 100644 index 0000000..52afb5c Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Square Minded.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).avs new file mode 100644 index 0000000..c5974e2 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze.avs new file mode 100644 index 0000000..67e6e48 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cartoon Trip.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cartoon Trip.avs new file mode 100644 index 0000000..60849e4 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cartoon Trip.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Containment.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Containment.avs new file mode 100644 index 0000000..d7518bc Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Containment.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cubic Flower.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cubic Flower.avs new file mode 100644 index 0000000..5d474ef Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Cubic Flower.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Jello Cube.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Jello Cube.avs new file mode 100644 index 0000000..ff7840a Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Jello Cube.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Mercury.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Mercury.avs new file mode 100644 index 0000000..ac0560e Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Mercury.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Neon Coaster.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Neon Coaster.avs new file mode 100644 index 0000000..027a28d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Neon Coaster.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Plankton.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Plankton.avs new file mode 100644 index 0000000..e52b1b0 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Plankton.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Reflectocube.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Reflectocube.avs new file mode 100644 index 0000000..eda5859 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Reflectocube.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Speeder 3K.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Speeder 3K.avs new file mode 100644 index 0000000..eb4c78d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Speeder 3K.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - TC Plasma.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - TC Plasma.avs new file mode 100644 index 0000000..48f7545 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - TC Plasma.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.avs new file mode 100644 index 0000000..8624b4a Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.avs new file mode 100644 index 0000000..8a49451 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.avs new file mode 100644 index 0000000..ddd9298 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).avs new file mode 100644 index 0000000..5fea8c8 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).avs new file mode 100644 index 0000000..795462d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).avs new file mode 100644 index 0000000..7d9ca80 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).avs new file mode 100644 index 0000000..9b9ec2c Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).avs new file mode 100644 index 0000000..658c25e Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/justin - newpoop (for wa5).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/justin - newpoop (for wa5).avs new file mode 100644 index 0000000..36813ba Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/justin - newpoop (for wa5).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).avs new file mode 100644 index 0000000..294375a Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Dragonscales.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Dragonscales.avs new file mode 100644 index 0000000..3dce1a1 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Dragonscales.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Encapsulade.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Encapsulade.avs new file mode 100644 index 0000000..3f3bfb1 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Encapsulade.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Freekwyrms.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Freekwyrms.avs new file mode 100644 index 0000000..e8e2fab Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Freekwyrms.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Lilypod.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Lilypod.avs new file mode 100644 index 0000000..e57591f Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Lilypod.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Mist Wisps.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Mist Wisps.avs new file mode 100644 index 0000000..16952bc Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Mist Wisps.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.avs new file mode 100644 index 0000000..bfa4783 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Reverb - Deep Net.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Reverb - Deep Net.avs new file mode 100644 index 0000000..4dca394 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Reverb - Deep Net.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Slyde - Tri.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Slyde - Tri.avs new file mode 100644 index 0000000..4cedd6d Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Slyde - Tri.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Solar Flaire.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Solar Flaire.avs new file mode 100644 index 0000000..52fb0f2 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Solar Flaire.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Spintronics - Backspin.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Spintronics - Backspin.avs new file mode 100644 index 0000000..99c23c3 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Spintronics - Backspin.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Starslappin.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Starslappin.avs new file mode 100644 index 0000000..48acd17 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Starslappin.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Street Art - Fire Inside.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Street Art - Fire Inside.avs new file mode 100644 index 0000000..d105a6e Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Street Art - Fire Inside.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Swoo - Slow.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Swoo - Slow.avs new file mode 100644 index 0000000..bda397b Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/mig - Swoo - Slow.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/nixa - smell of music (skupers remix).avs b/public/avs-originals/embedded/root/embedded/Amp Picks/nixa - smell of music (skupers remix).avs new file mode 100644 index 0000000..6c50626 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/nixa - smell of music (skupers remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/p-k - bumpy.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/p-k - bumpy.avs new file mode 100644 index 0000000..3a7e029 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/p-k - bumpy.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/splendora.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/splendora.avs new file mode 100644 index 0000000..05d6129 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/splendora.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Amp Picks/zxe-Urantia.avs b/public/avs-originals/embedded/root/embedded/Amp Picks/zxe-Urantia.avs new file mode 100644 index 0000000..ab87d90 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Amp Picks/zxe-Urantia.avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).avs b/public/avs-originals/embedded/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).avs new file mode 100644 index 0000000..2cd3f68 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).avs b/public/avs-originals/embedded/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).avs new file mode 100644 index 0000000..46859ad Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).avs b/public/avs-originals/embedded/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).avs new file mode 100644 index 0000000..6e5a049 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).avs b/public/avs-originals/embedded/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).avs new file mode 100644 index 0000000..9c13dbf Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).avs b/public/avs-originals/embedded/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).avs new file mode 100644 index 0000000..b118d72 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).avs b/public/avs-originals/embedded/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).avs new file mode 100644 index 0000000..da36cb4 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).avs b/public/avs-originals/embedded/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).avs new file mode 100644 index 0000000..bdadcc5 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).avs b/public/avs-originals/embedded/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).avs new file mode 100644 index 0000000..5aba88c Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).avs b/public/avs-originals/embedded/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).avs new file mode 100644 index 0000000..bab3906 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).avs b/public/avs-originals/embedded/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).avs new file mode 100644 index 0000000..425e8fd Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).avs b/public/avs-originals/embedded/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).avs new file mode 100644 index 0000000..fc6551e Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).avs b/public/avs-originals/embedded/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).avs new file mode 100644 index 0000000..c4ef188 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).avs b/public/avs-originals/embedded/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).avs new file mode 100644 index 0000000..956c25a Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/06 skupers - incantation (duo remix - edit).avs b/public/avs-originals/embedded/root/embedded/Envision/06 skupers - incantation (duo remix - edit).avs new file mode 100644 index 0000000..fc151ca Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/06 skupers - incantation (duo remix - edit).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/07 duo - miraculous flow (nemo remix).avs b/public/avs-originals/embedded/root/embedded/Envision/07 duo - miraculous flow (nemo remix).avs new file mode 100644 index 0000000..c1891ef Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/07 duo - miraculous flow (nemo remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/08 duo - warm freeform (mykal remix).avs b/public/avs-originals/embedded/root/embedded/Envision/08 duo - warm freeform (mykal remix).avs new file mode 100644 index 0000000..e5fea86 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/08 duo - warm freeform (mykal remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/09 zxe - pleasure point (duo remix).avs b/public/avs-originals/embedded/root/embedded/Envision/09 zxe - pleasure point (duo remix).avs new file mode 100644 index 0000000..ed854eb Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/09 zxe - pleasure point (duo remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).avs b/public/avs-originals/embedded/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).avs new file mode 100644 index 0000000..91951f3 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).avs b/public/avs-originals/embedded/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).avs new file mode 100644 index 0000000..b1e8647 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).avs b/public/avs-originals/embedded/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).avs new file mode 100644 index 0000000..1d8dd15 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).avs b/public/avs-originals/embedded/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).avs new file mode 100644 index 0000000..fb3b493 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).avs b/public/avs-originals/embedded/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).avs new file mode 100644 index 0000000..3f744eb Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).avs b/public/avs-originals/embedded/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).avs new file mode 100644 index 0000000..e4a1bc3 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).avs differ diff --git a/public/avs-originals/embedded/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).avs b/public/avs-originals/embedded/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).avs new file mode 100644 index 0000000..0a74663 Binary files /dev/null and b/public/avs-originals/embedded/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).avs differ diff --git a/src/app/api/folders/route.ts b/src/app/api/folders/route.ts new file mode 100644 index 0000000..c183556 --- /dev/null +++ b/src/app/api/folders/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { + PROTECTED_FOLDERS, + isValidName, + normalizeFolderPath, + parentOf, + subtreeFilter, + subtreeFolderFilter, +} from '@/lib/server/preset-tree' + +export const dynamic = 'force-dynamic' + +/** Create a subfolder inside an existing folder. Body: { path: "root/a/b" } */ +export async function POST(req: NextRequest) { + try { + const body = await req.json() + const folder = normalizeFolderPath(body?.path) + if (!folder) { + return NextResponse.json({ error: 'Invalid folder path' }, { status: 400 }) + } + if (folder.path === 'root') { + return NextResponse.json({ error: '"root" already exists' }, { status: 409 }) + } + if (PROTECTED_FOLDERS.has(folder.path)) { + return NextResponse.json({ error: 'That folder already exists' }, { status: 409 }) + } + const existing = await db.folder.findUnique({ where: { path: folder.path } }) + if (existing) { + return NextResponse.json({ error: `"${folder.path}" already exists` }, { status: 409 }) + } + const parent = parentOf(folder.segments) + const parentRow = await db.folder.findUnique({ where: { path: parent } }) + if (!parentRow) { + return NextResponse.json( + { error: `Parent folder "${parent}" does not exist` }, + { status: 400 }, + ) + } + await db.folder.create({ data: { path: folder.path } }) + return NextResponse.json({ folder: { path: folder.path } }, { status: 201 }) + } catch (err) { + console.error('POST /api/folders failed:', err) + return NextResponse.json({ error: 'Failed to create folder' }, { status: 500 }) + } +} + +/** Rename a folder (and everything below it). Body: { path, newName } */ +export async function PATCH(req: NextRequest) { + try { + const body = await req.json() + const folder = normalizeFolderPath(body?.path) + const newName = isValidName(body?.newName, 80) + if (!folder || !newName) { + return NextResponse.json({ error: 'Invalid folder or name' }, { status: 400 }) + } + if (PROTECTED_FOLDERS.has(folder.path)) { + return NextResponse.json( + { error: `"${folder.segments[folder.segments.length - 1]}" is a protected folder and cannot be renamed` }, + { status: 400 }, + ) + } + const row = await db.folder.findUnique({ where: { path: folder.path } }) + if (!row) { + return NextResponse.json({ error: 'Folder not found' }, { status: 404 }) + } + const newPath = `${parentOf(folder.segments)}/${newName}` + const clash = await db.folder.findUnique({ where: { path: newPath } }) + if (clash) { + return NextResponse.json({ error: `"${newName}" already exists there` }, { status: 409 }) + } + + const result = await db.$transaction(async (tx) => { + const folders = await tx.folder.findMany({ where: subtreeFolderFilter(folder.path) }) + const presets = await tx.preset.findMany({ + where: subtreeFilter(folder.path), + select: { id: true, folderPath: true }, + }) + for (const f of folders) { + const target = f.path === folder.path ? newPath : newPath + f.path.slice(folder.path.length) + await tx.folder.update({ where: { path: f.path }, data: { path: target } }) + } + for (const p of presets) { + await tx.preset.update({ + where: { id: p.id }, + data: { folderPath: newPath + p.folderPath.slice(folder.path.length) }, + }) + } + return { folders: folders.length, presets: presets.length } + }) + + return NextResponse.json({ renamed: folder.path, to: newPath, ...result }) + } catch (err) { + if ((err as { code?: string })?.code === 'P2002') { + return NextResponse.json({ error: 'A folder with that name already exists' }, { status: 409 }) + } + console.error('PATCH /api/folders failed:', err) + return NextResponse.json({ error: 'Failed to rename folder' }, { status: 500 }) + } +} + +/** Delete a folder subtree. Query: ?path=root/a/b */ +export async function DELETE(req: NextRequest) { + try { + const raw = new URL(req.url).searchParams.get('path') + const folder = normalizeFolderPath(raw) + if (!folder) { + return NextResponse.json({ error: 'Invalid folder path' }, { status: 400 }) + } + if (PROTECTED_FOLDERS.has(folder.path)) { + return NextResponse.json( + { error: `"${folder.segments[folder.segments.length - 1]}" is a protected folder and cannot be deleted` }, + { status: 400 }, + ) + } + const row = await db.folder.findUnique({ where: { path: folder.path } }) + if (!row) { + return NextResponse.json({ error: 'Folder not found' }, { status: 404 }) + } + const deletedPresets = await db.preset.deleteMany({ where: subtreeFilter(folder.path) }) + const deletedFolders = await db.folder.deleteMany({ where: subtreeFolderFilter(folder.path) }) + return NextResponse.json({ + deleted: true, + folders: deletedFolders.count, + presets: deletedPresets.count, + }) + } catch (err) { + console.error('DELETE /api/folders failed:', err) + return NextResponse.json({ error: 'Failed to delete folder' }, { status: 500 }) + } +} diff --git a/src/app/api/presets/[id]/route.ts b/src/app/api/presets/[id]/route.ts index d897573..80a8fd3 100755 --- a/src/app/api/presets/[id]/route.ts +++ b/src/app/api/presets/[id]/route.ts @@ -1,8 +1,10 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' +import { isValidName } from '@/lib/server/preset-tree' export const dynamic = 'force-dynamic' +/** Update a preset (content, author, description, or rename within its folder). */ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params @@ -11,14 +13,29 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: if (!preset) { return NextResponse.json({ error: 'Preset not found' }, { status: 404 }) } - if (preset.isBuiltIn) { - return NextResponse.json({ error: 'Built-in presets cannot be modified' }, { status: 403 }) - } + const data: Record = {} - if (typeof body.name === 'string' && body.name.trim()) data.name = body.name.trim() - if (typeof body.content === 'string') data.content = body.content - if (typeof body.author === 'string') data.author = body.author.trim() - if (typeof body.description === 'string') data.description = body.description.trim() + if (typeof body?.name === 'string' && body.name.trim() && body.name.trim() !== preset.name) { + const name = isValidName(body.name) + if (!name) { + return NextResponse.json({ error: 'Invalid preset name' }, { status: 400 }) + } + const clash = await db.preset.findFirst({ + where: { folderPath: preset.folderPath, name }, + select: { id: true }, + }) + if (clash && clash.id !== id) { + return NextResponse.json( + { error: `A preset named "${name}" already exists in ${preset.folderPath}` }, + { status: 409 }, + ) + } + data.name = name + } + if (typeof body?.content === 'string') data.content = body.content + if (typeof body?.author === 'string') data.author = body.author.trim().slice(0, 120) + if (typeof body?.description === 'string') data.description = body.description.trim().slice(0, 500) + const updated = await db.preset.update({ where: { id }, data }) return NextResponse.json({ preset: updated }) } catch (err) { @@ -27,6 +44,7 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: } } +/** Delete a preset (any preset — the tree is fully user-manageable). */ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params @@ -34,9 +52,6 @@ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ if (!preset) { return NextResponse.json({ error: 'Preset not found' }, { status: 404 }) } - if (preset.isBuiltIn) { - return NextResponse.json({ error: 'Built-in presets cannot be deleted' }, { status: 403 }) - } await db.preset.delete({ where: { id } }) return NextResponse.json({ deleted: true }) } catch (err) { diff --git a/src/app/api/presets/export/route.ts b/src/app/api/presets/export/route.ts new file mode 100644 index 0000000..5e6071d --- /dev/null +++ b/src/app/api/presets/export/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' +import JSZip from 'jszip' +import { db } from '@/lib/db' +import { normalizeFolderPath, subtreeFilter, subtreeFolderFilter } from '@/lib/server/preset-tree' + +export const dynamic = 'force-dynamic' + +/** + * Export a folder (and everything below it) as a .zip that preserves the + * directory structure from the virtual root — e.g. exporting + * "root/user created/My Fav Presets" produces entries under + * "root/user created/My Fav Presets/.ava" so the zip can be + * re-imported anywhere with the same tree. + * + * GET /api/presets/export?path=root%2Fembedded%2FAmp%20Picks + */ +export async function GET(req: NextRequest) { + try { + const raw = new URL(req.url).searchParams.get('path') ?? 'root' + const folder = normalizeFolderPath(raw) + if (!folder) { + return NextResponse.json({ error: 'Invalid folder path' }, { status: 400 }) + } + if (folder.path !== 'root') { + const row = await db.folder.findUnique({ where: { path: folder.path } }) + if (!row) { + return NextResponse.json({ error: 'Folder not found' }, { status: 404 }) + } + } + + const [folders, presets] = await Promise.all([ + db.folder.findMany({ where: subtreeFolderFilter(folder.path), select: { path: true } }), + db.preset.findMany({ + where: subtreeFilter(folder.path), + select: { name: true, content: true, folderPath: true }, + }), + ]) + + // folder set = stored rows ∪ every ancestor of every preset's path + const folderSet = new Set(folders.map((f) => f.path)) + for (const p of presets) { + const segs = p.folderPath.split('/') + for (let i = 1; i <= segs.length; i++) folderSet.add(segs.slice(0, i).join('/')) + } + + const zip = new JSZip() + for (const fp of folderSet) zip.folder(fp) + for (const p of presets) zip.file(`${p.folderPath}/${p.name}.ava`, p.content) + + const data = await zip.generateAsync({ type: 'uint8array' }) + // copy into a plain ArrayBuffer-backed view (BodyInit/BlobPart typing) + const body = new Uint8Array(data.byteLength) + body.set(data) + const label = folder.segments[folder.segments.length - 1] ?? 'root' + const safeLabel = label.replace(/["\u0000-\u001f\u007f]/g, '').trim() || 'root' + return new NextResponse(body, { + status: 200, + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="AVA presets - ${safeLabel}.zip"`, + 'Content-Length': String(data.length), + 'Cache-Control': 'no-store', + }, + }) + } catch (err) { + console.error('GET /api/presets/export failed:', err) + return NextResponse.json({ error: 'Failed to export presets' }, { status: 500 }) + } +} diff --git a/src/app/api/presets/import/route.ts b/src/app/api/presets/import/route.ts new file mode 100644 index 0000000..b8e05a9 --- /dev/null +++ b/src/app/api/presets/import/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from 'next/server' +import JSZip from 'jszip' +import { db } from '@/lib/db' +import { ensureFolderRows, isValidName, normalizeFolderPath } from '@/lib/server/preset-tree' + +export const dynamic = 'force-dynamic' + +const MAX_ZIP_BYTES = 50 * 1024 * 1024 + +interface ImportEntry { + folderPath: string + name: string + content: string +} + +/** + * Import a .zip of presets, preserving its directory tree. Entries stored + * under "root/…" land at exactly that path; anything else is placed inside + * the target folder selected in the UI. Only .ava (and .txt) script files + * are imported — other files are counted and skipped. + * + * POST /api/presets/import (multipart/form-data: file, targetPath) + */ +export async function POST(req: NextRequest) { + try { + const form = await req.formData() + const file = form.get('file') + if (!(file instanceof File)) { + return NextResponse.json({ error: 'Attach a .zip file to import' }, { status: 400 }) + } + if (file.size > MAX_ZIP_BYTES) { + return NextResponse.json({ error: 'That zip is too large (50 MB limit)' }, { status: 413 }) + } + const target = normalizeFolderPath(form.get('targetPath') ?? 'root/user created') + if (!target) { + return NextResponse.json({ error: 'Invalid target folder' }, { status: 400 }) + } + + const zip = await JSZip.loadAsync(await file.arrayBuffer()) + + const entries: ImportEntry[] = [] + let skippedOther = 0 + for (const entry of Object.values(zip.files)) { + if (entry.dir) continue + const rawPath = entry.name.replace(/\\/g, '/') + if (rawPath.includes('__MACOSX') || rawPath.split('/').some((s) => s.startsWith('.'))) continue + if (!/\.(ava|txt)$/i.test(rawPath)) { + skippedOther++ + continue + } + const segs = rawPath + .split('/') + .map((s) => s.trim()) + .filter((s) => s.length > 0 && s !== '.' && s !== '..') + if (segs.length === 0) continue + const fileName = segs[segs.length - 1] + const name = isValidName(fileName.replace(/\.(ava|txt)$/i, '')) + if (!name) { + skippedOther++ + continue + } + const dirSegs = segs.slice(0, -1) + const joined = dirSegs.length > 0 ? dirSegs.join('/') : null + const folderPath = joined + ? joined === 'root' || joined.startsWith('root/') + ? joined + : `${target.path}/${joined}` + : target.path + const norm = normalizeFolderPath(folderPath) + if (!norm) { + skippedOther++ + continue + } + const content = await entry.async('string') + entries.push({ folderPath: norm.path, name, content }) + } + + if (entries.length === 0) { + return NextResponse.json( + { error: 'No .ava preset files found in that zip' }, + { status: 400 }, + ) + } + + const foldersCreated = await ensureFolderRows([...new Set(entries.map((e) => e.folderPath))]) + + let imported = 0 + let skipped = 0 + for (const e of entries) { + const clash = await db.preset.findFirst({ + where: { folderPath: e.folderPath, name: e.name }, + select: { id: true }, + }) + if (clash) { + skipped++ + continue + } + const author = + /^author:\s*(.+)$/m + .exec(e.content)?.[1] + ?.replace(/^["']|["']$/g, '') + .trim() + .slice(0, 120) ?? '' + await db.preset.create({ + data: { + name: e.name, + content: e.content, + author, + folderPath: e.folderPath, + isBuiltIn: false, + }, + }) + imported++ + } + + return NextResponse.json({ imported, skipped, skippedOther, foldersCreated }) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + if (message.includes('corrupted') || message.includes('zip')) { + return NextResponse.json({ error: 'That file is not a valid zip archive' }, { status: 400 }) + } + console.error('POST /api/presets/import failed:', err) + return NextResponse.json({ error: 'Failed to import presets' }, { status: 500 }) + } +} diff --git a/src/app/api/presets/route.ts b/src/app/api/presets/route.ts index c182056..0cfd7b1 100755 --- a/src/app/api/presets/route.ts +++ b/src/app/api/presets/route.ts @@ -1,42 +1,86 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' import { BUILTIN_PRESETS } from '@/lib/ava/presets' +import { EMBEDDED_LIBRARY } from '@/lib/ava/embedded-presets' +import { ensureFolderRows, isValidName, normalizeFolderPath } from '@/lib/server/preset-tree' export const dynamic = 'force-dynamic' -/** Seed the built-in presets on first run. */ +/** The structural backbone of the tree, created on first run. */ +const BASE_FOLDERS = [ + 'root', + 'root/embedded', + 'root/user created', + 'root/embedded/Synth-C', + 'root/embedded/Amp Picks', + 'root/embedded/Dynamics', + 'root/embedded/Envision', +] + +/** + * One-time seed: folder backbone + the full embedded library + * (5 hand-ported classics + 90 auto-ported Winamp AVS presets from the + * user's Google Drive collection, organized exactly as the drive folders + * were: Synth-C / Amp Picks / Dynamics / Envision). + */ async function ensureSeeded(): Promise { - const count = await db.preset.count() - if (count > 0) return - for (const p of BUILTIN_PRESETS) { - await db.preset.create({ - data: { - name: p.name, - author: p.author, - description: p.description, - content: p.content, - isBuiltIn: true, - }, + const folderCount = await db.folder.count() + if (folderCount > 0) return + try { + await db.folder.createMany({ data: BASE_FOLDERS.map((path) => ({ path })) }) + // Migrate legacy pre-folder databases: old built-ins sat at the top level. + await db.preset.deleteMany({ where: { isBuiltIn: true } }) + await db.preset.createMany({ + data: [ + ...BUILTIN_PRESETS.map((p) => ({ + name: p.name, + author: p.author, + description: p.description, + content: p.content, + originalFile: p.originalFile ?? null, + isBuiltIn: true, + folderPath: 'root/embedded/Synth-C', + })), + ...EMBEDDED_LIBRARY.map((p) => ({ + name: p.name, + author: p.author, + description: p.description, + content: p.content, + originalFile: p.originalFile ?? null, + isBuiltIn: true, + // the converter recorded the download nesting — normalize to the + // clean tree ("root/embedded/root/embedded/Amp Picks" → "root/embedded/Amp Picks") + folderPath: p.folder.replace('root/embedded/root/embedded/', 'root/embedded/'), + })), + ], }) + } catch (err) { + // Two first requests racing → unique violation: the other request seeded. + if ((err as { code?: string })?.code !== 'P2002') throw err } } export async function GET() { try { await ensureSeeded() - const presets = await db.preset.findMany({ - orderBy: [{ isBuiltIn: 'desc' }, { createdAt: 'asc' }], - select: { - id: true, - name: true, - author: true, - description: true, - content: true, - isBuiltIn: true, - updatedAt: true, - }, - }) - return NextResponse.json({ presets }) + const [presets, folders] = await Promise.all([ + db.preset.findMany({ + orderBy: [{ folderPath: 'asc' }, { name: 'asc' }], + select: { + id: true, + name: true, + author: true, + description: true, + content: true, + isBuiltIn: true, + folderPath: true, + originalFile: true, + updatedAt: true, + }, + }), + db.folder.findMany({ orderBy: { path: 'asc' }, select: { path: true } }), + ]) + return NextResponse.json({ presets, folders: folders.map((f) => f.path) }) } catch (err) { console.error('GET /api/presets failed:', err) return NextResponse.json({ error: 'Failed to load presets' }, { status: 500 }) @@ -46,32 +90,36 @@ export async function GET() { export async function POST(req: NextRequest) { try { const body = await req.json() - const name = typeof body.name === 'string' ? body.name.trim() : '' - const content = typeof body.content === 'string' ? body.content : '' - const author = typeof body.author === 'string' ? body.author.trim() : '' - const description = typeof body.description === 'string' ? body.description.trim() : '' + const name = isValidName(body?.name) + const content = typeof body?.content === 'string' ? body.content : '' + const author = typeof body?.author === 'string' ? body.author.trim().slice(0, 120) : '' + const description = typeof body?.description === 'string' ? body.description.trim().slice(0, 500) : '' + const folder = normalizeFolderPath(body?.folderPath ?? 'root/user created') if (!name || !content) { return NextResponse.json({ error: 'name and content are required' }, { status: 400 }) } - - const existing = await db.preset.findUnique({ where: { name } }) - if (existing?.isBuiltIn) { + if (!folder) { + return NextResponse.json({ error: 'Invalid folder path' }, { status: 400 }) + } + const folderRow = await db.folder.findUnique({ where: { path: folder.path } }) + if (!folderRow) { + // tolerate missing ancestor rows (e.g. very old data) by creating them + await ensureFolderRows([folder.path]) + } + const clash = await db.preset.findFirst({ + where: { folderPath: folder.path, name }, + select: { id: true }, + }) + if (clash) { return NextResponse.json( - { error: `'${name}' is a built-in preset — save under a different name` }, - { status: 409 } + { error: `A preset named "${name}" already exists in ${folder.path}` }, + { status: 409 }, ) } - if (existing) { - const updated = await db.preset.update({ - where: { id: existing.id }, - data: { content, author, description }, - }) - return NextResponse.json({ preset: updated, updated: true }) - } const preset = await db.preset.create({ - data: { name, content, author, description, isBuiltIn: false }, + data: { name, content, author, description, folderPath: folder.path, isBuiltIn: false }, }) - return NextResponse.json({ preset, created: true }) + return NextResponse.json({ preset, created: true }, { status: 201 }) } catch (err) { console.error('POST /api/presets failed:', err) return NextResponse.json({ error: 'Failed to save preset' }, { status: 500 }) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4a2bbc9..8246215 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -23,7 +23,9 @@ import { lsGetString, readStoredBandConfig, readStoredBeatConfig, + restoreRandomSettings, useAvaStore, + type PresetItem, } from '@/lib/store' /** @@ -89,8 +91,10 @@ function useInitialLoad() { if (cancelled) return const store = useAvaStore.getState() - const presetsList = (presetsRes?.presets ?? []) as { content?: string }[] + const presetsList = (presetsRes?.presets ?? []) as PresetItem[] if (presetsRes?.presets) store.setPresets(presetsRes.presets) + const folders: string[] = presetsRes?.folders ?? [] + if (folders.length) store.setFolders(folders) const settings = (settingsRes ?? {}) as { defaultScript?: string | null @@ -103,12 +107,21 @@ function useInitialLoad() { const beat = isBeatConfig(settings.beatConfig) ? settings.beatConfig : readStoredBeatConfig() if (beat) store.applyBeatConfig(beat) - // startup script: saved default → localStorage backup → first preset → template + // startup script: saved default → localStorage backup → first classic → template let text: string | null = typeof settings.defaultScript === 'string' ? settings.defaultScript : null if (!text) text = lsGetString('ava.defaultScript') - if (!text) text = presetsList[0]?.content ?? null + let startupPreset: PresetItem | undefined + if (!text) { + startupPreset = + presetsList.find((p) => p.folderPath === 'root/embedded/Synth-C') ?? presetsList[0] + text = startupPreset?.content ?? null + } if (!text) text = STARTER_TEMPLATE applyScriptText(text) + if (startupPreset) store.setActivePresetId(startupPreset.id) + + // club mode: bring back the random-preset timer if it was on + restoreRandomSettings(folders.length ? folders : store.folders) })() return () => { cancelled = true diff --git a/src/components/ava/presets-tab.tsx b/src/components/ava/presets-tab.tsx index f0824a1..a794aad 100755 --- a/src/components/ava/presets-tab.tsx +++ b/src/components/ava/presets-tab.tsx @@ -1,146 +1,644 @@ -// AVA — Presets tab: load/delete preset scripts, download the original .avs -// binaries for the built-ins, save the current editor script as a preset. +// AVA — Presets tab: folder/file tree manager (Task 10 redesign). +// +// Left column: the preset library tree — folders expand/collapse (chevron +// only), rows select, double-clicking a preset runs it, the running preset +// is flagged emerald, and originals download straight from the row. +// Right column: context-sensitive management (load / create / rename / +// delete / export .zip / import .zip) plus the club-mode randomizer — the +// timer itself lives in the store, this tab only wires the controls. 'use client' -import { useEffect, useState } from 'react' -import { Download, FilePlus2, Play, Save, Trash2 } from 'lucide-react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + ChevronRight, + Download, + FilePlus2, + FileText, + Folder, + FolderOpen, + FolderPlus, + Pencil, + Play, + Trash2, + Upload, +} from 'lucide-react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' -import { originalFileUrl, STARTER_TEMPLATE } from '@/lib/ava/presets' -import { validateScript } from '@/lib/ava/script' +import { Label } from '@/components/ui/label' +import { STARTER_TEMPLATE } from '@/lib/ava/presets' import { refreshPresets, useAvaStore, type PresetItem } from '@/lib/store' import { applyScriptText } from '@/components/ava/audio-source' -import { modeOfScript, thinScrollbar } from '@/components/ava/shared' +import { thinScrollbar } from '@/components/ava/shared' import { useToast } from '@/hooks/use-toast' import { cn } from '@/lib/utils' -function PresetCard({ preset }: { preset: PresetItem }) { +/** The tree backbone — the backend refuses to rename or delete these. */ +const PROTECTED_FOLDERS = new Set(['root', 'root/embedded', 'root/user created']) + +/** Same default-expansion rule as the store: paths with ≤ 2 segments open. */ +function defaultOpen(path: string): boolean { + return path.split('/').length <= 2 +} + +function plural(n: number, word: string): string { + return `${n} ${word}${n === 1 ? '' : 's'}` +} + +// --------------------------------------------------------------------------- +// tree construction +// --------------------------------------------------------------------------- + +interface FolderNode { + path: string + name: string + depth: number + children: FolderNode[] + presets: PresetItem[] + /** Presets in this folder and every folder below it. */ + subtreeCount: number +} + +/** Segment-wise alphabetical compare (a parent always sorts before its children). */ +function comparePaths(a: string, b: string): number { + const sa = a.split('/') + const sb = b.split('/') + const shared = Math.min(sa.length, sb.length) + for (let i = 0; i < shared; i++) { + const c = sa[i].toLowerCase().localeCompare(sb[i].toLowerCase()) + if (c !== 0) return c + } + return sa.length - sb.length +} + +/** + * Build the tree from folder rows + presets. Defensive: a preset whose + * folderPath has no Folder row still gets a node (and its ancestors). + */ +function buildTree(folders: string[], presets: PresetItem[]): FolderNode[] { + const pathSet = new Set(folders) + for (const p of presets) { + const segs = p.folderPath.split('/') + for (let i = 1; i <= segs.length; i++) pathSet.add(segs.slice(0, i).join('/')) + } + const paths = [...pathSet].sort(comparePaths) + + const nodes = new Map() + for (const path of paths) { + const segs = path.split('/') + nodes.set(path, { + path, + name: segs[segs.length - 1] || path, + depth: segs.length - 1, + children: [], + presets: [], + subtreeCount: 0, + }) + } + const roots: FolderNode[] = [] + for (const path of paths) { + const node = nodes.get(path) + if (!node) continue + const slash = path.lastIndexOf('/') + const parent = slash >= 0 ? nodes.get(path.slice(0, slash)) : undefined + if (parent) parent.children.push(node) + else roots.push(node) + } + for (const p of presets) nodes.get(p.folderPath)?.presets.push(p) + for (const node of nodes.values()) { + node.presets.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + } + const countSubtree = (node: FolderNode): number => { + node.subtreeCount = node.presets.length + for (const child of node.children) node.subtreeCount += countSubtree(child) + return node.subtreeCount + } + roots.forEach(countSubtree) + return roots +} + +// --------------------------------------------------------------------------- +// shared load action (Load Preset button + tree double-click) +// --------------------------------------------------------------------------- + +function useLoadPreset() { const { toast } = useToast() - const setTab = useAvaStore((s) => s.setTab) - const [confirmingDelete, setConfirmingDelete] = useState(false) - const [deleting, setDeleting] = useState(false) + return useCallback( + (preset: PresetItem) => { + const result = applyScriptText(preset.content) + const st = useAvaStore.getState() + if (result.ok) { + st.setActivePresetId(preset.id) + st.setEditTarget({ id: preset.id, name: preset.name, folderPath: preset.folderPath }) + toast({ title: 'Preset loaded', description: `"${preset.name}" is now running.` }) + } else if (result.error) { + toast({ + title: 'Preset failed to compile', + description: `[${result.error.section}] line ${result.error.line}: ${result.error.message}`, + variant: 'destructive', + }) + st.setTab('script') + } + }, + [toast], + ) +} - useEffect(() => { - if (!confirmingDelete) return - const t = setTimeout(() => setConfirmingDelete(false), 3000) - return () => clearTimeout(t) - }, [confirmingDelete]) +// --------------------------------------------------------------------------- +// tree rows +// --------------------------------------------------------------------------- - const load = () => { - const result = applyScriptText(preset.content) - if (result.ok) { - toast({ title: 'Preset loaded', description: `"${preset.name}" is now running.` }) - } else if (result.error) { - toast({ - title: 'Preset failed to compile', - description: `[${result.error.section}] line ${result.error.line}: ${result.error.message}`, - variant: 'destructive', - }) - setTab('script') - } - } - - const remove = async () => { - setDeleting(true) - try { - const res = await fetch(`/api/presets/${preset.id}`, { method: 'DELETE' }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - await refreshPresets() - toast({ title: 'Preset deleted', description: `"${preset.name}" removed.` }) - } catch { - toast({ title: 'Delete failed', description: 'Could not remove that preset.', variant: 'destructive' }) - } finally { - setDeleting(false) - setConfirmingDelete(false) - } - } - - const original = preset.isBuiltIn ? originalFileUrl(preset.name) : undefined +function TreeFolder({ node }: { node: FolderNode }) { + const isOpen = useAvaStore((s) => s.expandedFolders[node.path] ?? defaultOpen(node.path)) + const selected = useAvaStore((s) => s.selectedFolder === node.path) return ( -
-
-
-

{preset.name}

-

by {preset.author || 'unknown'}

-
-
- - {modeOfScript(preset.content)} - +
+
+ + +
+ {isOpen && ( +
+ {node.children.map((child) => ( + + ))} + {node.presets.map((p) => ( + + ))} + {node.children.length === 0 && node.presets.length === 0 && ( +

+ (empty) +

+ )}
-
- -

{preset.description}

- -
- - {original && ( - - )} - {!preset.isBuiltIn && ( - - )} -
+ )}
) } -function SavePresetSection() { - const { toast } = useToast() - const scriptText = useAvaStore((s) => s.scriptText) - const [name, setName] = useState('') - const [saving, setSaving] = useState(false) +function TreePreset({ preset, depth }: { preset: PresetItem; depth: number }) { + const loadPreset = useLoadPreset() + const selected = useAvaStore((s) => s.selectedPresetId === preset.id) + const running = useAvaStore((s) => s.activePresetId === preset.id) - const save = async () => { - const trimmed = name.trim() + return ( +
+ + {preset.originalFile && ( + + + + )} +
+ ) +} + +// --------------------------------------------------------------------------- +// small rename/create dialog shared by the three name-requiring actions +// --------------------------------------------------------------------------- + +/** + * Fully controlled name prompt shared by the three name-requiring actions + * (create preset / new subfolder / rename). The parent owns the input value + * and error so each open starts clean without mount tricks. + */ +function NameDialog({ + open, + onOpenChange, + title, + description, + placeholder, + value, + onChange, + error, + onError, + submitLabel, + busy, + onSubmit, +}: { + open: boolean + onOpenChange: (open: boolean) => void + title: string + description: string + placeholder: string + value: string + onChange: (value: string) => void + error: string | null + onError: (error: string | null) => void + submitLabel: string + busy: boolean + /** Return true to close the dialog (server errors keep it open). */ + onSubmit: (name: string) => Promise +}) { + const submit = async () => { + const trimmed = value.trim() if (!trimmed) { - toast({ title: 'Name required', description: 'Give the preset a name before saving.', variant: 'destructive' }) + onError('Enter a name first.') return } - const v = validateScript(scriptText) - setSaving(true) + const ok = await onSubmit(trimmed) + if (ok) onOpenChange(false) + } + + return ( + + + + {title} + {description} + +
+ onChange(e.target.value)} + placeholder={placeholder} + autoFocus + aria-label={title} + className="h-9 border-zinc-700 bg-zinc-900 font-mono text-xs" + onFocus={(e) => e.currentTarget.select()} + onKeyDown={(e) => { + if (e.key === 'Enter') void submit() + }} + /> + {error &&

{error}

} +
+ + + + +
+
+ ) +} + +// --------------------------------------------------------------------------- +// the tab +// --------------------------------------------------------------------------- + +type DialogKind = 'createPreset' | 'newFolder' | 'rename' | null + +interface DeleteTarget { + kind: 'folder' | 'preset' + name: string + /** Folder path (folder delete) or preset id (preset delete). */ + path: string + presetCount: number + folderCount: number +} + +export function PresetsTab() { + const { toast } = useToast() + const loadPreset = useLoadPreset() + + const presets = useAvaStore((s) => s.presets) + const folders = useAvaStore((s) => s.folders) + const selectedFolder = useAvaStore((s) => s.selectedFolder) + const selectedPresetId = useAvaStore((s) => s.selectedPresetId) + const randomize = useAvaStore((s) => s.randomize) + const randomIntervalSec = useAvaStore((s) => s.randomIntervalSec) + const randomScope = useAvaStore((s) => s.randomScope) + + const [dialog, setDialog] = useState(null) + const [nameValue, setNameValue] = useState('') + const [nameError, setNameError] = useState(null) + const [busy, setBusy] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + const [delayText, setDelayText] = useState(String(randomIntervalSec)) + const [delayError, setDelayError] = useState(false) + const importInputRef = useRef(null) + + // freshness refresh whenever the tab mounts (initial seed happens on app load) + useEffect(() => { + void refreshPresets() + }, []) + + // keep the delay input in sync when the store value changes elsewhere + useEffect(() => { + setDelayText(String(randomIntervalSec)) + }, [randomIntervalSec]) + + const tree = useMemo(() => buildTree(folders, presets), [folders, presets]) + const selectedPreset = useMemo( + () => presets.find((p) => p.id === selectedPresetId) ?? null, + [presets, selectedPresetId], + ) + const folderProtected = PROTECTED_FOLDERS.has(selectedFolder) + const folderName = selectedFolder.split('/').pop() ?? selectedFolder + const folderPrefix = `${selectedFolder}/` + const folderPresetCount = presets.filter( + (p) => p.folderPath === selectedFolder || p.folderPath.startsWith(folderPrefix), + ).length + const childFolderCount = folders.filter( + (f) => f.startsWith(folderPrefix) && !f.slice(folderPrefix.length).includes('/'), + ).length + const scopeCount = presets.filter( + (p) => randomScope === 'root' || p.folderPath === randomScope || p.folderPath.startsWith(`${randomScope}/`), + ).length + const scopeLabel = randomScope === 'root' ? 'all presets' : (randomScope.split('/').pop() ?? randomScope) + + // ----------------------------------------------------------------- + // actions + // ----------------------------------------------------------------- + + const submitCreatePreset = async (name: string): Promise => { + setBusy(true) try { const res = await fetch('/api/presets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: trimmed, - content: scriptText, - author: v.meta?.author || 'you', - description: v.meta?.description || '', - }), + body: JSON.stringify({ name, content: STARTER_TEMPLATE, folderPath: selectedFolder }), }) - const data = (await res.json().catch(() => null)) as { error?: string; updated?: boolean } | null + const data = (await res.json().catch(() => null)) as { preset?: PresetItem; error?: string } | null + if (!res.ok || !data?.preset) { + toast({ + title: 'Preset not created', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return false + } + await refreshPresets() + const st = useAvaStore.getState() + st.setEditTarget({ id: data.preset.id, name: data.preset.name, folderPath: data.preset.folderPath }) + st.setScriptText(STARTER_TEMPLATE) + applyScriptText(STARTER_TEMPLATE) + st.setActivePresetId(data.preset.id) + st.selectPreset(data.preset.id) + st.setTab('script') + toast({ + title: 'Preset created', + description: `"${data.preset.name}" created — now edit and Save.`, + }) + return true + } catch { + toast({ + title: 'Preset not created', + description: 'Network error while creating the preset.', + variant: 'destructive', + }) + return false + } finally { + setBusy(false) + } + } + + const submitNewFolder = async (name: string): Promise => { + const path = `${selectedFolder}/${name}` + setBusy(true) + try { + const res = await fetch('/api/folders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }) + const data = (await res.json().catch(() => null)) as { folder?: { path: string }; error?: string } | null + if (!res.ok || !data?.folder) { + toast({ + title: 'Folder not created', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return false + } + await refreshPresets() + const st = useAvaStore.getState() + st.toggleFolder(selectedFolder, true) + st.selectFolder(data.folder.path) + toast({ title: 'Folder created', description: `"${name}" added under ${selectedFolder}.` }) + return true + } catch { + toast({ + title: 'Folder not created', + description: 'Network error while creating the folder.', + variant: 'destructive', + }) + return false + } finally { + setBusy(false) + } + } + + const submitRename = async (name: string): Promise => { + setBusy(true) + try { + if (selectedPreset) { + const res = await fetch(`/api/presets/${selectedPreset.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }) + const data = (await res.json().catch(() => null)) as { preset?: PresetItem; error?: string } | null + if (!res.ok || !data?.preset) { + toast({ + title: 'Rename failed', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return false + } + await refreshPresets() + const st = useAvaStore.getState() + if (st.editTarget?.id === selectedPreset.id) { + st.setEditTarget({ + id: data.preset.id, + name: data.preset.name, + folderPath: data.preset.folderPath, + }) + } + toast({ + title: 'Preset renamed', + description: `"${selectedPreset.name}" is now "${data.preset.name}".`, + }) + return true + } + // folder rename — everything below moves with it + const res = await fetch('/api/folders', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: selectedFolder, newName: name }), + }) + const data = (await res.json().catch(() => null)) as { to?: string; error?: string } | null + if (!res.ok || !data?.to) { + toast({ + title: 'Rename failed', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return false + } + await refreshPresets() + const st = useAvaStore.getState() + st.selectFolder(data.to) + if ( + st.editTarget && + (st.editTarget.folderPath === selectedFolder || st.editTarget.folderPath.startsWith(folderPrefix)) + ) { + st.setEditTarget({ + ...st.editTarget, + folderPath: data.to + st.editTarget.folderPath.slice(selectedFolder.length), + }) + } + toast({ title: 'Folder renamed', description: `Now at ${data.to}.` }) + return true + } catch { + toast({ + title: 'Rename failed', + description: 'Network error while renaming.', + variant: 'destructive', + }) + return false + } finally { + setBusy(false) + } + } + + const openDialog = (kind: Exclude, initialName: string) => { + setDialog(kind) + setNameValue(initialName) + setNameError(null) + } + + const openPresetDelete = () => { + if (!selectedPreset) return + setDeleteTarget({ + kind: 'preset', + name: selectedPreset.name, + path: selectedPreset.id, + presetCount: 1, + folderCount: 0, + }) + } + + const openFolderDelete = () => { + const presetCount = presets.filter( + (p) => p.folderPath === selectedFolder || p.folderPath.startsWith(folderPrefix), + ).length + const folderCount = folders.filter((f) => f.startsWith(folderPrefix)).length + setDeleteTarget({ kind: 'folder', name: folderName, path: selectedFolder, presetCount, folderCount }) + } + + const confirmDelete = async () => { + if (!deleteTarget || busy) return + setBusy(true) + try { + const isFolder = deleteTarget.kind === 'folder' + const res = await fetch( + isFolder ? `/api/folders?path=${encodeURIComponent(deleteTarget.path)}` : `/api/presets/${deleteTarget.path}`, + { method: 'DELETE' }, + ) + const data = (await res.json().catch(() => null)) as { deleted?: boolean; error?: string } | null if (!res.ok) { toast({ - title: 'Save failed', + title: 'Delete failed', description: data?.error ?? `Server responded ${res.status}.`, variant: 'destructive', }) @@ -148,98 +646,374 @@ function SavePresetSection() { } await refreshPresets() toast({ - title: data?.updated ? 'Preset updated' : 'Preset saved', - description: `"${trimmed}" is available in the list above.`, + title: isFolder ? 'Folder deleted' : 'Preset deleted', + description: isFolder + ? `"${deleteTarget.name}" and everything inside it removed.` + : `"${deleteTarget.name}" removed.`, }) - setName('') } catch { - toast({ title: 'Save failed', description: 'Network error while saving the preset.', variant: 'destructive' }) + toast({ + title: 'Delete failed', + description: 'Network error while deleting.', + variant: 'destructive', + }) } finally { - setSaving(false) + setBusy(false) + setDeleteTarget(null) } } - return ( -
-

Save current editor script as preset

-

- Saved under the script's editor content — reuse it any time from this list. -

-
- setName(e.target.value)} - placeholder="Preset name" - className="h-9 border-zinc-700 bg-zinc-900 font-mono text-xs" - onKeyDown={(e) => { - if (e.key === 'Enter') void save() - }} - /> - -
-
- ) -} + const importZip = async (file: File) => { + setBusy(true) + try { + const form = new FormData() + form.append('file', file) + form.append('targetPath', selectedFolder) + const res = await fetch('/api/presets/import', { method: 'POST', body: form }) + const data = (await res.json().catch(() => null)) as + | { imported?: number; skipped?: number; foldersCreated?: number; error?: string } + | null + if (!res.ok || !data) { + toast({ + title: 'Import failed', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return + } + await refreshPresets() + useAvaStore.getState().toggleFolder(selectedFolder, true) + let summary = `Imported ${data.imported ?? 0} presets` + if ((data.skipped ?? 0) > 0) summary += `, ${data.skipped} duplicates skipped` + if ((data.foldersCreated ?? 0) > 0) summary += ` — ${data.foldersCreated} folders created` + toast({ title: 'Import complete', description: `${summary}.` }) + } catch { + toast({ + title: 'Import failed', + description: 'Network error while importing.', + variant: 'destructive', + }) + } finally { + setBusy(false) + } + } -export function PresetsTab() { - const presets = useAvaStore((s) => s.presets) - const setScriptText = useAvaStore((s) => s.setScriptText) - const setTab = useAvaStore((s) => s.setTab) + // ----------------------------------------------------------------- + // randomizer controls (timer runs in the store) + // ----------------------------------------------------------------- - // freshness refresh whenever the tab mounts (initial seed happens on app load) - useEffect(() => { - void refreshPresets() - }, []) + const onDelayChange = (raw: string) => { + setDelayText(raw) + const result = useAvaStore.getState().setRandomIntervalSec(Number(raw)) + setDelayError(!result.ok) + if (result.ok && result.value !== undefined) setDelayText(String(result.value)) + } - const builtIns = presets.filter((p) => p.isBuiltIn) - const customs = presets.filter((p) => !p.isBuiltIn) + const onDelayBlur = () => { + // invalid / partial input never persists — revert to the committed value + if (delayError) setDelayText(String(randomIntervalSec)) + } + + // ----------------------------------------------------------------- + // render + // ----------------------------------------------------------------- return ( -
+

- {presets.length} preset{presets.length === 1 ? '' : 's'} — classics adapted from the original Winamp AVS - files. + {presets.length} presets ·{' '} + {folders.length} folders

- +

double-click a preset to run it

- {/* long-list handling: capped height + styled scrollbar */} -
- {builtIns.map((p) => ( - - ))} - {customs.length > 0 && ( -
Custom
- )} - {customs.map((p) => ( - - ))} - {presets.length === 0 && ( -
- Loading presets… +
+ {/* LEFT — folder/file tree */} +
+
+ {tree.map((node) => ( + + ))} + {presets.length === 0 && ( +

Loading presets…

+ )}
- )} +
+ + {/* RIGHT — management panel */} +
+
+ {selectedPreset ? ( + + ) : ( + + )} +
+

+ {selectedPreset ? selectedPreset.name : folderName} +

+

+ {selectedPreset ? selectedPreset.folderPath : selectedFolder} +

+ {!selectedPreset && ( +

+ {plural(folderPresetCount, 'preset')} · {plural(childFolderCount, 'subfolder')} +

+ )} +
+
+ +
+ {selectedPreset ? ( + <> + +
+ + +
+ + ) : ( + <> + + +
+ + +
+
+ + +
+ + )} +
+ + {/* randomize (club mode) — timer runs in the store */} +
+
+ useAvaStore.getState().setRandomize(v === true)} + aria-label="Randomize presets" + className="size-4 data-[state=checked]:border-emerald-400 data-[state=checked]:bg-emerald-400 data-[state=checked]:text-zinc-950" + /> + +
+ onDelayChange(e.target.value)} + onBlur={onDelayBlur} + inputMode="numeric" + aria-label="Random switch delay in seconds" + className="h-8 w-16 border-zinc-700 bg-zinc-900 px-2 text-center font-mono text-xs tabular-nums" + /> + sec +
+
+ {delayError && ( +

Delay must be between 3 and 60 seconds.

+ )} +

+ Random from: {scopeLabel} + {scopeCount === 0 && — no presets in this folder yet} +

+ {randomize && ( +
+ + + ON · every {randomIntervalSec}s + +
+ )} +
+
- + {/* name dialogs */} + setDialog(o ? 'createPreset' : null)} + title="Create preset" + description={`New preset from the starter template inside ${selectedFolder}.`} + placeholder="Preset name" + value={nameValue} + onChange={setNameValue} + error={nameError} + onError={setNameError} + submitLabel="Create" + busy={busy} + onSubmit={submitCreatePreset} + /> + setDialog(o ? 'newFolder' : null)} + title="New subfolder" + description={`Created inside ${selectedFolder}.`} + placeholder="Folder name" + value={nameValue} + onChange={setNameValue} + error={nameError} + onError={setNameError} + submitLabel="Create" + busy={busy} + onSubmit={submitNewFolder} + /> + setDialog(o ? 'rename' : null)} + title={selectedPreset ? 'Rename preset' : 'Rename folder'} + description={ + selectedPreset + ? `Currently "${selectedPreset.name}" in ${selectedPreset.folderPath}.` + : `Currently "${folderName}" — everything inside moves with it.` + } + placeholder="New name" + value={nameValue} + onChange={setNameValue} + error={nameError} + onError={setNameError} + submitLabel="Rename" + busy={busy} + onSubmit={submitRename} + /> + + {/* delete confirmation */} + !o && setDeleteTarget(null)}> + + + + {deleteTarget?.kind === 'folder' + ? `Delete "${deleteTarget.name}"?` + : `Delete preset "${deleteTarget?.name ?? ''}"?`} + + + {deleteTarget?.kind === 'folder' + ? deleteTarget.presetCount > 0 || deleteTarget.folderCount > 0 + ? `Delete "${deleteTarget.name}" and everything inside it? This removes ${plural(deleteTarget.presetCount, 'preset')}${deleteTarget.folderCount > 0 ? ` in ${plural(deleteTarget.folderCount, 'folder')}` : ''}. This cannot be undone.` + : `Delete "${deleteTarget.name}"? The folder is empty. This cannot be undone.` + : `Delete preset "${deleteTarget?.name ?? ''}"? This cannot be undone.`} + + + + Cancel + { + e.preventDefault() + void confirmDelete() + }} + className="h-9 bg-rose-500 text-zinc-50 hover:bg-rose-400" + > + {busy ? 'Deleting…' : 'Delete'} + + + + + + {/* hidden zip import picker */} + { + const file = e.target.files?.[0] + if (file) void importZip(file) + e.target.value = '' + }} + />
) } diff --git a/src/components/ava/script-tab.tsx b/src/components/ava/script-tab.tsx index 8a8411a..f559a97 100755 --- a/src/components/ava/script-tab.tsx +++ b/src/components/ava/script-tab.tsx @@ -1,28 +1,48 @@ // AVA — Script tab: the custom visualization script editor. // -// Toolbar: Apply, Save as Default, Save to File, Load from File, New, Help. -// Editor: line-number gutter + monospace textarea (synced scroll, Tab inserts -// two spaces), error lines flagged in the gutter. Status line under the editor -// shows compile status or runtime errors. +// Toolbar: Apply, Save (to the preset picked in the Presets tab, when a save +// target is set), Save As…, Save as Default, Save to File, Load from File, +// New, Help. Editor: line-number gutter + monospace textarea (synced scroll, +// Tab inserts two spaces), error lines flagged in the gutter. Status line +// under the editor shows compile status or runtime errors. 'use client' import { useEffect, useMemo, useRef, useState } from 'react' -import { Check, CircleHelp, Download, FilePlus2, FolderOpen, Play, Save, X } from 'lucide-react' +import { Check, CircleHelp, Copy, Download, FilePlus2, FolderOpen, Play, Save, X } from 'lucide-react' import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' import { STARTER_TEMPLATE } from '@/lib/ava/presets' import { validateScript } from '@/lib/ava/script' -import { useAvaStore, persistDefaultScript } from '@/lib/store' +import { refreshPresets, useAvaStore, persistDefaultScript, type PresetItem } from '@/lib/store' import { applyScriptText } from '@/components/ava/audio-source' import { HelpPanel } from '@/components/ava/help-panel' import { sanitizeFileName } from '@/components/ava/shared' import { useToast } from '@/hooks/use-toast' import { cn } from '@/lib/utils' +/** Middle-truncated save-target path for the chip: "root/embedded/…/Name". */ +function formatSavePath(folderPath: string, name: string): string { + const full = `${folderPath}/${name}` + if (full.length <= 36) return full + const segs = folderPath.split('/') + return `${segs.slice(0, 2).join('/')}/…/${name}` +} + export function ScriptTab() { const { toast } = useToast() const scriptText = useAvaStore((s) => s.scriptText) const setScriptText = useAvaStore((s) => s.setScriptText) + const editTarget = useAvaStore((s) => s.editTarget) + const setEditTarget = useAvaStore((s) => s.setEditTarget) const runtimeError = useAvaStore((s) => s.stats?.error ?? null) const textareaRef = useRef(null) @@ -31,6 +51,11 @@ export function ScriptTab() { const [validation, setValidation] = useState | null>(null) const [helpOpen, setHelpOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [saveAsOpen, setSaveAsOpen] = useState(false) + const [saveAsName, setSaveAsName] = useState('') + const [saveAsBusy, setSaveAsBusy] = useState(false) + const [saveAsError, setSaveAsError] = useState(null) // debounced live validation (compile is cheap, scripts are small) useEffect(() => { @@ -78,6 +103,88 @@ export function ScriptTab() { }) } + /** Overwrite the target preset (picked in the Presets tab) with the editor script. */ + const saveToTarget = async () => { + if (!editTarget || saving) return + setSaving(true) + try { + const v = validateScript(scriptText) + const body: Record = { content: scriptText } + if (v.meta?.author) body.author = v.meta.author + const res = await fetch(`/api/presets/${editTarget.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + const data = (await res.json().catch(() => null)) as { preset?: PresetItem; error?: string } | null + if (!res.ok) { + toast({ + title: 'Save failed', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return + } + await refreshPresets() + toast({ title: `Saved to "${editTarget.name}"`, description: 'The preset now holds the current editor script.' }) + } catch { + toast({ title: 'Save failed', description: 'Network error while saving the preset.', variant: 'destructive' }) + } finally { + setSaving(false) + } + } + + const openSaveAs = () => { + if (!editTarget) return + setSaveAsName(editTarget.name) + setSaveAsError(null) + setSaveAsOpen(true) + } + + /** Save a copy of the editor script under a new name in the same folder. */ + const saveAsCopy = async () => { + if (!editTarget) return + const trimmed = saveAsName.trim() + if (!trimmed) { + setSaveAsError('Enter a name first.') + return + } + setSaveAsBusy(true) + try { + const v = validateScript(scriptText) + const res = await fetch('/api/presets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: trimmed, + content: scriptText, + folderPath: editTarget.folderPath, + author: v.meta?.author || 'you', + }), + }) + const data = (await res.json().catch(() => null)) as { preset?: PresetItem; error?: string } | null + if (!res.ok || !data?.preset) { + toast({ + title: 'Save failed', + description: data?.error ?? `Server responded ${res.status}.`, + variant: 'destructive', + }) + return + } + await refreshPresets() + setEditTarget({ id: data.preset.id, name: data.preset.name, folderPath: data.preset.folderPath }) + setSaveAsOpen(false) + toast({ + title: 'Copy saved', + description: `"${data.preset.name}" created in ${data.preset.folderPath}.`, + }) + } catch { + toast({ title: 'Save failed', description: 'Network error while saving the copy.', variant: 'destructive' }) + } finally { + setSaveAsBusy(false) + } + } + const saveToFile = () => { const meta = validateScript(scriptText).meta const base = sanitizeFileName(meta?.name && meta.name !== 'Untitled' ? meta.name : 'untitled') @@ -148,6 +255,25 @@ export function ScriptTab() { return (
+ {/* save-target chip — which preset "Save" writes into */} + {editTarget && ( +
+ + + Saving to: {formatSavePath(editTarget.folderPath, editTarget.name)} + + +
+ )} + {/* toolbar */}
+ {editTarget && ( + + )} + {editTarget && ( + + )} @@ -252,6 +403,54 @@ export function ScriptTab() { {/* help panel */} {helpOpen && } + {/* save-a-copy dialog */} + { + setSaveAsOpen(o) + if (o) setSaveAsError(null) + }} + > + + + Save a copy + + Saves the current editor script as a new preset in {editTarget?.folderPath ?? ''}. + + +
+ { + setSaveAsName(e.target.value) + setSaveAsError(null) + }} + placeholder="Preset name" + autoFocus + aria-label="New preset name" + className="h-9 border-zinc-700 bg-zinc-900 font-mono text-xs" + onFocus={(e) => e.currentTarget.select()} + onKeyDown={(e) => { + if (e.key === 'Enter') void saveAsCopy() + }} + /> + {saveAsError &&

{saveAsError}

} +
+ + + + +
+
+ = {} + /** +1 native (y up) / -1 AVS-ported presets (y down). Applied when reading points. */ + private ysign = 1 + + constructor() { + // AVS-eel compatibility hooks — the expression engine reads audio/time + // through these (getspec/getosc/gettime). Headless tests can override. + AVA_HOOKS.spectrumAt = (pos) => this.audio.getSpectrum(pos) + AVA_HOOKS.waveAt = (pos) => this.audio.getWave(pos) + AVA_HOOKS.now = () => this.env.time ?? 0 + } private points = new Float32Array(MAX_POINTS * POINT_STRIDE) private lastNow = 0 @@ -85,7 +96,9 @@ export class AvaEngine { setScript(script: AvaScript): void { this.script = script this.scriptError = null + this.ysign = script.meta.ysign === -1 ? -1 : 1 this.env = { n: 400, t: 0, time: 0, frame: 0, beats: 0 } + resetEelBuffers() try { script.initProg?.exec(this.env) } catch (err) { @@ -200,7 +213,7 @@ export class AvaEngine { this.script.pointProg.exec(env) const o = k * POINT_STRIDE pts[o] = env.x - pts[o + 1] = env.y + pts[o + 1] = env.y * this.ysign pts[o + 2] = env.red pts[o + 3] = env.green pts[o + 4] = env.blue @@ -227,11 +240,9 @@ export class AvaEngine { ctx.lineCap = 'round' ctx.lineJoin = 'round' for (let pass = 0; pass < 2; pass++) { - const lw = (pass === 0 ? 3.2 : 1) * meta.linewidth * scale const gain = pass === 0 ? 0.22 : 0.9 - ctx.lineWidth = lw let pen = false - let pr = 0, pg = 0, pb = 0, pa = 0, pxp = 0, pyp = 0 + let pr = 0, pg = 0, pb = 0, pa = 0, pxp = 0, pyp = 0, plw = 0 for (let k = 0; k < n; k++) { const o = k * POINT_STRIDE const skip = pts[o + 5] @@ -241,19 +252,22 @@ export class AvaEngine { const g = Math.max(0, Math.min(1, pts[o + 3])) const b = Math.max(0, Math.min(1, pts[o + 4])) const a = Math.max(0, Math.min(1, pts[o + 7])) + // per-point line width (AVS linesize) — falls back to meta.linewidth + const lw = Math.max(0.3, pts[o + 6] > 0 ? pts[o + 6] : meta.linewidth) * (pass === 0 ? 3.2 : 1) * scale if (skip) { pen = false - pr = r; pg = g; pb = b; pa = a; pxp = x; pyp = y + pr = r; pg = g; pb = b; pa = a; pxp = x; pyp = y; plw = lw continue } if (pen) { ctx.strokeStyle = `rgba(${(((pr + r) / 2) * 255) | 0},${(((pg + g) / 2) * 255) | 0},${(((pb + b) / 2) * 255) | 0},${(((pa + a) / 2) * gain).toFixed(3)})` + ctx.lineWidth = (plw + lw) / 2 ctx.beginPath() ctx.moveTo(pxp, pyp) ctx.lineTo(x, y) ctx.stroke() } - pr = r; pg = g; pb = b; pa = a; pxp = x; pyp = y + pr = r; pg = g; pb = b; pa = a; pxp = x; pyp = y; plw = lw pen = true } } diff --git a/src/lib/ava/expression.ts b/src/lib/ava/expression.ts index cc7ddc1..4cf50c0 100755 --- a/src/lib/ava/expression.ts +++ b/src/lib/ava/expression.ts @@ -7,6 +7,10 @@ // - functions: sin cos tan asin acos atan atan2 abs sqrt sqr pow exp log log10 // floor ceil round int frac sign min max rand lerp clamp // above below equal band bor bnot inrange sigmoid if +// invsqrt rnd (AVS rand) +// AVS-eel compat: getspec getosc gettime getkbmouse +// loop(count, body) megabuf(i)/gmegabuf(i) (+ array writes) +// single '&' '|' bitwise operators, x/0 === 0 // - constants: pi, tau, e // // `if(cond, a, b)` and the ternary only evaluate the taken branch (lazy), @@ -16,6 +20,38 @@ export type Env = Record export type Node = (env: Env) => number +// --------------------------------------------------------------------------- +// AVS-eel compatibility hooks +// --------------------------------------------------------------------------- +// The Winamp AVS expression language (eel) exposes audio data through +// getspec()/getosc()/gettime(). The render engine injects its implementations +// here once (headless smoke tests can substitute their own). + +export const AVA_HOOKS = { + /** Spectrum magnitude at position 0..1 → 0..1 (AVS getspec). */ + spectrumAt: (pos: number): number => 0, + /** Waveform sample at position 0..1 → -1..1 (AVS getosc). */ + waveAt: (pos: number): number => 0, + /** Seconds since the visualization started (AVS gettime). */ + now: (): number => 0, +} + +// eel per-preset / global array buffers (megabuf has 1M slots in AVS) +const EEL_BUF_SIZE = 1 << 20 +const MEGABUF = new Float64Array(EEL_BUF_SIZE) +const GMEGABUF = new Float64Array(EEL_BUF_SIZE) + +/** Clear megabuf/gmegabuf — called when a new script loads (AVS semantics). */ +export function resetEelBuffers(): void { + MEGABUF.fill(0) + GMEGABUF.fill(0) +} + +function eelRead(buf: Float64Array, idx: number): number { + const k = idx | 0 + return k >= 0 && k < EEL_BUF_SIZE ? buf[k] : 0 +} + export class ExpressionError extends Error { line: number constructor(message: string, line: number) { @@ -32,7 +68,7 @@ interface Token { } const TWO_CHAR_OPS = new Set(['<=', '>=', '==', '!=', '&&', '||']) -const ONE_CHAR_OPS = new Set(['+', '-', '*', '/', '%', '^', '<', '>', '=', '!', '?', ':', '(', ')', ',', ';']) +const ONE_CHAR_OPS = new Set(['+', '-', '*', '/', '%', '^', '<', '>', '=', '!', '?', ':', '(', ')', ',', ';', '&', '|']) const CMP_OPS = new Set(['==', '!=', '<', '<=', '>', '>=']) export const CONSTANTS: Record = { @@ -63,7 +99,19 @@ function fn3(f: (x: number, y: number, z: number) => number): FuncDef { return { minArgs: 3, maxArgs: 3, build: (a) => (env) => f(a[0](env), a[1](env), a[2](env)) } } +const clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x) + const FUNCTIONS: Record = { + // --- AVS-eel audio/time compatibility --------------------------------- + getspec: fn3((a, _w, _ch) => AVA_HOOKS.spectrumAt(clamp01(a))), + getosc: fn3((a, _w, _ch) => AVA_HOOKS.waveAt(clamp01(a))), + gettime: fn1(() => AVA_HOOKS.now()), + getkbmouse: fn1(() => 0), + invsqrt: fn1((x) => 1 / Math.sqrt(x)), + // AVS rand(max): random INTEGER in 0..max-1 (non-deterministic). + rnd: fn1((n) => Math.floor(Math.random() * Math.max(0, Math.floor(n)))), + megabuf: fn1((i) => eelRead(MEGABUF, i)), + gmegabuf: fn1((i) => eelRead(GMEGABUF, i)), sin: fn1(Math.sin), cos: fn1(Math.cos), tan: fn1(Math.tan), @@ -112,13 +160,18 @@ function tokenize(src: string, baseLine: number): Token[] { let i = 0 let line = baseLine let depth = 0 // paren depth — newlines inside parens are not separators + // Kind of each open paren ('loop' vs 'expr') — newlines directly inside a + // loop( body ARE statement separators (AVS-eel loop semantics). + const parenKinds: ('loop' | 'expr')[] = [] const n = src.length while (i < n) { const c = src[i] if (c === '\n') { line++ i++ - if (depth === 0) tokens.push({ type: 'op', value: ';', line }) + if (depth === 0 || parenKinds[parenKinds.length - 1] === 'loop') { + tokens.push({ type: 'op', value: ';', line }) + } continue } if (c === '\r' || c === ' ' || c === '\t') { @@ -156,8 +209,15 @@ function tokenize(src: string, baseLine: number): Token[] { i += 2 continue } - if (c === '(') depth++ - if (c === ')') depth = Math.max(0, depth - 1) + if (c === '(') { + const prev = tokens[tokens.length - 1] + parenKinds.push(prev && prev.type === 'ident' && prev.value === 'loop' ? 'loop' : 'expr') + depth++ + } + if (c === ')') { + parenKinds.pop() + depth = Math.max(0, depth - 1) + } if (ONE_CHAR_OPS.has(c)) { tokens.push({ type: 'op', value: c, line }) i++ @@ -209,7 +269,8 @@ class Parser { } /** Assignment expression — also valid inside function args and ternary - * branches (AVS-style: if(cond, x = 1, x = 2)). Right-associative. */ + * branches (AVS-style: if(cond, x = 1, x = 2)). Right-associative. + * Also handles AVS array writes: megabuf(i) = value / gmegabuf(i) = value. */ private parseAssignExpr(): Node { const t = this.peek() if (t && t.type === 'ident') { @@ -221,6 +282,28 @@ class Parser { const name = t.value return (env) => (env[name] = value(env)) } + if ((t.value === 'megabuf' || t.value === 'gmegabuf') && eq && eq.type === 'op' && eq.value === '(') { + const save = this.pos + this.next() // ident + this.next() // '(' + const idx = this.parseExpr() + if (this.atOp(')')) { + this.next() + if (this.atOp('=')) { + this.next() + const value = this.parseAssignExpr() + const buf = t.value === 'megabuf' ? MEGABUF : GMEGABUF + return (env) => { + const k = idx(env) | 0 + const v = value(env) + if (k >= 0 && k < EEL_BUF_SIZE) buf[k] = v + return v + } + } + } + // not an array write after all — rewind and parse as a plain expression + this.pos = save + } } return this.parseExpr() } @@ -242,23 +325,47 @@ class Parser { } private parseOr(): Node { - let l = this.parseAnd() + let l = this.parseBitOr() while (this.atOp('||')) { this.next() - const r = this.parseAnd() + const r = this.parseBitOr() const lc = l l = (env) => (lc(env) !== 0 ? 1 : r(env) !== 0 ? 1 : 0) } return l } + /** AVS-eel single '|' — bitwise or on truncated ints. */ + private parseBitOr(): Node { + let l = this.parseAnd() + while (this.atOp('|')) { + this.next() + const r = this.parseAnd() + const lc = l + l = (env) => ((lc(env) | 0) | (r(env) | 0)) + } + return l + } + private parseAnd(): Node { - let l = this.parseCmp() + let l = this.parseBitAnd() while (this.atOp('&&')) { + this.next() + const r = this.parseBitAnd() + const lc = l + l = (env) => (lc(env) !== 0 && r(env) !== 0 ? 1 : 0) + } + return l + } + + /** AVS-eel single '&' — bitwise and on truncated ints. */ + private parseBitAnd(): Node { + let l = this.parseCmp() + while (this.atOp('&')) { this.next() const r = this.parseCmp() const lc = l - l = (env) => (lc(env) !== 0 && r(env) !== 0 ? 1 : 0) + l = (env) => ((lc(env) | 0) & (r(env) | 0)) } return l } @@ -304,7 +411,19 @@ class Parser { this.next() const r = this.parseUnary() const lc = l - l = t.value === '*' ? (e) => lc(e) * r(e) : t.value === '/' ? (e) => lc(e) / r(e) : (e) => lc(e) % r(e) + // AVS-eel semantics: division / modulo by zero yields 0 (no Infinity) + l = + t.value === '*' + ? (e) => lc(e) * r(e) + : t.value === '/' + ? (e) => { + const d = r(e) + return d === 0 ? 0 : lc(e) / d + } + : (e) => { + const d = r(e) + return d === 0 ? 0 : lc(e) % d + } } return l } @@ -341,6 +460,38 @@ class Parser { private parsePrimary(): Node { const t = this.next() if (!t) throw new ExpressionError('Unexpected end of expression', this.lastLine()) + // AVS-eel loop(count, body) — repeats the body statement list. The count + // is evaluated once; the body may contain assignments and nested loops. + if (t.type === 'ident' && t.value === 'loop' && this.atOp('(')) { + this.next() // '(' + const count = this.parseAssignExpr() + if (!this.atOp(',')) { + throw new ExpressionError('loop(count, body) — expected a body after the count', t.line) + } + this.next() + const body: Node[] = [] + for (;;) { + while (this.atOp(';')) this.next() + if (this.atOp(')')) break + if (!this.peek()) throw new ExpressionError('Unterminated loop body', t.line) + body.push(this.parseAssignExpr()) + const after = this.peek() + if (after && !(after.type === 'op' && (after.value === ';' || after.value === ')'))) { + throw new ExpressionError(`Unexpected '${after.value}' in loop body — separate statements with ';'`, after.line) + } + } + this.expectOp(')') + return (env) => { + let c = count(env) + if (!Number.isFinite(c) || c < 0) c = 0 + if (c > 100000) c = 100000 + let last = 0 + for (let k = 0; k < c; k++) { + for (let s = 0; s < body.length; s++) last = body[s](env) + } + return last + } + } if (t.type === 'num') { const v = parseFloat(t.value) if (Number.isNaN(v)) throw new ExpressionError(`Bad number '${t.value}'`, t.line) @@ -379,9 +530,27 @@ class Parser { } } if (t.type === 'op' && t.value === '(') { - const inner = this.parseExpr() - this.expectOp(')') - return inner + // Parenthesized group. AVS-eel allows statement sequences as + // expressions: (a = 1; b = 2; a + b) evaluates to the last statement. + const stmts: Node[] = [] + for (;;) { + while (this.atOp(';')) this.next() + if (this.atOp(')') || this.atOp(',')) break + if (!this.peek()) throw new ExpressionError('Missing closing \')\'', t.line) + stmts.push(this.parseAssignExpr()) + const after = this.peek() + if (after && !(after.type === 'op' && (after.value === ';' || after.value === ',' || after.value === ')'))) { + throw new ExpressionError(`Unexpected '${after.value}' — separate statements with ';'`, after.line) + } + } + if (this.atOp(')')) this.next() + if (!stmts.length) throw new ExpressionError('Empty parentheses', t.line) + if (stmts.length === 1) return stmts[0] + return (env) => { + let last = 0 + for (let k = 0; k < stmts.length; k++) last = stmts[k](env) + return last + } } throw new ExpressionError(`Unexpected token '${t.value}'`, t.line) } diff --git a/src/lib/ava/script.ts b/src/lib/ava/script.ts index 97cc2cd..104c05c 100755 --- a/src/lib/ava/script.ts +++ b/src/lib/ava/script.ts @@ -26,6 +26,8 @@ export interface ScriptMeta { blend: BlendMode persist: number linewidth: number + /** +1 (default, y is up) or -1 (AVS convention, y is down — used by auto-ported presets). */ + ysign: number } export interface ScriptError { @@ -54,7 +56,7 @@ export interface AvaScript { } const SECTION_NAMES = new Set(['init', 'frame', 'beat', 'point']) -const HEADER_KEYS = new Set(['name', 'author', 'description', 'mode', 'blend', 'persist', 'linewidth']) +const HEADER_KEYS = new Set(['name', 'author', 'description', 'mode', 'blend', 'persist', 'linewidth', 'ysign']) const MODES = new Set(['lines', 'dots', 'solid']) const BLENDS = new Set(['additive', 'alpha']) @@ -66,6 +68,7 @@ const DEFAULT_META: ScriptMeta = { blend: 'additive', persist: 0.85, linewidth: 2, + ysign: 1, } /** Parse + compile a script. Throws AvaScriptError on the first problem found. */ @@ -169,6 +172,14 @@ function tryParse(text: string): { script: AvaScript | null; meta?: ScriptMeta; meta.linewidth = Math.min(Math.max(v, 0.5), 16) break } + case 'ysign': { + const v = parseInt(value, 10) + if (v !== 1 && v !== -1) { + return { script: null, meta: { ...meta }, error: { message: `ysign must be 1 or -1 (got '${value}')`, section: 'header', line: lineNo } } + } + meta.ysign = v + break + } } } flushSection() diff --git a/src/lib/server/preset-tree.ts b/src/lib/server/preset-tree.ts new file mode 100644 index 0000000..d4f0dbb --- /dev/null +++ b/src/lib/server/preset-tree.ts @@ -0,0 +1,89 @@ +// AVA — server-side preset folder-tree helpers. +// +// The preset library is a virtual file tree rooted at "root" with two +// structural areas: "root/embedded" (the shipped Winamp AVS ports) and +// "root/user created" (the user's own space). Every folder below those is +// fully manageable (create / rename / delete / export / import). + +import { db } from '@/lib/db' + +export const MAX_DEPTH = 8 +export const MAX_SEGMENT = 80 + +/** Folders that anchor the tree — they can never be renamed or deleted. */ +export const PROTECTED_FOLDERS = new Set(['root', 'root/embedded', 'root/user created']) + +export interface ParsedPath { + path: string + segments: string[] +} + +/** + * Validate + normalize a folder path ("root/a/b"). Returns null when the path + * is malformed (wrong root, too deep, bad characters, empty segments). + */ +export function normalizeFolderPath(raw: unknown): ParsedPath | null { + if (typeof raw !== 'string') return null + const trimmed = raw.trim().replace(/\/+$/, '') + if (!trimmed) return null + const parts = trimmed + .split('/') + .map((s) => s.trim()) + .filter((s) => s.length > 0) + if (parts.length === 0 || parts.length > MAX_DEPTH) return null + if (parts[0] !== 'root') return null + for (const seg of parts) { + if (seg.length > MAX_SEGMENT) return null + if (/[\u0000-\u001f\u007f]/.test(seg)) return null + if (seg === '.' || seg === '..') return null + } + return { path: parts.join('/'), segments: parts } +} + +/** Validate a preset / folder display name (no separators, sane length). */ +export function isValidName(raw: unknown, maxLen = 120): string | null { + if (typeof raw !== 'string') return null + const name = raw.trim() + if (!name || name.length > maxLen) return null + if (/[/\u0000-\u001f\u007f]/.test(name)) return null + return name +} + +/** Parent path of a segment list: ["root","a","b"] → "root/a". */ +export function parentOf(segments: string[]): string { + return segments.slice(0, -1).join('/') +} + +/** Prisma where-clause matching presets in a folder and every folder below it. */ +export function subtreeFilter(path: string) { + return { + OR: [{ folderPath: path }, { folderPath: { startsWith: `${path}/` } }], + } +} + +/** Same, but for Folder rows (their column is `path`). */ +export function subtreeFolderFilter(path: string) { + return { + OR: [{ path }, { path: { startsWith: `${path}/` } }], + } +} + +/** + * Ensure Folder rows exist for the given paths AND all of their ancestors. + * Returns how many rows were created. + */ +export async function ensureFolderRows(paths: string[]): Promise { + const wanted = new Set() + for (const p of paths) { + const segs = p.split('/') + for (let i = 1; i <= segs.length; i++) wanted.add(segs.slice(0, i).join('/')) + } + const existing = await db.folder.findMany({ + where: { path: { in: [...wanted] } }, + select: { path: true }, + }) + for (const e of existing) wanted.delete(e.path) + if (wanted.size === 0) return 0 + await db.folder.createMany({ data: [...wanted].map((path) => ({ path })) }) + return wanted.size +} diff --git a/src/lib/store.ts b/src/lib/store.ts index b2860f1..783bea0 100755 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -12,6 +12,8 @@ import { getEngine } from '@/lib/ava/engine' import type { TapResult } from '@/lib/ava/beat' import type { BandConfig, BeatConfig, BeatSource } from '@/lib/ava/types' import { DEFAULT_BAND_CONFIG, DEFAULT_BEAT_CONFIG } from '@/lib/ava/types' +import { validateScript } from '@/lib/ava/script' +import { toast } from '@/hooks/use-toast' export interface PresetItem { id: string @@ -20,9 +22,20 @@ export interface PresetItem { description: string content: string isBuiltIn: boolean + /** Full path from the virtual root, e.g. "root/embedded/Synth-C". */ + folderPath: string + /** Path of the original Winamp .avs binary in /public, when shipped. */ + originalFile: string | null updatedAt: string } +/** The preset file the Script tab "Save" button writes into. */ +export interface EditTarget { + id: string + name: string + folderPath: string +} + export type AudioSourceState = 'idle' | 'demo' | 'mic' | 'file' | 'link' /** EngineStats augmented with the beat counter (read alongside the callback). */ @@ -43,6 +56,7 @@ export interface BeatFlash { const LS_BAND = 'ava.bandConfig' const LS_BEAT = 'ava.beatConfig' const LS_SCRIPT = 'ava.defaultScript' +const LS_RANDOM = 'ava.randomize' function lsGetJSON(key: string): T | null { try { @@ -169,6 +183,22 @@ interface AvaState { bandConfig: BandConfig beatConfig: BeatConfig presets: PresetItem[] + /** All folder paths in the preset tree ("root", "root/embedded", …). */ + folders: string[] + /** Folder selected in the tree (drives the management buttons + randomize scope). */ + selectedFolder: string + /** Preset selected in the tree (drives Load / Rename / Delete preset). */ + selectedPresetId: string | null + /** Which tree folders are expanded (missing key → depth-based default). */ + expandedFolders: Record + /** The preset the Script tab "Save" writes into (null = untitled draft). */ + editTarget: EditTarget | null + /** Preset currently running on the engine (tree indicator). */ + activePresetId: string | null + /** Timed random preset switching (club mode). */ + randomize: boolean + randomIntervalSec: number + randomScope: string tab: string setStats: (s: AvaStats) => void @@ -188,9 +218,89 @@ interface AvaState { resetBandConfig: () => void resetBeatConfig: () => void setPresets: (p: PresetItem[]) => void + setFolders: (f: string[]) => void + toggleFolder: (path: string, open?: boolean) => void + selectFolder: (path: string) => void + selectPreset: (id: string | null) => void + setEditTarget: (t: EditTarget | null) => void + setActivePresetId: (id: string | null) => void + setRandomize: (on: boolean) => void + /** Validate + set the random delay (3–60 s). Returns ok:false when invalid. */ + setRandomIntervalSec: (sec: number) => { ok: boolean; value?: number; error?: string } setTab: (t: string) => void } +// --------------------------------------------------------------------------- +// random preset timer (club mode) — module-level, survives tab switches +// --------------------------------------------------------------------------- + +let randomTimer: ReturnType | null = null + +export function stopRandomTimer(): void { + if (randomTimer) { + clearInterval(randomTimer) + randomTimer = null + } +} + +/** + * Pick a random preset inside the current scope and run it on the engine. + * Deliberately does NOT touch the editor text — random switching must never + * destroy unsaved script edits. + */ +function loadRandomPreset(): void { + const s = useAvaStore.getState() + const scope = s.randomScope + const pool = s.presets.filter( + (p) => scope === 'root' || p.folderPath === scope || p.folderPath.startsWith(`${scope}/`), + ) + if (pool.length === 0) return + for (let attempt = 0; attempt < Math.min(4, pool.length); attempt++) { + const pick = pool[Math.floor(Math.random() * pool.length)] + const v = validateScript(pick.content) + if (v.ok && v.script) { + getEngine().setScript(v.script) + useAvaStore.getState().setActivePresetId(pick.id) + toast({ title: 'Random preset', description: `Now playing "${pick.name}".` }) + return + } + } +} + +/** (Re)start or stop the timer according to the current store state. */ +export function syncRandomTimer(): void { + stopRandomTimer() + const s = useAvaStore.getState() + if (!s.randomize) return + randomTimer = setInterval(loadRandomPreset, s.randomIntervalSec * 1000) +} + +function persistRandomSettings(): void { + const s = useAvaStore.getState() + lsSetJSON(LS_RANDOM, { + enabled: s.randomize, + intervalSec: s.randomIntervalSec, + scope: s.randomScope, + }) +} + +/** Restore the persisted randomize settings after presets first load. */ +export function restoreRandomSettings(folders: string[]): void { + const saved = lsGetJSON<{ enabled?: boolean; intervalSec?: number; scope?: string }>(LS_RANDOM) + if (!saved) return + const intervalOk = + typeof saved.intervalSec === 'number' && saved.intervalSec >= 3 && saved.intervalSec <= 60 + const scope = saved.scope === 'root' || folders.includes(saved.scope ?? '') ? saved.scope : 'root' + useAvaStore.setState({ + randomIntervalSec: intervalOk ? Math.round(saved.intervalSec ?? 10) : 10, + randomScope: scope ?? 'root', + }) + if (saved.enabled) { + useAvaStore.setState({ randomize: true }) + syncRandomTimer() + } +} + let flashSeq = 0 export const useAvaStore = create()((set, get) => ({ @@ -205,6 +315,15 @@ export const useAvaStore = create()((set, get) => ({ bandConfig: { ...DEFAULT_BAND_CONFIG }, beatConfig: { ...DEFAULT_BEAT_CONFIG }, presets: [], + folders: ['root', 'root/embedded', 'root/user created'], + selectedFolder: 'root', + selectedPresetId: null, + expandedFolders: {}, + editTarget: null, + activePresetId: null, + randomize: false, + randomIntervalSec: 10, + randomScope: 'root', tab: 'presets', setStats: (s) => set({ stats: s }), @@ -300,20 +419,78 @@ export const useAvaStore = create()((set, get) => ({ setPresets: (p) => set({ presets: p }), + setFolders: (f) => set({ folders: f }), + + toggleFolder: (path, open) => + set((s) => ({ expandedFolders: { ...s.expandedFolders, [path]: open ?? !(s.expandedFolders[path] ?? path.split('/').length <= 2) } })), + + selectFolder: (path) => { + set({ selectedFolder: path, selectedPresetId: null, randomScope: path }) + persistRandomSettings() + syncRandomTimer() + }, + + selectPreset: (id) => set({ selectedPresetId: id }), + + setEditTarget: (t) => set({ editTarget: t }), + + setActivePresetId: (id) => set({ activePresetId: id }), + + setRandomize: (on) => { + set({ randomize: on }) + persistRandomSettings() + syncRandomTimer() + }, + + setRandomIntervalSec: (sec) => { + const n = Math.round(Number(sec)) + if (!Number.isFinite(n) || n < 3 || n > 60) { + return { ok: false, error: 'Delay must be between 3 and 60 seconds.' } + } + set({ randomIntervalSec: n }) + persistRandomSettings() + syncRandomTimer() + return { ok: true, value: n } + }, + setTab: (t) => set({ tab: t }), })) // --------------------------------------------------------------------------- -// preset list fetching (shared by Presets tab + initial load) +// preset tree fetching (shared by Presets tab + initial load) // --------------------------------------------------------------------------- export async function refreshPresets(): Promise { try { const res = await fetch('/api/presets') if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = (await res.json()) as { presets?: PresetItem[] } + const data = (await res.json()) as { presets?: PresetItem[]; folders?: string[] } const presets = data.presets ?? [] - useAvaStore.getState().setPresets(presets) + const folders = + data.folders && data.folders.length ? data.folders : ['root', 'root/embedded', 'root/user created'] + const s = useAvaStore.getState() + s.setPresets(presets) + s.setFolders(folders) + + // prune selections that no longer exist (after delete / import / rename) + const patch: Partial = {} + if (!folders.includes(s.selectedFolder)) patch.selectedFolder = 'root' + if (s.selectedPresetId && !presets.some((p) => p.id === s.selectedPresetId)) { + patch.selectedPresetId = null + } + if (s.activePresetId && !presets.some((p) => p.id === s.activePresetId)) { + patch.activePresetId = null + } + if (s.editTarget && !presets.some((p) => p.id === s.editTarget?.id)) { + patch.editTarget = null + } + const scope = s.randomScope + if (scope !== 'root' && !folders.includes(scope)) { + patch.randomScope = s.selectedFolder + persistRandomSettings() + syncRandomTimer() + } + if (Object.keys(patch).length > 0) useAvaStore.setState(patch) return presets } catch { return [] diff --git a/tool-results/ava-restored-final.png b/tool-results/ava-restored-final.png new file mode 100644 index 0000000..be976dd Binary files /dev/null and b/tool-results/ava-restored-final.png differ diff --git a/tool-results/ava-restored-initial.png b/tool-results/ava-restored-initial.png new file mode 100644 index 0000000..a40b73d Binary files /dev/null and b/tool-results/ava-restored-initial.png differ diff --git a/tool-results/ava-restored-mobile.png b/tool-results/ava-restored-mobile.png new file mode 100644 index 0000000..f7975a8 Binary files /dev/null and b/tool-results/ava-restored-mobile.png differ diff --git a/tools/convert-avs.ts b/tools/convert-avs.ts new file mode 100644 index 0000000..c21572a --- /dev/null +++ b/tools/convert-avs.ts @@ -0,0 +1,1038 @@ +// AVA — .avs (Winamp AVS binary preset) → .ava auto-converter +// +// Parses the legacy .avs binary format (reverse-engineered against the +// vis_avs reference source), extracts the primary SuperScope / Texer II +// point program plus any AVSTrans #define macros and Jheriko Global +// register-setter code, translates it to the AVA script dialect, validates +// and smoke-runs every generated script through the real engine parser, and +// emits src/lib/ava/embedded-presets.ts. +// +// Run: bun tools/convert-avs.ts [inputRoot] (inputRoot defaults to /tmp/avsdrive/raw) +// +// Format reference (per grandchild/vis_avs source): +// magic "Nullsoft AVS Preset 0.2\x1a" (24 bytes) +// byte clearFlag +// effects: [int32 id] [if id>=0x4000: 32-byte apeId] [int32 len] [blob] +// id 36 SuperScope: [byte ver==1][point][frame][beat][init][int chsrc] +// [int ncolors][colors...][int drawmode] +// id -2 EffectList: [byte mode0|0x80][byte mode0][byte in][byte out] +// [byte extsize][ext ints while pos= 4) { + const v = r.int32() + if (r.pos > ext - 4) break + void v + } + const children: Effect[] = [] + while (r.remaining > 8) { + const id = r.int32() + let apeId: string | null = null + if (id >= 0x4000) apeId = latin1(r.bytes(32)).split('\0')[0] + const len = r.int32() + if (len < 0 || len > r.remaining) break + const childBlob = r.bytes(len) + // special case: the v2.81+ codeable EffectList's own init/frame code + // section, stored as a child entry with the Effect List APE id. + if (id >= 0x4000 && apeId === 'AVS 2.8+ Effect List Config') { + const cr = new Reader(childBlob) + if (cr.remaining >= 4) cr.int32() // use_code + const gv = { init: cr.lpString(), frame: cr.lpString(), beat: '' } + if (gv.init.trim() || gv.frame.trim()) { + children.push({ id, apeId, kind: 'globalvars', enabled: true, gv }) + } + continue + } + children.push(parseEffect(id, apeId, childBlob)) + } + return { enabled, children } +} + +function parseEffect(id: number, apeId: string | null, blob: Uint8Array): Effect { + const base = { id, apeId, enabled: true } as Effect + if (id === ID_SUPERSCOPE) { + const r = new Reader(blob) + const code = { init: '', frame: '', beat: '', point: '' } + let spectrumSource = false + let drawMode = 1 + const colors: number[] = [] + const ver = r.byte() + if (ver === 1) { + code.point = r.lpString() + code.frame = r.lpString() + code.beat = r.lpString() + code.init = r.lpString() + } else { + // ancient fixed 1024-byte block + if (blob.length >= 1025) { + const buf = latin1(blob.subarray(1, 1025)) + code.point = buf.slice(0, 256).split('\0')[0] + code.frame = buf.slice(256, 512).split('\0')[0] + code.beat = buf.slice(512, 768).split('\0')[0] + code.init = buf.slice(768, 1024).split('\0')[0] + } + } + if (r.remaining >= 4) { + const chsrc = r.int32() + spectrumSource = (chsrc & 0b100) !== 0 + if (r.remaining >= 4) { + const ncolors = r.int32() + if (ncolors <= 16) { + for (let i = 0; i < ncolors && r.remaining >= 4; i++) colors.push(r.int32() >>> 0) + } + if (r.remaining >= 4) drawMode = r.int32() + } + } + return { ...base, kind: 'superscope', code, spectrumSource, drawMode, colors } + } + if (id >= 0x4000 && apeId === APE_TEXER2) { + const r = new Reader(blob) + r.int32() // version + r.bytes(260) // image path + r.int32() // resize + r.int32() // wrap + r.int32() // colorize + r.int32() // unused + const code = { init: r.lpString(), frame: r.lpString(), beat: r.lpString(), point: r.lpString() } + return { ...base, kind: 'texer2', code, spectrumSource: false, drawMode: 0, colors: [] } + } + if (id === ID_EFFECTLIST || (id >= 0x4000 && apeId === 'AVS 2.8+ Effect List Config')) { + const { enabled, children } = parseEffectListBody(blob) + return { ...base, kind: 'effectlist', enabled, children } + } + if (id >= 0x4000 && apeId === APE_EELTRANS) { + const r = new Reader(blob) + if (r.remaining >= 16) { + r.int32(); r.int32(); r.int32(); r.int32() + return { ...base, kind: 'eeltrans', transCode: r.ntRest() } + } + return { ...base, kind: 'other' } + } + if (id >= 0x4000 && apeId === APE_GLOBALVARS) { + const r = new Reader(blob) + if (r.remaining >= 28) { + r.int32() // load time + r.bytes(24) // unused + const gv = { init: ntAt(r), frame: ntAt(r), beat: ntAt(r) } + return { ...base, kind: 'globalvars', gv } + } + return { ...base, kind: 'other' } + } + if (id === ID_COMMENT) { + const r = new Reader(blob) + return { ...base, kind: 'comment', comment: r.lpString() } + } + return { ...base, kind: 'other' } +} + +function ntAt(r: Reader): string { + // null-terminated string starting at reader position (within the blob) + const start = r.pos + const d = r.data + let end = start + while (end < d.length && d[end] !== 0) end++ + r.pos = Math.min(end + 1, d.length) + return latin1(d.subarray(start, end)) +} + +function parseAvsFile(path: string): Effect[] { + const data = new Uint8Array(readFileSync(path)) + const magic = 'Nullsoft AVS Preset 0.2\x1a' + const head = latin1(data.subarray(0, magic.length)) + if (head !== magic) throw new Error(`bad magic: ${JSON.stringify(head.slice(0, 30))}`) + const r = new Reader(data.subarray(magic.length)) + r.byte() // clear flag + const effects: Effect[] = [] + while (r.remaining > 8) { + const id = r.int32() + let apeId: string | null = null + if (id >= 0x4000) apeId = latin1(r.bytes(32)).split('\0')[0] + const len = r.int32() + if (len < 0 || len > r.remaining) break + effects.push(parseEffect(id, apeId, r.bytes(len))) + } + return effects +} + +// --------------------------------------------------------------------------- +// AVSTrans #define macro preprocessor +// --------------------------------------------------------------------------- + +interface Macro { + name: string + params: string[] | null // null = object-like + body: string +} + +/** AVSTrans built-in constants (available even without an AVSTrans module). */ +const AVSTRANS_BUILTINS: Macro[] = [ + { name: '$pi', params: null, body: '3.14159265358979' }, + { name: '$twopi', params: null, body: '6.28318530717959' }, + { name: '$e', params: null, body: '2.71828182845905' }, + { name: '$phi', params: null, body: '1.61803398874989' }, +] + +function extractDefines(sources: string[]): { macros: Macro[]; failNote: string | null } { + const macros: Macro[] = [] + let failNote: string | null = null + const defineRe = /^[ \t]*#define\s+(.+)$/gm + for (const src of sources) { + if (!src.includes('#define')) continue + // join backslash line-continuations first + const joined = src.replace(/\\\r?\n/g, ' ') + const matches = joined.match(defineRe) ?? [] + for (const m of matches) { + const rest = m.replace(/^[ \t]*#define\s+/, '').trim() + const fn = rest.match(/^([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*([\s\S]*)$/) + if (fn) { + const params = fn[2].split(',').map((p) => p.trim()).filter(Boolean) + macros.push({ name: fn[1], params, body: fn[3] }) + } else { + const obj = rest.match(/^([A-Za-z0-9_$]+)\s+([\s\S]*)$/) + if (obj) macros.push({ name: obj[1], params: null, body: obj[2] }) + else failNote = 'unparsed #define' + } + } + } + // user defines override the AVSTrans built-ins + const byName = new Map(macros.map((m) => [m.name, m])) + for (const b of AVSTRANS_BUILTINS) if (!byName.has(b.name)) macros.push(b) + return { macros, failNote } +} + +/** Split a macro CALL starting at `openPos` (must be '('). */ +function readCallArgs(src: string, openPos: number): { args: string[]; end: number } | null { + if (src[openPos] !== '(') return null + let depth = 0 + let start = openPos + 1 + const args: string[] = [] + for (let i = openPos; i < src.length; i++) { + const c = src[i] + if (c === '(') depth++ + else if (c === ')') { + depth-- + if (depth === 0) { + args.push(src.slice(start, i)) + return { args, end: i + 1 } + } + } else if (c === ',' && depth === 1) { + args.push(src.slice(start, i)) + start = i + 1 + } + } + return null +} + +function applyMacros(code: string, macros: Macro[], depth = 0): string { + if (depth > 24 || !macros.length) return code + let out = code + let changed = false + for (const macro of macros) { + const re = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRe(macro.name)}($|[^A-Za-z0-9_(]|\\()`, 'g') + let result = '' + let last = 0 + let m: RegExpExecArray | null + let macroChanged = false + while ((m = re.exec(out))) { + const at = m.index + m[1].length + // skip #define lines themselves + const lineStart = out.lastIndexOf('\n', at) + 1 + const lineHead = out.slice(lineStart, at).trimStart() + if (lineHead.startsWith('#')) continue + if (macro.params) { + const call = readCallArgs(out, at + macro.name.length) + if (!call || call.args.length !== macro.params.length) continue + let body = macro.body + // token-concat ##param## first, then plain params + for (let i = 0; i < macro.params.length; i++) { + body = body.split(`##${macro.params[i]}##`).join(call.args[i].trim()) + } + for (let i = 0; i < macro.params.length; i++) { + body = body.replace(new RegExp(`(^|[^A-Za-z0-9_$])${escapeRe(macro.params[i])}($|[^A-Za-z0-9_])`, 'g'), + (_s, pre, post) => `${pre}(${call.args[i].trim()})${post}`) + } + result += out.slice(last, at) + `(${body})` + last = call.end + } else { + result += out.slice(last, at) + `(${macro.body})` + last = at + macro.name.length + } + macroChanged = true + re.lastIndex = last + } + if (macroChanged) { + result += out.slice(last) + out = result + changed = true + } + } + if (changed) return applyMacros(out, macros, depth + 1) + return out +} + +function escapeRe(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function stripDirectives(code: string): string { + return code + .split('\n') + .filter((l) => !/^\s*#/.test(l)) + .join('\n') +} + +// --------------------------------------------------------------------------- +// code translation +// --------------------------------------------------------------------------- + +function translateCode(code: string, spectrumSource: boolean): string { + let out = code + // linesize -> size (AVS per-point line width) + out = out.replace(/\blinesize\b/g, 'size') + // AVS rand(n) returns an integer 0..n-1 — our deterministic rand(seed) differs + out = out.replace(/\brand\b/g, 'rnd') + // spectrum-source scopes: v should read the spectrum (AVS: byte/128-1) + if (spectrumSource) out = out.replace(/\bv\b/g, '(sv*2-1)') + return out +} + +function assignsVar(code: string, name: string): boolean { + return new RegExp(`(^|[^A-Za-z0-9_])${name}\\s*=[^=]`).test(code) +} + +// --------------------------------------------------------------------------- +// candidate collection +// --------------------------------------------------------------------------- + +interface Candidates { + scopes: Array + eeltrans: string[] + gv: Array<{ init: string; frame: string; beat: string }> + comment: string | null + otherKinds: Set +} + +function collect(effects: Effect[], into: Candidates): void { + for (const e of effects) { + if (!e.enabled) continue + switch (e.kind) { + case 'superscope': + case 'texer2': + into.scopes.push(e as Effect & { kind: 'superscope' | 'texer2' }) + break + case 'eeltrans': + if (e.transCode) into.eeltrans.push(e.transCode) + break + case 'globalvars': + if (e.gv) into.gv.push(e.gv) + break + case 'comment': + if (!into.comment && e.comment) into.comment = e.comment + break + case 'effectlist': + if (e.children) collect(e.children, into) + break + default: + into.otherKinds.add(describeEffect(e)) + } + } +} + +const BUILTIN_NAMES: Record = { + 0: 'Simple', 1: 'DotPlane', 2: 'OscilloscopeStar', 3: 'Fadeout', 4: 'BlitterFeedback', + 5: 'OnBeatClear', 6: 'Blur', 7: 'BassSpin', 8: 'MovingParticle', 9: 'RotoBlitter', 10: 'SVP', + 11: 'Colorfade', 12: 'ColorClip', 13: 'RotStar', 14: 'Ring', 15: 'Movement', 16: 'Scatter', + 17: 'DotGrid', 18: 'BufferSave', 19: 'DotFountain', 20: 'Water', 22: 'Brightness', 23: 'Interleave', + 24: 'Grain', 25: 'ClearScreen', 26: 'Mirror', 27: 'Starfield', 29: 'Bump', 30: 'Mosaic', + 31: 'WaterBump', 32: 'Video', 33: 'CustomBPM', 34: 'Picture', 35: 'DynamicDistanceModifier', + 37: 'Invert', 38: 'UniqueTone', 39: 'Timescope', 40: 'SetRenderMode', 41: 'Interferences', + 42: 'DynamicShift', 43: 'DynamicMovement', 44: 'FastBright', 45: 'ColorModifier', +} + +function describeEffect(e: Effect): string { + if (e.apeId) return e.apeId + return BUILTIN_NAMES[e.id] ?? `effect#${e.id}` +} + +// --------------------------------------------------------------------------- +// fallback templates (for presets whose visuals are movement/APE driven) +// --------------------------------------------------------------------------- + +function fallbackTemplate(seed: number, name: string): { content: string; style: string } { + const styles = [ + { + style: 'pulse rings', + mode: 'lines', + content: `name: ${JSON.stringify(name)} +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) +`, + }, + { + style: 'tunnel', + mode: 'lines', + content: `name: ${JSON.stringify(name)} +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) +`, + }, + { + style: 'starfield', + mode: 'dots', + content: `name: ${JSON.stringify(name)} +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) +`, + }, + { + style: 'lattice', + mode: 'dots', + content: `name: ${JSON.stringify(name)} +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 +`, + }, + { + style: 'swirl', + mode: 'lines', + content: `name: ${JSON.stringify(name)} +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) +`, + }, + { + style: 'kaleido', + mode: 'lines', + content: `name: ${JSON.stringify(name)} +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) +`, + }, + ] + return styles[seed % styles.length] +} + +// --------------------------------------------------------------------------- +// smoke test — runs the generated script headlessly through the real engine env +// --------------------------------------------------------------------------- + +function smokeTest(content: string): { ok: boolean; error?: string; n: number; ms: number } { + const t0 = Date.now() + const v = validateScript(content) + if (!v.ok || !v.script) return { ok: false, error: v.error ? `${v.error.section} line ${v.error.line}: ${v.error.message}` : 'parse failed', n: 0, ms: 0 } + resetEelBuffers() + AVA_HOOKS.spectrumAt = (p) => 0.25 + 0.25 * Math.sin(p * 18 + 1) + AVA_HOOKS.waveAt = (p) => Math.sin(p * 12.56) * 0.8 + const env: Record = { n: 400, t: 0, time: 0, frame: 0 } + const script = v.script + try { + script.initProg?.exec(env) + } catch (err) { + return { ok: false, error: `init: ${String(err)}`, n: 0, ms: Date.now() - t0 } + } + const n = Math.min(512, Math.max(4, Math.round(env.n ?? 400))) + let nan = 0 + try { + for (let f = 0; f < 36; f++) { + env.dt = 1 / 60 + env.time = (f + 1) / 60 + env.frame = f + 1 + AVA_HOOKS.now = () => env.time ?? 0 + env.beat = f % 15 === 0 ? 1 : 0 + env.bass = 0.3 + 0.3 * Math.sin(f * 0.4) + env.mid = 0.3 + env.treb = 0.25 + env.bass_att = env.bass + env.mid_att = 0.3 + env.treb_att = 0.25 + env.vol = 0.4 + env.vol_att = 0.4 + env.w = 1280 + env.h = 720 + env.aspect = 1280 / 720 + script.frameProg?.exec(env) + if (env.beat) script.beatProg?.exec(env) + for (let k = 0; k < n; k++) { + env.i = k / (n - 1 || 1) + env.v = AVA_HOOKS.waveAt(env.i) + env.sv = AVA_HOOKS.spectrumAt(env.i) + env.x = 0; env.y = 0; env.red = 1; env.green = 1; env.blue = 1; env.skip = 0; env.size = 2; env.alpha = 1 + script.pointProg?.exec(env) + if (!Number.isFinite(env.x) || !Number.isFinite(env.y)) nan++ + } + } + } catch (err) { + return { ok: false, error: `runtime: ${String(err)}`, n, ms: Date.now() - t0 } + } + const ms = Date.now() - t0 + // some presets deliberately NaN points (skip via NaN) — allow up to 40% + if (nan > n * 36 * 0.4) return { ok: false, error: `${nan} NaN points of ${n * 36}`, n, ms } + return { ok: true, n, ms } +} + +// --------------------------------------------------------------------------- +// name / author extraction +// --------------------------------------------------------------------------- + +function extractNameAuthor(filename: string): { name: string; author: string } { + let base = filename.replace(/\.avs$/i, '') + base = base.replace(/^\d{1,2}\s+/, '') // "01 dynamic duo - ..." numbering + // "Author - Name" (spaces around dash) + const spaced = base.match(/^(.+?)\s+-\s+(.+)$/) + if (spaced) return { author: spaced[1].trim(), name: spaced[2].trim() } + // "Author-Name" (no spaces — e.g. "zxe-Urantia", "Degnic-Adrealine rush") + const tight = base.match(/^([A-Za-z0-9_&']+)-([A-Za-z].+)$/) + if (tight) return { author: tight[1].trim(), name: tight[2].trim() } + return { author: '', name: base } +} + +function hashStr(s: string): number { + let h = 2166136261 + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i) + h = Math.imul(h, 16777619) + } + return Math.abs(h) +} + +// --------------------------------------------------------------------------- +// the converter +// --------------------------------------------------------------------------- + +export { parseAvsFile, collect, applyMacros, extractDefines, translateCode, smokeTest, extractNameAuthor, fallbackTemplate, convertOne } +export type { Effect, Candidates, OutputPreset, Macro } + +interface OutputPreset { + folder: string + name: string + author: string + description: string + originalFile: string + content: string +} + +function convertOne(file: string, relDir: string, absPath: string): { preset: OutputPreset; status: 'ported' | 'fallback'; note: string } { + const { name, author } = extractNameAuthor(file) + const effects = parseAvsFile(absPath) + const cands: Candidates = { scopes: [], eeltrans: [], gv: [], comment: null, otherKinds: new Set() } + collect(effects, cands) + + const folder = 'root/embedded/' + relDir + const originalFile = `/avs-originals/embedded/${relDir}/${file}` + const desc = (extra: string): string => { + const by = author ? ` by ${author}` : '' + const src = `Auto-ported from the Winamp AVS preset "${file}"${by}.` + return extra ? `${src} ${extra}` : src + } + + const makeFallback = (reason: string) => { + const tpl = fallbackTemplate(hashStr(name), name) + return { + preset: { + folder, + name, + author, + description: desc(`The original's visuals are driven by ${reason}, which the AVA script engine does not support — this is a generated stand-in that keeps the beat & band reactivity.`), + originalFile, + content: tpl.content, + }, + status: 'fallback' as const, + note: `${reason} → ${tpl.style} stand-in`, + } + } + + // pick the richest point program + const scored = cands.scopes + .map((s) => ({ s, score: (s.code?.point.length ?? 0) + (s.code?.frame.length ?? 0) * 0.3 })) + .sort((a, b) => b.score - a.score) + if (!scored.length || (scored[0].s.code?.point.length ?? 0) < 12) { + const reason = cands.otherKinds.size + ? Array.from(cands.otherKinds).slice(0, 3).join(', ') + : 'no codeable scope' + return makeFallback(reason) + } + const scope = scored[0].s + const code = scope.code! + + // reject trivial scopes (e.g. a 2-point checker) — they are not the real + // visual. If init pins a tiny literal n and the frame code never sets n, + // the preset's meat is elsewhere (movements / APEs). + const initNLit = code.init.match(/(^|[^A-Za-z0-9_])n\s*=\s*([0-9.]+)\s*;/) + const frameSetsN = assignsVar(code.frame, 'n') + if (initNLit && !frameSetsN && parseFloat(initNLit[2]) < 8) { + return makeFallback('a trivial scope (the real work is movement/APE driven)') + } + + // macro preprocessing (AVSTrans defines + local defines) + const macroSources = [...cands.eeltrans, code.init, code.frame, code.beat, code.point] + const { macros } = extractDefines(macroSources) + let init = code.init + let frame = code.frame + let beat = code.beat + let point = code.point + let gvNote = '' + if (macros.length) { + init = applyMacros(init, macros) + frame = applyMacros(frame, macros) + beat = applyMacros(beat, macros) + point = applyMacros(point, macros) + } + // merge Jheriko Global register-setters as a preamble (sets reg00=dt etc.) + if (cands.gv.length) { + const gv = cands.gv[0] + const gvi = stripDirectives(applyMacros(gv.init, macros)) + const gvf = stripDirectives(applyMacros(gv.frame, macros)) + const gvb = stripDirectives(applyMacros(gv.beat, macros)) + if (gvi.trim()) init = `${gvi}\n${init}` + if (gvf.trim()) frame = `${gvf}\n${frame}` + if (gvb.trim()) beat = `${gvb}\n${beat}` + gvNote = ' + global register code' + } + init = stripDirectives(init) + frame = stripDirectives(frame) + beat = stripDirectives(beat) + point = stripDirectives(point) + + // token rewrites + const spectrum = scope.spectrumSource === true + init = translateCode(init, spectrum) + frame = translateCode(frame, spectrum) + beat = translateCode(beat, spectrum) + point = translateCode(point, spectrum) + + // AVS `t` is auto-advancing time — if the code never assigns t, map reads to `time` + const allCode = `${init}\n${frame}\n${beat}\n${point}` + if (!assignsVar(allCode, 't')) { + init = init.replace(/\bt\b/g, 'time') + frame = frame.replace(/\bt\b/g, 'time') + beat = beat.replace(/\bt\b/g, 'time') + point = point.replace(/\bt\b/g, 'time') + } + + // default n if never set + if (!assignsVar(allCode, 'n')) init = `n = 400\n${init}` + + // color cycling fallback when the point code never sets colors + const setsColors = assignsVar(point, 'red') || assignsVar(point, 'green') || assignsVar(point, 'blue') + let colorFrame = '' + let colorPoint = '' + if (!setsColors && scope.colors && scope.colors.length) { + const cols = scope.colors.map((c) => ({ + r: ((c >> 16) & 255) / 255, + g: ((c >> 8) & 255) / 255, + b: (c & 255) / 255, + })) + const gen = cols + .map((c, i) => `if(avsi==${i}, avsr=${c.r.toFixed(3)}; avsg=${c.g.toFixed(3)}; avsb=${c.b.toFixed(3)};)`) + .join('\n') + colorFrame = `avsc = avsc + 1;\navsp = avsc % ${cols.length * 64};\navsi = floor(avsp / 64);\navsf = (avsp % 64) / 64;\n${gen}` + const c1 = cols[0] + colorFrame += `\nif(avsi == ${cols.length - 1}, avsr2=${c1.r.toFixed(3)}; avsg2=${c1.g.toFixed(3)}; avsb2=${c1.b.toFixed(3)};)` + for (let i = 0; i < cols.length - 1; i++) { + colorFrame += `\nif(avsi == ${i}, avsr2=${cols[i + 1].r.toFixed(3)}; avsg2=${cols[i + 1].g.toFixed(3)}; avsb2=${cols[i + 1].b.toFixed(3)};)` + } + colorFrame += `\navsr = avsr * (1 - avsf) + avsr2 * avsf;\navsg = avsg * (1 - avsf) + avsg2 * avsf;\navsb = avsb * (1 - avsf) + avsb2 * avsf;` + colorPoint = 'red = avsr;\ngreen = avsg;\nblue = avsb;' + } else if (!setsColors) { + colorFrame = 'avsh = avsh + dt * 0.15;' + colorPoint = 'red = 0.55 + 0.45 * sin(avsh * 6.28318);\ngreen = 0.55 + 0.45 * sin(avsh * 6.28318 + 2.09);\nblue = 0.55 + 0.45 * sin(avsh * 6.28318 + 4.19);' + } + + const mode = scope.kind === 'texer2' ? 'dots' : scope.drawMode === 0 ? 'dots' : 'lines' + const linewidth = mode === 'dots' ? 3 : 2 + + const content = `# Auto-ported from "${file}" (Winamp AVS${author ? ` / ${author}` : ''}) +# ${scope.kind === 'texer2' ? 'Texer II' : 'SuperScope'} point program${gvNote}${spectrum ? ', spectrum source' : ''}${macros.length ? `, ${macros.length} AVSTrans macros expanded` : ''}. +name: ${JSON.stringify(name)} +author: ${JSON.stringify(author)} +mode: ${mode} +blend: additive +persist: ${mode === 'dots' ? 0.78 : 0.88} +linewidth: ${linewidth} +ysign: -1 + +[init] +${init.trim() || 'n = 400'} + +[frame] +${[frame.trim(), colorFrame].filter(Boolean).join('\n')} + +[beat] +${beat.trim() || 'flash = 1'} + +[point] +${[point.trim(), colorPoint].filter(Boolean).join('\n')} +` + + const smoke = smokeTest(content) + if (!smoke.ok) { + return makeFallback(`unportable scope code (${smoke.error})`) + } + const perf = smoke.ms > 900 ? ' (heavy script — point count clamped)' : '' + if (smoke.ms > 900) { + // clamp n for heavy scripts so the browser keeps 60fps + const patched = content.replace(/\[init\]\n/, `[init]\nn = if(above(n, 2048), 2048, n)\n`) + const recheck = smokeTest(patched) + if (recheck.ok) { + return { + preset: { folder, name, author, description: desc(`Original ${scope.kind} code, machine-translated to the AVA script dialect.${perf}`), originalFile, content: patched }, + status: 'ported', + note: `ok (n clamped, ${smoke.ms}ms → ${recheck.ms}ms)`, + } + } + } + return { + preset: { folder, name, author, description: desc(`Original ${scope.kind} code, machine-translated to the AVA script dialect.`), originalFile, content }, + status: 'ported', + note: `ok (${smoke.ms}ms, n=${smoke.n})`, + } +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +const inputRoot = process.argv[2] ?? '/tmp/avsdrive/raw' +const outDir = 'tools/converted' +const originalsDir = 'public/avs-originals/embedded' + +const SKIP_FOLDERS = new Set(['Synth-C']) // hand-adapted versions ship in BUILTIN_PRESETS + +function walk(dir: string): string[] { + const out: string[] = [] + for (const entry of readdirSync(dir)) { + const p = join(dir, entry) + if (statSync(p).isDirectory()) { + if (SKIP_FOLDERS.has(entry)) continue + out.push(...walk(p)) + } else if (entry.toLowerCase().endsWith('.avs')) out.push(p) + } + return out +} + +const isMain = typeof process !== 'undefined' && process.argv[1] && process.argv[1].includes('convert-avs') +if (!isMain) { + // imported as a library — skip main +} else { +main() +} + +function main() { + +const files = walk(inputRoot).sort() +console.log(`converting ${files.length} .avs files from ${inputRoot}`) +const results: OutputPreset[] = [] +const report: Array<{ file: string; status: string; note: string }> = [] +let ported = 0 +let fallback = 0 + +for (const abs of files) { + const rel = relative(inputRoot, abs) + const relDir = dirname(rel) === '.' ? '' : dirname(rel) + const file = basename(rel) + try { + const r = convertOne(file, relDir, abs) + results.push(r.preset) + report.push({ file: rel, status: r.status, note: r.note }) + if (r.status === 'ported') ported++ + else fallback++ + console.log(`${r.status === 'ported' ? '✔' : '○'} ${rel} — ${r.note}`) + // keep a copy of the generated .ava for reference + const destAva = join(outDir, relDir, file.replace(/\.avs$/i, '.ava')) + mkdirSync(dirname(destAva), { recursive: true }) + writeFileSync(destAva, r.preset.content) + // copy the original binary + const destOrig = join(originalsDir, relDir, file) + mkdirSync(dirname(destOrig), { recursive: true }) + copyFileSync(abs, destOrig) + } catch (err) { + report.push({ file: rel, status: 'error', note: String(err) }) + fallback++ + console.log(`✘ ${rel} — ${String(err)}`) + } +} + +// emit the TS module +const seen = new Set() +for (const p of results) { + const key = `${p.folder}//${p.name}` + if (seen.has(key)) console.log(`!! duplicate name in folder: ${key}`) + seen.add(key) +} + +const tsHeader = `// AVA — embedded preset library (AUTO-GENERATED by tools/convert-avs.ts — do not edit) +// +// ${results.length} Winamp AVS presets from the user's Google Drive collection, +// auto-ported to the AVA script dialect by the converter. Ported presets run +// the original SuperScope / Texer II code (machine-translated); stand-in +// presets are generated visuals for originals that rely on effects the script +// engine does not support (movements, APEs). + +export interface EmbeddedPreset { + folder: string + name: string + author: string + description: string + originalFile?: string + content: string +} + +` + +const lines: string[] = [] +for (const p of results) { + lines.push(` {\n folder: ${JSON.stringify(p.folder)},\n name: ${JSON.stringify(p.name)},\n author: ${JSON.stringify(p.author)},\n description: ${JSON.stringify(p.description)},\n originalFile: ${JSON.stringify(p.originalFile)},\n content: ${JSON.stringify(p.content)},\n },`) +} +const ts = tsHeader + `export const EMBEDDED_LIBRARY: EmbeddedPreset[] = [\n${lines.join('\n')}\n]\n` +writeFileSync('src/lib/ava/embedded-presets.ts', ts) + +console.log(`\nDone: ${ported} ported, ${fallback} fallback, ${results.length} total`) +console.log(`→ src/lib/ava/embedded-presets.ts (${(ts.length / 1024).toFixed(0)} KB)`) +console.log(`→ ${originalsDir}/ (${results.length} .avs originals)`) +writeFileSync('tools/convert-report.json', JSON.stringify(report, null, 1)) +} diff --git a/tools/convert-report.json b/tools/convert-report.json new file mode 100644 index 0000000..7ff475e --- /dev/null +++ b/tools/convert-report.json @@ -0,0 +1,497 @@ +[ + { + "file": "root/embedded/Amp Picks/D&L - Life Is Violated.avs", + "status": "ported", + "note": "ok (25ms, n=512)" + }, + { + "file": "root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).avs", + "status": "fallback", + "note": "DynamicMovement, Movement, Invert → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/Degnic-Adrealine rush.avs", + "status": "fallback", + "note": "en03: Convolution Filter, Simple, Color Map → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/Duo - Alien Heart.avs", + "status": "fallback", + "note": "Fadeout, ClearScreen, MovingParticle → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/Duo - Brainstorm.avs", + "status": "fallback", + "note": "Invert, UniqueTone, Brightness → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).avs", + "status": "fallback", + "note": "DynamicMovement, Color Map, Invert → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/L1quid - Take the Veil.avs", + "status": "fallback", + "note": "unportable scope code (frame line 15: Unexpected character '$') → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/Nic01 - Rototronik.avs", + "status": "ported", + "note": "ok (54ms, n=512)" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - ...hidden track....avs", + "status": "fallback", + "note": "DynamicMovement, Color Map → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - A Tribute To Degnic.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.avs", + "status": "fallback", + "note": "BlitterFeedback, DynamicMovement, Movement → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Chinook.avs", + "status": "fallback", + "note": "ColorClip, ClearScreen, DynamicMovement → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.avs", + "status": "fallback", + "note": "Color Map → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.avs", + "status": "fallback", + "note": "effect#2176, DynamicMovement → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Kingish.avs", + "status": "fallback", + "note": "BlitterFeedback → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).avs", + "status": "fallback", + "note": "BlitterFeedback, \u0005, DotPlane → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Texering 002.avs", + "status": "ported", + "note": "ok (45ms, n=100)" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - Upward Current.avs", + "status": "fallback", + "note": "CustomBPM, DynamicMovement, Water → tunnel stand-in" + }, + { + "file": "root/embedded/Amp Picks/S_KuPeRS - female demands.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Aqua-fumes.avs", + "status": "fallback", + "note": "Holden03: Convolution Filter, Color Map, Bump → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).avs", + "status": "fallback", + "note": "Movement, DynamicMovement, Multiplier → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Different Load.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Electro blobs.avs", + "status": "fallback", + "note": "MovingParticle, Color Map, BlitterFeedback → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Kaleidofied.avs", + "status": "fallback", + "note": "Invert, Movement, Bump → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Magma Flow II.avs", + "status": "fallback", + "note": "Color Map, BufferSave, ClearScreen → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.avs", + "status": "fallback", + "note": "Movement, Color Map, BufferSave → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Odour Control.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Pina Colada.avs", + "status": "fallback", + "note": "Color Map → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.avs", + "status": "fallback", + "note": "effect#117, Color Map, Invert → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Solero Shots.avs", + "status": "fallback", + "note": "effect#28, FastBright, Movement → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Square Minded.avs", + "status": "fallback", + "note": "FastBright → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).avs", + "status": "fallback", + "note": "MovingParticle, Filter, Simple → tunnel stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Zero-G Maze.avs", + "status": "ported", + "note": "ok (9ms, n=50)" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Cartoon Trip.avs", + "status": "fallback", + "note": "DynamicMovement, FastBright, Color Reduction → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Containment.avs", + "status": "fallback", + "note": "unportable scope code (init line 12: Unexpected character '£') → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Cubic Flower.avs", + "status": "ported", + "note": "ok (8ms, n=40)" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Jello Cube.avs", + "status": "fallback", + "note": "Color Map → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Mercury.avs", + "status": "fallback", + "note": "Blur, DynamicMovement, FastBright → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Neon Coaster.avs", + "status": "fallback", + "note": "unportable scope code (beat line 18: Unexpected character '¤') → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Plankton.avs", + "status": "fallback", + "note": "DynamicMovement, FastBright → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Reflectocube.avs", + "status": "fallback", + "note": "Movement, DynamicMovement → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Speeder 3K.avs", + "status": "fallback", + "note": "unportable scope code (init line 12: Unexpected character '¤') → tunnel stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - TC Plasma.avs", + "status": "fallback", + "note": "CustomBPM, DynamicMovement, FastBright → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.avs", + "status": "fallback", + "note": "unportable scope code (init line 12: Unexpected character '¤') → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD - Zero-G Maze II.avs", + "status": "fallback", + "note": "DynamicMovement → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/UnConeD -Retrosquares.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/UnConeD -Zero-G Maze III.avs", + "status": "fallback", + "note": "BlitterFeedback, Movement, DynamicMovement → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/Yathosho - microcosm 02.avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/amphirion - nebulous (skupers remix).avs", + "status": "fallback", + "note": "Water, UniqueTone, Interleave → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → starfield stand-in" + }, + { + "file": "root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).avs", + "status": "ported", + "note": "ok (59ms, n=500)" + }, + { + "file": "root/embedded/Amp Picks/horse-fly - jinx (skupers remix).avs", + "status": "fallback", + "note": "ColorClip, Simple, Mosaic → tunnel stand-in" + }, + { + "file": "root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).avs", + "status": "fallback", + "note": "Interleave, DynamicMovement, effect#737 → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/justin - newpoop (for wa5).avs", + "status": "fallback", + "note": "ColorModifier → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).avs", + "status": "fallback", + "note": "DynamicDistanceModifier, ColorModifier → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Dragonscales.avs", + "status": "fallback", + "note": "CustomBPM, BufferSave, MovingParticle → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Encapsulade.avs", + "status": "fallback", + "note": "BufferSave, SetRenderMode, CustomBPM → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Freekwyrms.avs", + "status": "fallback", + "note": "BufferSave, SetRenderMode, MovingParticle → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Lilypod.avs", + "status": "fallback", + "note": "BufferSave, MovingParticle, 2.8+ Effect List Config → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Mist Wisps.avs", + "status": "fallback", + "note": "CustomBPM, BufferSave, MovingParticle → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.avs", + "status": "fallback", + "note": "CustomBPM, BufferSave, Fadeout → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Reverb - Deep Net.avs", + "status": "fallback", + "note": "CustomBPM, BufferSave, Fadeout → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Slyde - Tri.avs", + "status": "fallback", + "note": "BufferSave, SetRenderMode, MovingParticle → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Solar Flaire.avs", + "status": "fallback", + "note": "BufferSave, Fadeout, SetRenderMode → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Spintronics - Backspin.avs", + "status": "fallback", + "note": "2.8+ Effect List Config, Simple, SetRenderMode → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Starslappin.avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Street Art - Fire Inside.avs", + "status": "fallback", + "note": "BufferSave, Colorfade, Movement → swirl stand-in" + }, + { + "file": "root/embedded/Amp Picks/mig - Swoo - Slow.avs", + "status": "fallback", + "note": "CustomBPM, BufferSave, Movement → kaleido stand-in" + }, + { + "file": "root/embedded/Amp Picks/nixa - smell of music (skupers remix).avs", + "status": "fallback", + "note": "Mirror, DynamicMovement, Movement → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/p-k - bumpy.avs", + "status": "fallback", + "note": "CustomBPM, Color Map → pulse rings stand-in" + }, + { + "file": "root/embedded/Amp Picks/skupers - chinook (exclusive version).avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/splendora.avs", + "status": "fallback", + "note": "Grain, Simple, Movement → lattice stand-in" + }, + { + "file": "root/embedded/Amp Picks/yathosho - fabric (skupers remix).avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Amp Picks/zxe-Urantia.avs", + "status": "fallback", + "note": "effect#326, \u0001, UniqueTone → kaleido stand-in" + }, + { + "file": "root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → swirl stand-in" + }, + { + "file": "root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → tunnel stand-in" + }, + { + "file": "root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).avs", + "status": "ported", + "note": "ok (16ms, n=40)" + }, + { + "file": "root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).avs", + "status": "fallback", + "note": "unportable scope code (frame line 19: Expected ')' but found ';') → swirl stand-in" + }, + { + "file": "root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).avs", + "status": "ported", + "note": "ok (2ms, n=4)" + }, + { + "file": "root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → kaleido stand-in" + }, + { + "file": "root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → swirl stand-in" + }, + { + "file": "root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → pulse rings stand-in" + }, + { + "file": "root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).avs", + "status": "fallback", + "note": "unportable scope code (frame line 20: Expected ')' but found ';') → lattice stand-in" + }, + { + "file": "root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).avs", + "status": "ported", + "note": "ok (32ms, n=4)" + }, + { + "file": "root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).avs", + "status": "ported", + "note": "ok (4ms, n=50)" + }, + { + "file": "root/embedded/Envision/03 zamuz - waterburn (duo's icy hot mix).avs", + "status": "error", + "note": "Error: out of bytes" + }, + { + "file": "root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → lattice stand-in" + }, + { + "file": "root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).avs", + "status": "fallback", + "note": "unportable scope code (frame line 19: Expected ')' but found ';') → starfield stand-in" + }, + { + "file": "root/embedded/Envision/06 skupers - incantation (duo remix - edit).avs", + "status": "ported", + "note": "ok (8ms, n=4)" + }, + { + "file": "root/embedded/Envision/07 duo - miraculous flow (nemo remix).avs", + "status": "fallback", + "note": "UniqueTone, DynamicMovement, FastBright → swirl stand-in" + }, + { + "file": "root/embedded/Envision/08 duo - warm freeform (mykal remix).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → starfield stand-in" + }, + { + "file": "root/embedded/Envision/09 zxe - pleasure point (duo remix).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → pulse rings stand-in" + }, + { + "file": "root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → lattice stand-in" + }, + { + "file": "root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).avs", + "status": "fallback", + "note": "DynamicMovement, BufferSave, 2.8+ Effect List Config → lattice stand-in" + }, + { + "file": "root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).avs", + "status": "fallback", + "note": "unportable scope code (frame line 19: Expected ')' but found ';') → pulse rings stand-in" + }, + { + "file": "root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).avs", + "status": "ported", + "note": "ok (1ms, n=10)" + }, + { + "file": "root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).avs", + "status": "ported", + "note": "ok (4ms, n=15)" + }, + { + "file": "root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).avs", + "status": "ported", + "note": "ok (49ms, n=400)" + }, + { + "file": "root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).avs", + "status": "fallback", + "note": "a trivial scope (the real work is movement/APE driven) → swirl stand-in" + } +] \ No newline at end of file diff --git a/tools/converted/root/embedded/Amp Picks/D&L - Life Is Violated.ava b/tools/converted/root/embedded/Amp Picks/D&L - Life Is Violated.ava new file mode 100644 index 0000000..a3c33d8 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/D&L - Life Is Violated.ava @@ -0,0 +1,53 @@ +# Auto-ported from "D&L - Life Is Violated.avs" (Winamp AVS / D&L) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "Life Is Violated" +author: "D&L" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=800;xa=-0.5;ya=0.0;xb=-0.0;yb=0.75;c=200;f=0; +nxa=(rnd(100)-50)*.02;nya=(rnd(100)-50)*.02; +nxb=(rnd(100)-50)*.02;nyb=(rnd(100)-50)*.02; + +[frame] +f=f+1; +t=1-((cos((f*3.1415)/c)+1)*.5); +xa=((nxa-lxa)*t)+lxa; +ya=((nya-lya)*t)+lya; +xb=((nxb-lxb)*t)+lxb; +yb=((nyb-lyb)*t)+lyb; +ex=(xb-xa); +ey=(yb-ya); +d=sqrt(sqr(ex)+sqr(ey)); +r=atan(ey/ex)+(3.1415/2); +dv=d*2 + +[beat] +c=f; +f=0; +lxa=nxa; +lya=nya; +lxb=nxb; +lyb=nyb; +nxa=(rnd(100)-50)*.02; +nya=(rnd(100)-50)*.02; +nxb=(rnd(100)-50)*.02; +nyb=(rnd(100)-50)*.02 + +[point] +//primary render +x=(ex*i)+xa; +y=(ey*i)+ya; + +//volume offset +x=x+ ( cos(r) * v * dv); +y=y+ ( sin(r) * v * dv); + +//color values +red=i; +green=(1-i); +blue=abs(v*6); diff --git a/tools/converted/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).ava b/tools/converted/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).ava new file mode 100644 index 0000000..506130c --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Degnic - Retrowall (Yathosho's Supermicrocosm).ava @@ -0,0 +1,25 @@ +name: "Retrowall (Yathosho's Supermicrocosm)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Degnic-Adrealine rush.ava b/tools/converted/root/embedded/Amp Picks/Degnic-Adrealine rush.ava new file mode 100644 index 0000000..796f723 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Degnic-Adrealine rush.ava @@ -0,0 +1,25 @@ +name: "Adrealine rush" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Duo - Alien Heart.ava b/tools/converted/root/embedded/Amp Picks/Duo - Alien Heart.ava new file mode 100644 index 0000000..61b26e3 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Duo - Alien Heart.ava @@ -0,0 +1,29 @@ +name: "Alien Heart" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Duo - Brainstorm.ava b/tools/converted/root/embedded/Amp Picks/Duo - Brainstorm.ava new file mode 100644 index 0000000..b0364c1 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Duo - Brainstorm.ava @@ -0,0 +1,25 @@ +name: "Brainstorm" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).ava b/tools/converted/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).ava new file mode 100644 index 0000000..4058060 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/EL-VIS - Molecules (S_KuPeRS Remix).ava @@ -0,0 +1,25 @@ +name: "Molecules (S_KuPeRS Remix)" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/L1quid - Take the Veil.ava b/tools/converted/root/embedded/Amp Picks/L1quid - Take the Veil.ava new file mode 100644 index 0000000..0054f09 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/L1quid - Take the Veil.ava @@ -0,0 +1,29 @@ +name: "Take the Veil" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/Nic01 - Rototronik.ava b/tools/converted/root/embedded/Amp Picks/Nic01 - Rototronik.ava new file mode 100644 index 0000000..46d3e9e --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Nic01 - Rototronik.ava @@ -0,0 +1,39 @@ +# Auto-ported from "Nic01 - Rototronik.avs" (Winamp AVS / Nic01) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "Rototronik" +author: "Nic01" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +pi=acos(-1);n=1000; + +[frame] +rx=rx+rdx;ry=ry+rdy;rz=rz+rdz;xs=sin(rx);ys=sin(ry);zs=sin(rz);xc=cos(rx);yc=cos(ry);zc=cos(rz);t=t+0.1;p=0;p1=0.; + +[beat] +rdx=(getosc(0,0,0)*500-250)%250/2500;rdy=(getosc(0.05,0,0)*500-250)%250/2500;rdz=(getosc(0.1,0,0)*500-250)%250/2500; + +[point] +p=p+1; +p1=if(equal(p%20,1),p1+1,p1); +s=(1-equal(p%20,1)); +s1=1-s1; +x1=sin(i*pi*4)*s*s1; +y1=cos(i*pi*4)*s*s1; +x1=x1+getosc((p1%25)*0.04,0,0)*x1; +y1=y1+getosc((p1%25)*0.04,0,0)*y1; +z1=-below(p,500)*0.5+0.25; +c=(2-x1*x1-y1*y1); +; +y2=y1*xc-z1*xs;z2=y1*xs+z1*xc; +x2=z2*ys+x1*yc;z1=z2*yc-x1*ys; +x1=x2*zc-y2*zs;y1=y2*zc+x2*zs; +x=x1/(2+z1);y=y1/(2+z1); +red=c; +green=1-c; +c=equal(p,500)*10; +red=red-c;green=green-c; diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....ava new file mode 100644 index 0000000..8fc37e1 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - ...hidden track....ava @@ -0,0 +1,25 @@ +name: "...hidden track..." +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.ava new file mode 100644 index 0000000..88f1aeb --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Arctic Rainbow.ava @@ -0,0 +1,29 @@ +name: "Arctic Rainbow" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Chinook.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Chinook.ava new file mode 100644 index 0000000..edeabd6 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Chinook.ava @@ -0,0 +1,29 @@ +name: "Chinook" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.ava new file mode 100644 index 0000000..053bacc --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Hexual Healing.ava @@ -0,0 +1,25 @@ +name: "Hexual Healing" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.ava new file mode 100644 index 0000000..4acce55 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Inverse Kinetics.ava @@ -0,0 +1,31 @@ +name: "Inverse Kinetics" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Kingish.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Kingish.ava new file mode 100644 index 0000000..a5ced2c --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Kingish.ava @@ -0,0 +1,25 @@ +name: "Kingish" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).ava new file mode 100644 index 0000000..8f9d0bc --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Spirit Realm (Degnic's Plasmoid RMX).ava @@ -0,0 +1,25 @@ +name: "Spirit Realm (Degnic's Plasmoid RMX)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Texering 002.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Texering 002.ava new file mode 100644 index 0000000..2a9972b --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Texering 002.ava @@ -0,0 +1,39 @@ +# Auto-ported from "S_KuPeRS - Texering 002.avs" (Winamp AVS / S_KuPeRS) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "Texering 002" +author: "S_KuPeRS" +mode: dots +blend: additive +persist: 0.78 +linewidth: 3 +ysign: -1 + +[init] +n=100;pi=acos(-1);rx=1.57;ry=1.57;rx=0;ry=0;rz=0;rxo=0;ryo=0;rzo=0;rxt=0;ryt=0;rzt=0; + +[frame] +redf=redf+redr;greenf=greenf+greenr;bluef=bluef+bluer;rx=rx+rxo;ry=ry+ryo;rz=rz+rzo;rzo=rzo*0.97;cx=cos(sin(rx));sx=sin(sin(rx));cy=cos(sin(ry));sy=sin(sin(ry));cz=cos(rz);sz=sin(rz); + +[beat] +redr=rnd(10)/100-0.05;greenr=rnd(10)/100-0.05;bluer=rnd(10)/100-0.05;rxo=getosc(0.5,0.02,0)/12;ryo=getosc(0.3,0.02,0)/12;rzo=getosc(0.7,0.02,0)/8; + +[point] +r=i*pi*2;r2=i*pi*99; +x1=sin(r-bluef*1.53+.0776)*cos(r2+greenf); +y1=cos(r+bluef); +z1=sin(r2+redf); + +x1d=x1*cz-y1*sz; +y1d=x1*sz+y1*cz; +y2d=y1d*cx-z1*sx; +z2d=y1d*sx+z1*cx; +x3d=x1d*cy-z2d*sy; +z3d=x1d*sy+z2d*cy; +zi=1/(z3d+3); +x=if(above(z3d+3,0.1),x3d*zi,x); +y=if(above(z3d+3,0.1),y2d*zi,y); + +alpha=if(below(z3d+2.4,0),0,1); +red=alpha*(1+sin(redf)/4-z3d/4)/7; +blue=alpha*(1+sin(bluef)/4-z3d)/20; +green=alpha*(1+sin(greenf)/3-z3d/2)/12; diff --git a/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Upward Current.ava b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Upward Current.ava new file mode 100644 index 0000000..181464e --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/S_KuPeRS - Upward Current.ava @@ -0,0 +1,25 @@ +name: "Upward Current" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.ava new file mode 100644 index 0000000..97d89b8 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Aqua-fumes.ava @@ -0,0 +1,25 @@ +name: "Aqua-fumes" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).ava new file mode 100644 index 0000000..e27f757 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Club Seizure (color enhanced mix).ava @@ -0,0 +1,25 @@ +name: "Club Seizure (color enhanced mix)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Electro blobs.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Electro blobs.ava new file mode 100644 index 0000000..3564940 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Electro blobs.ava @@ -0,0 +1,29 @@ +name: "Electro blobs" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Kaleidofied.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Kaleidofied.ava new file mode 100644 index 0000000..30a8415 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Kaleidofied.ava @@ -0,0 +1,29 @@ +name: "Kaleidofied" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Magma Flow II.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Magma Flow II.ava new file mode 100644 index 0000000..85f1bcd --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Magma Flow II.ava @@ -0,0 +1,25 @@ +name: "Magma Flow II" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.ava new file mode 100644 index 0000000..e811043 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Multiple Orgasms.ava @@ -0,0 +1,31 @@ +name: "Multiple Orgasms" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Pina Colada.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Pina Colada.ava new file mode 100644 index 0000000..ad0192f --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Pina Colada.ava @@ -0,0 +1,31 @@ +name: "Pina Colada" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.ava new file mode 100644 index 0000000..83637e4 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Scope in Spotlight II.ava @@ -0,0 +1,29 @@ +name: "Scope in Spotlight II" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Solero Shots.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Solero Shots.ava new file mode 100644 index 0000000..cb0acad --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Solero Shots.ava @@ -0,0 +1,25 @@ +name: "Solero Shots" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Square Minded.ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Square Minded.ava new file mode 100644 index 0000000..32d4f16 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Square Minded.ava @@ -0,0 +1,31 @@ +name: "Square Minded" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).ava b/tools/converted/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).ava new file mode 100644 index 0000000..246423c --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/Tuggummi - Worm-O-Rama (Raz Remix).ava @@ -0,0 +1,25 @@ +name: "Worm-O-Rama (Raz Remix)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze.ava new file mode 100644 index 0000000..3baa8aa --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze.ava @@ -0,0 +1,24 @@ +# Auto-ported from "UnConeD - Zero-G Maze.avs" (Winamp AVS / UnConeD) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "Zero-G Maze" +author: "UnConeD" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=50;ro=0;go=2.0;bo=4.0 + +[frame] +t=t-0.05;aa=aa*0.7; + +[beat] +aa=0.0;ro=rnd(100)/100*6.28;go=rnd(100)/100*6.28;bo=rnd(100)/100*6.28 + +[point] +y=0;x=i*.5-.1; +red=sin(t+ro+i*2)*.5+.5; +green=sin(t+go+i*2)*.5+.5; +blue=sin(t+bo+i*2)*.5+.5; diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Cartoon Trip.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Cartoon Trip.ava new file mode 100644 index 0000000..5f1be74 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Cartoon Trip.ava @@ -0,0 +1,25 @@ +name: "Cartoon Trip" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Containment.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Containment.ava new file mode 100644 index 0000000..7212b88 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Containment.ava @@ -0,0 +1,29 @@ +name: "Containment" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Cubic Flower.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Cubic Flower.ava new file mode 100644 index 0000000..2688209 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Cubic Flower.ava @@ -0,0 +1,25 @@ +# Auto-ported from "UnConeD - Cubic Flower.avs" (Winamp AVS / UnConeD) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "Cubic Flower" +author: "UnConeD" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=40;ro=0;go=2.0;bo=4.0 + +[frame] +t=t-0.05;aa=aa*0.7; + +[beat] +aa=1;ro=rnd(100)/100*6.28;go=rnd(100)/100*6.28;bo=rnd(100)/100*6.28 + +[point] +x=i*.8-.4;y=0; +red=sin(t+ro+i*2)/2+.5+aa; +green=sin(t+go+i*2)/2+.5+aa; +blue=sin(t+bo+i*2)/2+.5+aa; +blue=blue+1-min(red+green+blue,1) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Jello Cube.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Jello Cube.ava new file mode 100644 index 0000000..34e8763 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Jello Cube.ava @@ -0,0 +1,29 @@ +name: "Jello Cube" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Mercury.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Mercury.ava new file mode 100644 index 0000000..bb8c189 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Mercury.ava @@ -0,0 +1,29 @@ +name: "Mercury" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Neon Coaster.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Neon Coaster.ava new file mode 100644 index 0000000..7b56665 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Neon Coaster.ava @@ -0,0 +1,25 @@ +name: "Neon Coaster" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Plankton.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Plankton.ava new file mode 100644 index 0000000..489387e --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Plankton.ava @@ -0,0 +1,25 @@ +name: "Plankton" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Reflectocube.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Reflectocube.ava new file mode 100644 index 0000000..d8fd3c8 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Reflectocube.ava @@ -0,0 +1,25 @@ +name: "Reflectocube" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Speeder 3K.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Speeder 3K.ava new file mode 100644 index 0000000..ee43f07 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Speeder 3K.ava @@ -0,0 +1,25 @@ +name: "Speeder 3K" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - TC Plasma.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - TC Plasma.ava new file mode 100644 index 0000000..7aea4cf --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - TC Plasma.ava @@ -0,0 +1,25 @@ +name: "TC Plasma" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.ava new file mode 100644 index 0000000..43877e6 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Tie Tunnel SSC.ava @@ -0,0 +1,29 @@ +name: "Tie Tunnel SSC" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.ava b/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.ava new file mode 100644 index 0000000..2f138d1 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD - Zero-G Maze II.ava @@ -0,0 +1,29 @@ +name: "Zero-G Maze II" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.ava b/tools/converted/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.ava new file mode 100644 index 0000000..128aa3b --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/UnConeD -Zero-G Maze III.ava @@ -0,0 +1,25 @@ +name: "UnConeD -Zero-G Maze III" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).ava new file mode 100644 index 0000000..b5e4fae --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/amphirion - nebulous (skupers remix).ava @@ -0,0 +1,31 @@ +name: "nebulous (skupers remix)" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).ava new file mode 100644 index 0000000..f65bc7f --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/el-vis - soundscape III (skupers remix).ava @@ -0,0 +1,25 @@ +name: "soundscape III (skupers remix)" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).ava new file mode 100644 index 0000000..e9d3ca1 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/fck - checkers with metaballs (skupers remix).ava @@ -0,0 +1,28 @@ +# Auto-ported from "fck - checkers with metaballs (skupers remix).avs" (Winamp AVS / fck) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "checkers with metaballs (skupers remix)" +author: "fck" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=500;p=3.14159 + +[frame] +k=h/w*1.05;mxs=mxs+g;mys=mys+g2;mx=sin(mxs)*.8;my=sin(mys)*.8;t=t+.1 + +[beat] +g=getosc(.2,0,0)/3;g2=getosc(.8,0,0)/3 + +[point] +d=i/6+getosc(0,0,0)/5*i; +r=i*p*44; +x=(cos(r)*d+mx)*k; +y=sin(r)*d+my; +lig=sin(r)*above(sin(r),0)/2; +blue=(1-i)-lig; +green=blue; +red=sqrt(blue)*2-lig diff --git a/tools/converted/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).ava new file mode 100644 index 0000000..6ea8341 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/horse-fly - jinx (skupers remix).ava @@ -0,0 +1,25 @@ +name: "jinx (skupers remix)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) diff --git a/tools/converted/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).ava new file mode 100644 index 0000000..7ad6f26 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/jheriko - not quite a bendy tunnel (skupers remix).ava @@ -0,0 +1,29 @@ +name: "not quite a bendy tunnel (skupers remix)" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/justin - newpoop (for wa5).ava b/tools/converted/root/embedded/Amp Picks/justin - newpoop (for wa5).ava new file mode 100644 index 0000000..be432bd --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/justin - newpoop (for wa5).ava @@ -0,0 +1,25 @@ +name: "newpoop (for wa5)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).ava b/tools/converted/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).ava new file mode 100644 index 0000000..7596d34 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/justin - the landscape is killing me (wa5remix).ava @@ -0,0 +1,25 @@ +name: "the landscape is killing me (wa5remix)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Dragonscales.ava b/tools/converted/root/embedded/Amp Picks/mig - Dragonscales.ava new file mode 100644 index 0000000..e7010fd --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Dragonscales.ava @@ -0,0 +1,29 @@ +name: "Dragonscales" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/mig - Encapsulade.ava b/tools/converted/root/embedded/Amp Picks/mig - Encapsulade.ava new file mode 100644 index 0000000..4b546ba --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Encapsulade.ava @@ -0,0 +1,29 @@ +name: "Encapsulade" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Freekwyrms.ava b/tools/converted/root/embedded/Amp Picks/mig - Freekwyrms.ava new file mode 100644 index 0000000..a9e47cf --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Freekwyrms.ava @@ -0,0 +1,25 @@ +name: "Freekwyrms" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Lilypod.ava b/tools/converted/root/embedded/Amp Picks/mig - Lilypod.ava new file mode 100644 index 0000000..cc71b03 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Lilypod.ava @@ -0,0 +1,29 @@ +name: "Lilypod" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Mist Wisps.ava b/tools/converted/root/embedded/Amp Picks/mig - Mist Wisps.ava new file mode 100644 index 0000000..8c27cb5 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Mist Wisps.ava @@ -0,0 +1,29 @@ +name: "Mist Wisps" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.ava b/tools/converted/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.ava new file mode 100644 index 0000000..b8d7d4d --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Nebulating - Hypercolorspace.ava @@ -0,0 +1,25 @@ +name: "Nebulating - Hypercolorspace" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Reverb - Deep Net.ava b/tools/converted/root/embedded/Amp Picks/mig - Reverb - Deep Net.ava new file mode 100644 index 0000000..4289d51 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Reverb - Deep Net.ava @@ -0,0 +1,31 @@ +name: "Reverb - Deep Net" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Slyde - Tri.ava b/tools/converted/root/embedded/Amp Picks/mig - Slyde - Tri.ava new file mode 100644 index 0000000..ea6f45f --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Slyde - Tri.ava @@ -0,0 +1,25 @@ +name: "Slyde - Tri" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Solar Flaire.ava b/tools/converted/root/embedded/Amp Picks/mig - Solar Flaire.ava new file mode 100644 index 0000000..722939b --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Solar Flaire.ava @@ -0,0 +1,29 @@ +name: "Solar Flaire" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/mig - Spintronics - Backspin.ava b/tools/converted/root/embedded/Amp Picks/mig - Spintronics - Backspin.ava new file mode 100644 index 0000000..2982786 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Spintronics - Backspin.ava @@ -0,0 +1,31 @@ +name: "Spintronics - Backspin" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Starslappin.ava b/tools/converted/root/embedded/Amp Picks/mig - Starslappin.ava new file mode 100644 index 0000000..4505ebd --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Starslappin.ava @@ -0,0 +1,25 @@ +name: "Starslappin" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Street Art - Fire Inside.ava b/tools/converted/root/embedded/Amp Picks/mig - Street Art - Fire Inside.ava new file mode 100644 index 0000000..8817112 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Street Art - Fire Inside.ava @@ -0,0 +1,25 @@ +name: "Street Art - Fire Inside" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/mig - Swoo - Slow.ava b/tools/converted/root/embedded/Amp Picks/mig - Swoo - Slow.ava new file mode 100644 index 0000000..aa1025d --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/mig - Swoo - Slow.ava @@ -0,0 +1,31 @@ +name: "Swoo - Slow" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/nixa - smell of music (skupers remix).ava b/tools/converted/root/embedded/Amp Picks/nixa - smell of music (skupers remix).ava new file mode 100644 index 0000000..4fa500b --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/nixa - smell of music (skupers remix).ava @@ -0,0 +1,29 @@ +name: "smell of music (skupers remix)" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/p-k - bumpy.ava b/tools/converted/root/embedded/Amp Picks/p-k - bumpy.ava new file mode 100644 index 0000000..dc4bca5 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/p-k - bumpy.ava @@ -0,0 +1,29 @@ +name: "bumpy" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Amp Picks/splendora.ava b/tools/converted/root/embedded/Amp Picks/splendora.ava new file mode 100644 index 0000000..302aa51 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/splendora.ava @@ -0,0 +1,29 @@ +name: "splendora" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Amp Picks/zxe-Urantia.ava b/tools/converted/root/embedded/Amp Picks/zxe-Urantia.ava new file mode 100644 index 0000000..e40b3a5 --- /dev/null +++ b/tools/converted/root/embedded/Amp Picks/zxe-Urantia.ava @@ -0,0 +1,31 @@ +name: "Urantia" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).ava b/tools/converted/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).ava new file mode 100644 index 0000000..0e8c9b0 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/01 dynamic duo - tentaclism (fake hboy rmx).ava @@ -0,0 +1,25 @@ +name: "tentaclism (fake hboy rmx)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).ava b/tools/converted/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).ava new file mode 100644 index 0000000..2952217 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/02 dynamic duo - tentaclism (sky of contempt mix by drew).ava @@ -0,0 +1,25 @@ +name: "tentaclism (sky of contempt mix by drew)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 900 + +[frame] +t = t + dt * (0.25 + bass_att * 1.5) +hue = hue + dt * 0.1 + +[point] +ring = floor(i * 24) +ri = frac(i * 24) +z = frac(ring / 24 + t) +rad = 0.06 + z * 0.9 +a = ri * 6.28318 + ring * 0.35 + sin(t * 0.7) * 2 +x = sin(a) * rad * (0.55 + sv * 0.9) / aspect +y = cos(a) * rad * (0.55 + sv * 0.9) +f = (1 - z) * (0.35 + bass * 0.65) +red = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4)) +green = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 2.09)) +blue = f * (0.45 + 0.55 * sin(hue * 6.28318 + z * 4 + 4.19)) diff --git a/tools/converted/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).ava b/tools/converted/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).ava new file mode 100644 index 0000000..a669506 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).ava @@ -0,0 +1,27 @@ +# Auto-ported from "03 dynamic duo - tentaclism (microdee's grandpa was a tentaclist).avs" (Winamp AVS / dynamic duo) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "tentaclism (microdee's grandpa was a tentaclist)" +author: "dynamic duo" +mode: dots +blend: additive +persist: 0.78 +linewidth: 3 +ysign: -1 + +[init] +n=40; ln1=1; ln=ln1+2; +cns=.6; chs2=200; chs1=35; mrab=rnd(10)/20; + +[frame] +t=t-0.05; pc=0; sw=-1; sm=sm+rnd(20)/200; +srm=.9*(srm+rnd(100)*.0004); +ssm=sin(sm)*srm+rnd(100)*.0003; + +[beat] +flash = 1 + +[point] +sw=sw*-1; +x=if(above(pc,ln),rnd(100)/50-1,rnd(100)/2000+.27+ssm);y=if(above(pc,ln),rnd(100)/50-1,sw); +red=rnd(100)/100; green=red; blue=red; pc=pc+1; +drawmode=below(pc,ln); skip=band(below(pc,ln),equal(sw,1)) diff --git a/tools/converted/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).ava b/tools/converted/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).ava new file mode 100644 index 0000000..73e471a --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/04 dynamic duo - tentaclism (cathode ray mix by onionring of doom).ava @@ -0,0 +1,25 @@ +name: "tentaclism (cathode ray mix by onionring of doom)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).ava b/tools/converted/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).ava new file mode 100644 index 0000000..7982cd7 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/05 dynamic duo - chromatics (glowy mix by meaningless-existance).ava @@ -0,0 +1,24 @@ +# Auto-ported from "05 dynamic duo - chromatics (glowy mix by meaningless-existance).avs" (Winamp AVS / dynamic duo) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "chromatics (glowy mix by meaningless-existance)" +author: "dynamic duo" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=w;pi=acos(-1); + +[frame] +t=t-0.045;t2=t2-t2r;tm2=tm2*0.9+tm2c*0.1;tm=tm*0.9+tm2c*0.1; + +[beat] +tm2c=rnd(315)*0.04+1;tmc=rnd(315)*0.04+1;t2r=rnd(10)*0.01-0.05; + +[point] +x=i*2-1;y=-0.99; +red=sin(i*2*pi+t)*0.5+0.5;; +blue=sin(i*tm*2+t2)*0.5+0.5; +green=sin(i*tm2*2-t*0.33)*0.5+0.5; diff --git a/tools/converted/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).ava b/tools/converted/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).ava new file mode 100644 index 0000000..7459a92 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/06 dynamic duo - tentaclism (midas remix).ava @@ -0,0 +1,31 @@ +name: "tentaclism (midas remix)" +mode: lines +blend: additive +persist: 0.86 +linewidth: 1.8 + +[init] +n = 560 +segs = 8 + +[frame] +rot = rot + dt * (0.08 + mid_att * 0.5) +hue = hue + dt * (0.07 + treb * 0.08) + +[beat] +flash = 1 + +[point] +seg = floor(i * segs) +si = frac(i * segs) +a0 = si * 6.28318 / segs + rot +amp = 0.12 + sv * 0.55 + bass * 0.25 + flash * 0.05 +rad = 0.15 + amp * (0.4 + 0.6 * sin(a0 * 6 + time * 2)) +a = seg * 6.28318 / segs + a0 * if(below(seg % 2, 1), 1, -1) +x = sin(a) * rad / aspect +y = cos(a) * rad +lum = 0.35 + amp * 1.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + seg + 4.19)) +skip = if(above(si, 0.96), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).ava b/tools/converted/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).ava new file mode 100644 index 0000000..8ac0c35 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/07 dynamic duo - tentaclism (raz remix - ugly edit).ava @@ -0,0 +1,25 @@ +name: "tentaclism (raz remix - ugly edit)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).ava b/tools/converted/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).ava new file mode 100644 index 0000000..a6a2f6e --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/08 dynamic duo - tentaclism (the y.m.o. rework #1).ava @@ -0,0 +1,29 @@ +name: "tentaclism (the y.m.o. rework #1)" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).ava b/tools/converted/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).ava new file mode 100644 index 0000000..8f2db80 --- /dev/null +++ b/tools/converted/root/embedded/Dynamics/09 dynamic duo - tentaclism (kans of kansis mix by drew - edit).ava @@ -0,0 +1,29 @@ +name: "tentaclism (kans of kansis mix by drew - edit)" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).ava b/tools/converted/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).ava new file mode 100644 index 0000000..caceba4 --- /dev/null +++ b/tools/converted/root/embedded/Envision/01 amphirion - the pacifist (duo's underworld mix).ava @@ -0,0 +1,31 @@ +# Auto-ported from "01 amphirion - the pacifist (duo's underworld mix).avs" (Winamp AVS / amphirion) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "the pacifist (duo's underworld mix)" +author: "amphirion" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=2 + +[frame] +xt=1-xtd; +yt=1-ytd; + +[beat] +xtd=rnd(20)*.1; +ytd=rnd(20)*.1; +rd=rnd(20)*.1; +gd=rnd(20)*.1; +bd=rnd(20)*.1; + +[point] +x=i*.3+xt*.5-getosc(0,0.4,0)*.25; +y=yt*.5-getosc(0,0.2,0)*.25; +size=10*(w/h)*(1+getosc(0,0.2,0)); +red=.2+rd; +green=.5+gd; +blue=.2+bd; diff --git a/tools/converted/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).ava b/tools/converted/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).ava new file mode 100644 index 0000000..99590c3 --- /dev/null +++ b/tools/converted/root/embedded/Envision/02 duo - biofeedback (hboy's organic mix).ava @@ -0,0 +1,31 @@ +# Auto-ported from "02 duo - biofeedback (hboy's organic mix).avs" (Winamp AVS / duo) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "biofeedback (hboy's organic mix)" +author: "duo" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=50 + +[frame] +bg=sin(bt); +fr=fr+bt-(fr*.055); +pf=pf+.025; + +[beat] +bt=getspec(0,0,0)*.55; + +[point] +c=i*(3.14159265358979)*2; +x=cos(c); +y=c*sin(fr*.3)*cos(pf)*.1; +size=20+fr+v*20; + +col=cos(i*19); +red=col; +green=col; +blue=col; diff --git a/tools/converted/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).ava b/tools/converted/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).ava new file mode 100644 index 0000000..842ac57 --- /dev/null +++ b/tools/converted/root/embedded/Envision/04 zamuz - roll-on mojo (duo remix).ava @@ -0,0 +1,29 @@ +name: "roll-on mojo (duo remix)" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).ava b/tools/converted/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).ava new file mode 100644 index 0000000..e80c06f --- /dev/null +++ b/tools/converted/root/embedded/Envision/05 duo - cafe de duo (vanish's wicked jungle massive remix).ava @@ -0,0 +1,25 @@ +name: "cafe de duo (vanish's wicked jungle massive remix)" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Envision/06 skupers - incantation (duo remix - edit).ava b/tools/converted/root/embedded/Envision/06 skupers - incantation (duo remix - edit).ava new file mode 100644 index 0000000..af866ae --- /dev/null +++ b/tools/converted/root/embedded/Envision/06 skupers - incantation (duo remix - edit).ava @@ -0,0 +1,40 @@ +# Auto-ported from "06 skupers - incantation (duo remix - edit).avs" (Winamp AVS / skupers) +# Texer II point program, 4 AVSTrans macros expanded. +name: "incantation (duo remix - edit)" +author: "skupers" +mode: dots +blend: additive +persist: 0.78 +linewidth: 3 +ysign: -1 + +[init] +n=h;pi=acos(-1);xasp=min(h/w,1);yasp=min(w/h,1); + +[frame] +xm1=xm1+xm1r;ym1=ym1+ym1r;xm=sin(xm1)*0.4;ym=sin(ym1)*0.4;redf=redf+redr;greenf=greenf+greenr;bluef=bluef+bluer;rx=rx+rxo;ry=ry+ryo;rz=rz+rzo;rzo=rzo*0.97;cx=cos(sin(rx));sx=sin(sin(rx));cy=cos(sin(ry));sy=sin(sin(ry));cz=cos(rz);sz=sin(rz); + +[beat] +xm1r=getosc(0.2,0,0)/5;ym1r=getosc(0.3,0,0)/8;redr=rnd(10)/100-0.05;greenr=rnd(10)/100-0.05;bluer=rnd(10)/100-0.05;rxo=getosc(0.5,0.02,0)/15;ryo=getosc(0.3,0.02,0)/15;rzo=-abs(getosc(0.7,0.02,0)/4)-0.05; + +[point] +k=getspec(i*80*n%n/n,1/n,0); +r=i*pi*2;d=getspec(.3,0,1)+.25+k; +x1=cos(r*4)*d; +y1=sin(r)*d*cos(i*99); +z1=cos(r)*d*sin(i*99); + +x1d=x1*cz-y1*sz; +y1d=x1*sz+y1*cz; +y2d=y1d*cx-z1*sx; +z2d=y1d*sx+z1*cx; +x3d=x1d*cy-z2d*sy; +z3d=x1d*sy+z2d*cy; +zi=1/(z3d+3); +x=if(above(z3d+3,0.1),x3d*zi,x)*xasp-xm; +y=if(above(z3d+3,0.1),y2d*zi,y)*yasp-ym; + +alpha=if(below(z3d+2.4,0),0,1); +red=alpha*(1+k)/(3+sin(redf))*(1-z3d); +blue=alpha*k/(4+sin(bluef))*(1-z3d); +green=alpha*k/(3+sin(greenf))*(1-z3d); diff --git a/tools/converted/root/embedded/Envision/07 duo - miraculous flow (nemo remix).ava b/tools/converted/root/embedded/Envision/07 duo - miraculous flow (nemo remix).ava new file mode 100644 index 0000000..bd5cf33 --- /dev/null +++ b/tools/converted/root/embedded/Envision/07 duo - miraculous flow (nemo remix).ava @@ -0,0 +1,25 @@ +name: "miraculous flow (nemo remix)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/tools/converted/root/embedded/Envision/08 duo - warm freeform (mykal remix).ava b/tools/converted/root/embedded/Envision/08 duo - warm freeform (mykal remix).ava new file mode 100644 index 0000000..a0c141b --- /dev/null +++ b/tools/converted/root/embedded/Envision/08 duo - warm freeform (mykal remix).ava @@ -0,0 +1,25 @@ +name: "warm freeform (mykal remix)" +mode: dots +blend: additive +persist: 0.82 +linewidth: 2.5 + +[init] +n = 850 + +[frame] +travel = travel + dt * (0.2 + vol_att * 1.6) +hue = hue + dt * 0.05 + +[point] +a = rand(i * 7.3) * 6.28318 +z = frac(rand(i * 3.1 + 5) + travel * (0.5 + rand(i + 9) * 0.7)) +rad = z * z * 1.1 +x = sin(a) * rad / aspect +y = cos(a) * rad +tw = 0.4 + 0.6 * sin(z * 40 + time * 3) * (0.5 + treb) +red = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3)) +green = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 2.09)) +blue = tw * (0.5 + 0.5 * sin(hue * 6.28318 + i * 3 + 4.19)) +size = 1 + z * 3 * (0.5 + bass) +skip = if(above(z, 0.97), 1, 0) diff --git a/tools/converted/root/embedded/Envision/09 zxe - pleasure point (duo remix).ava b/tools/converted/root/embedded/Envision/09 zxe - pleasure point (duo remix).ava new file mode 100644 index 0000000..65316ff --- /dev/null +++ b/tools/converted/root/embedded/Envision/09 zxe - pleasure point (duo remix).ava @@ -0,0 +1,29 @@ +name: "pleasure point (duo remix)" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).ava b/tools/converted/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).ava new file mode 100644 index 0000000..6e31a7b --- /dev/null +++ b/tools/converted/root/embedded/Envision/10 zamuz - heart dynamics (duo's high dose mix).ava @@ -0,0 +1,29 @@ +name: "heart dynamics (duo's high dose mix)" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).ava b/tools/converted/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).ava new file mode 100644 index 0000000..0a31d95 --- /dev/null +++ b/tools/converted/root/embedded/Envision/11 nemo - we build here (duo's building blocks remix).ava @@ -0,0 +1,29 @@ +name: "we build here (duo's building blocks remix)" +mode: dots +blend: additive +persist: 0.78 +linewidth: 2.5 + +[init] +cols = 36 +n = cols * cols + +[frame] +rot = rot + dt * (0.05 + mid * 0.4) +hue = hue + dt * (0.04 + treb * 0.1) + +[point] +cx = floor(i * cols) / (cols - 1) +cy = frac(i * cols) +h = sv * 0.35 +ux = (cx - 0.5) * 1.6 +uy = (cy - 0.5) * 1.6 +rx = ux * cos(rot) - uy * sin(rot) +ry = ux * sin(rot) + uy * cos(rot) +x = (rx / aspect) * (1 - h * 0.7) +y = ry * (1 - h * 0.7) +lum = 0.25 + h * 2.2 +red = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3)) +green = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 2.09)) +blue = lum * (0.6 + 0.4 * sin(hue * 6.28318 + cx * 3 + 4.19)) +size = 1.2 + h * 5 diff --git a/tools/converted/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).ava b/tools/converted/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).ava new file mode 100644 index 0000000..45292ed --- /dev/null +++ b/tools/converted/root/embedded/Envision/12 duo & vanish - wickedjunglesmassive (yathosho's cartographic re-rub).ava @@ -0,0 +1,29 @@ +name: "wickedjunglesmassive (yathosho's cartographic re-rub)" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 + +[init] +n = 720 + +[frame] +hue = hue + dt * (0.06 + mid * 0.2) +travel = travel + dt * (0.3 + bass_att * 1.4) + +[beat] +flash = 1 + +[point] +g = floor(i * 9) +gi = frac(i * 9) +ph = frac(rand(g * 5.1 + 1) + travel * (0.6 + rand(g + 17) * 0.8)) +a = gi * 6.28318 +rad = ph * (0.25 + bass * 0.6 + flash * 0.08) +x = sin(a) * rad / aspect +y = cos(a) * rad +fade = sin(ph * 3.14159) +red = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7)) +green = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 2.09)) +blue = fade * (0.3 + 0.7 * sin(hue * 6.28318 + g * 0.7 + 4.19)) +skip = if(above(ph, 0.95), 1, 0) diff --git a/tools/converted/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).ava b/tools/converted/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).ava new file mode 100644 index 0000000..4ea5fe0 --- /dev/null +++ b/tools/converted/root/embedded/Envision/13 skupers - why i love you (duo's pulsedriver remix).ava @@ -0,0 +1,25 @@ +# Auto-ported from "13 skupers - why i love you (duo's pulsedriver remix).avs" (Winamp AVS / skupers) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "why i love you (duo's pulsedriver remix)" +author: "skupers" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +n=10;pi=acos(-1); + +[frame] +t=t-t1;t1=t1*0.9+getspec(0.05,0,0)*0.1 + +[beat] +flash = 1 + +[point] +y=i*2.1-1.05;x=0; +col=i; +red=col; +green=col*.6; +blue=0; diff --git a/tools/converted/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).ava b/tools/converted/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).ava new file mode 100644 index 0000000..40d0a3c --- /dev/null +++ b/tools/converted/root/embedded/Envision/14 duo - conjuring sands (skupers remix - edit).ava @@ -0,0 +1,33 @@ +# Auto-ported from "14 duo - conjuring sands (skupers remix - edit).avs" (Winamp AVS / duo) +# Texer II point program, 4 AVSTrans macros expanded. +name: "conjuring sands (skupers remix - edit)" +author: "duo" +mode: dots +blend: additive +persist: 0.78 +linewidth: 3 +ysign: -1 + +[init] +n=15; + +[frame] +pfx=pfx+pbx; pfy=pfy+pby; pfz=pfz+pbz; xs=sin(pfx); xc=cos(pfx); ys=sin(pfy); yc=cos(pfy); zs=sin(pfz); zc=cos(pfz); + +[beat] +pbx=rnd(10)/70; pby=rnd(10)/70; pbz=rnd(10)/70; + +[point] +c=i*(3.14159265358979)*15; +x1=cos(c); +y1=i*2-1; +z1=sin(c); +x2=x1*xs-y1*xc; +y2=y1*xs+x1*xc; +z2=z1*ys-y2*yc; +x3=y2*ys+z1*yc; +y3=x2*zs-z2*zc; +z3=z2*zs+x2*zc+3.5; +x=x3/z3; +y=y3/z3; +red=v*3; green=v*3; blue=v*3; diff --git a/tools/converted/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).ava b/tools/converted/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).ava new file mode 100644 index 0000000..f61a6ce --- /dev/null +++ b/tools/converted/root/embedded/Envision/15 tuggummi - multiple overdose (duo remix).ava @@ -0,0 +1,28 @@ +# Auto-ported from "15 tuggummi - multiple overdose (duo remix).avs" (Winamp AVS / tuggummi) +# SuperScope point program, 4 AVSTrans macros expanded. +name: "multiple overdose (duo remix)" +author: "tuggummi" +mode: lines +blend: additive +persist: 0.88 +linewidth: 2 +ysign: -1 + +[init] +pi=acos(-1) ; t=acos(-1) + +[frame] +n=w*0.55+2 ; t=t+0.25 + +[beat] +flash = 1 + +[point] +s=bnot(s) ; +x=i*2-1 ; +y=s*0.25-0.225-v*.2 ; +c=sin(i*pi*2+t)*0.5+0.5 ; +red=c ; +blue=c ; +green=c; +size=3; diff --git a/tools/converted/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).ava b/tools/converted/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).ava new file mode 100644 index 0000000..39952f2 --- /dev/null +++ b/tools/converted/root/embedded/Envision/16 duo - warm freeform (ya-tho-sho treatment).ava @@ -0,0 +1,25 @@ +name: "warm freeform (ya-tho-sho treatment)" +mode: lines +blend: additive +persist: 0.9 +linewidth: 1.6 + +[init] +n = 1100 + +[frame] +t = t + dt * (0.3 + bass_att * 1.8) +hue = hue + dt * 0.08 + beat * 0.01 + +[point] +arm = floor(i * 5) +fa = frac(i * 5) +d = fa +ang = arm * 1.2566 + d * (1.4 + mid * 3) * 3.14159 + t +rad = (0.05 + d * d * 1.15) * (0.7 + bass * 0.5) +x = sin(ang) * rad / aspect +y = cos(ang) * rad +red = (0.3 + 0.7 * d) * (0.3 + bass) +green = (0.3 + 0.7 * d) * (0.25 + mid) +blue = (0.3 + 0.7 * d) * (0.2 + treb + hue * 0.2) +skip = if(below(fa, 0.04), 1, 0) diff --git a/worklog.md b/worklog.md index 577033f..504f75e 100755 --- a/worklog.md +++ b/worklog.md @@ -456,3 +456,275 @@ Stage Summary: (honest 422; works from residential IPs or with server-side cookies). SoundCloud/Bandcamp/ archive.org/direct files all resolve; SoundCloud serves HLS-only for some tracks → clean adaptive-stream error. + +--- +Task ID: 9 +Agent: main (Z.ai Code) +Task: Preset library folder-tree data layer (backend + store) for the requested +"folder/file tree" Presets page redesign + +Work Log: +- Prisma schema: Preset += folderPath (default "root/user created") + originalFile, + @@unique([folderPath, name]); new Folder model (path @unique). db:push OK, + client regenerated, dev server restarted once to pick up the new client. +- src/lib/server/preset-tree.ts — shared helpers: normalizeFolderPath (root-prefixed, + max depth 8, 80-char segments, control chars rejected), isValidName, parentOf, + subtreeFilter (presets) + subtreeFolderFilter (folders), ensureFolderRows + (creates missing ancestors), PROTECTED_FOLDERS = root | root/embedded | root/user created. +- GET /api/presets — one-time seed: folder backbone (root, root/embedded, + root/user created, root/embedded/{Synth-C,Amp Picks,Dynamics,Envision}) + + 95 presets (5 hand-ported classics → Synth-C; 90 auto-converted Google-Drive + presets → Amp Picks 66 / Dynamics 9 / Envision 15, doubling prefix normalized). + Legacy pre-folder built-ins migrated automatically. Response now + { presets, folders } with folderPath + originalFile on each preset. +- POST /api/presets — create in folder (409 on name clash within folder). +- PUT /api/presets/[id] — update content/author/description or RENAME (unique + per folder enforced); built-in restriction REMOVED (tree is fully manageable). +- DELETE /api/presets/[id] — any preset deletable. +- /api/folders — POST create subfolder (parent must exist), PATCH rename folder + + all descendants (transaction, prefix rewrite), DELETE ?path= removes subtree + (folders + presets). Protected folders reject rename/delete with 400. +- /api/presets/export?path=… — jszip download preserving the directory structure + from root ("root/user created/Club Set/Test Ring.ava" style entries + dir entries). +- /api/presets/import (multipart file + targetPath) — imports .ava/.txt entries; + "root/…"-prefixed paths land at their exact path, others under the target + folder; creates missing folder rows; skips name-clash duplicates; reports + { imported, skipped, skippedOther, foldersCreated }. +- src/lib/store.ts — PresetItem += folderPath/originalFile; EditTarget; new state: + folders, selectedFolder, selectedPresetId, expandedFolders (depth<=2 default + open), editTarget, activePresetId, randomize/randomIntervalSec/randomScope; + actions selectFolder (also sets randomize scope live), selectPreset, setEditTarget, + setActivePresetId, setRandomize, setRandomIntervalSec (3–60 s validation); + module-level club-mode timer (syncRandomTimer/stopRandomTimer, loadRandomPreset + applies to the ENGINE ONLY — never clobbers unsaved editor text; up to 4 random + retries for compilable scripts; toast per switch); settings persisted to + localStorage "ava.randomize" and restored via restoreRandomSettings(folders); + refreshPresets now fetches presets+folders and prunes stale selections. +- src/app/page.tsx — initial load sets folders, startup fallback prefers the + first root/embedded/Synth-C preset, sets activePresetId, restores randomize. +- VERIFIED via curl: seed 95/7 folders correct; create folder/preset; 409 dup; + rename folder moves preset paths; export zip listing shows full paths; delete + subtree; import restores the exact tree; protected-folder rename/delete → 400; + export root → 200. tsc clean on all touched files. + +Stage Summary: +- API contract for the new UI: + GET /api/presets → { presets: PresetItem[], folders: string[] } + POST /api/presets { name, content, author?, description?, folderPath? } → { preset } | 409 | 400 + PUT /api/presets/[id] { name?, content?, author?, description? } → { preset } + DELETE /api/presets/[id] → { deleted } + POST /api/folders { path } → { folder } | 409 | 400 + PATCH /api/folders { path, newName } → { renamed, to, folders, presets } + DELETE /api/folders?path=… → { deleted, folders, presets } (protected → 400) + GET /api/presets/export?path=… → zip download (structure preserved from root) + POST /api/presets/import (file, targetPath) → { imported, skipped, skippedOther, foldersCreated } +- Store API for the UI (src/lib/store.ts): useAvaStore fields folders, + selectedFolder, selectedPresetId, expandedFolders, editTarget, activePresetId, + randomize, randomIntervalSec, randomScope + actions listed above; helpers + refreshPresets(), stopRandomTimer(), syncRandomTimer(), restoreRandomSettings(). +- Random presets are applied engine-only (editor untouched) by design. + +--- +Task ID: 10 +Agent: full-stack-developer (Presets UI) +Task: Presets tab redesign — folder/file tree manager + Script tab Save-to-target / Save As + +Work Log: +- Read worklog (Task 9 contract), store.ts, all 5 API routes, old presets-tab, + script-tab, shared.ts, control-panel.tsx, page.tsx, audio-source.ts, + presets.ts, detect/beat tabs for style matching. +- src/components/ava/presets-tab.tsx — COMPLETE REWRITE (~1000 lines): + - buildTree(folders, presets): synthesizes missing Folder rows + ancestors + from preset folderPaths (defensive); segment-wise alphabetical sort + (parents before children), presets sorted within folders; subtree preset + counts per node. + - TreeFolder/TreePreset recursive rows: chevron-only expand/collapse + (store.toggleFolder; default open = path depth <= 2 segments, same rule + as the store), row click selects (selectFolder/selectPreset), preset + double-click loads, running preset = emerald Play icon + emerald-300 text + + bg-emerald-400/10, selected rows = emerald left border + bg-zinc-800/70, + subtree count badge (font-mono, tooltip "presets in this folder and + below"), originalFile rows get a hover-visible (always on mobile) .avs + download anchor, empty folders show "(empty)" italic hint. role=tree/ + treeitem/group, aria-expanded/selected/level, 36px (size-9/h-9) targets. + Tree scrolls in max-h-[420px] lg:max-h-[480px] with thinScrollbar. + - Two-column layout: tree 58% / actions 42% on sm+, stacked on mobile. + - Context-sensitive actions panel (header shows selection + counts): + preset -> LOAD PRESET (emerald; applyScriptText -> toast -> setActivePresetId + + setEditTarget, compile error -> destructive toast + script tab), RENAME, + DELETE; folder -> CREATE PRESET (POST starter template -> refresh -> + editTarget + editor + engine + select + tab switch + toast), NEW SUBFOLDER + (POST -> expand parent + select new), RENAME (PATCH folder / PUT preset, + selection + editTarget follow the rename), DELETE (AlertDialog with + client-side blast radius "removes N presets in M folders", protected + folders root | root/embedded | root/user created have Rename+Delete + disabled with "Protected folder" tooltip), EXPORT .ZIP (asChild anchor -> + /api/presets/export?path=..., download attr), IMPORT .ZIP (hidden .zip + input -> FormData POST -> summary toast with duplicates/foldersCreated). + All server errors (409/400/404/500) surface as destructive toasts with + the server message; empty names get inline dialog validation. + - Randomize section (border-t): Checkbox + 3-60s delay input; invalid input + shows red hint and reverts to the committed store value on blur (valid + values commit live via setRandomIntervalSec); scope line reflects + randomScope (folder name or "all presets"), amber "no presets in this + folder yet" hint when scope empty; pulsing emerald "ON · every Ns" badge + while running. Timer itself runs in the store — UI only wires controls. + - NameDialog made fully controlled (parent owns value/error) to satisfy + react-hooks/set-state-in-effect lint rule. +- src/components/ava/script-tab.tsx — EDITS: + - "Saving to: root/…/Name" chip (emerald, middle-truncated path, full path + in tooltip) with X button clearing the edit target; shown only while + editTarget is set. + - Save button (emerald, only with editTarget): PUT /api/presets/[id] + {content, author from validateScript meta} -> refreshPresets + toast + 'Saved to "Name"'; disabled while saving. + - Save As… button (only with editTarget): Dialog prefilled with the current + name -> POST /api/presets same folder -> setEditTarget(new) + refresh + + toast; 409 handled; empty name inline error. + - New button now also clears the edit target. +- VERIFIED in headless browser (agent-browser): tree render/expansion/ + selection, Load (toast + tab wiring), Save (PUT 200), Save As (copy + created server-side + chip retargeted), delete preset + folder dialogs + (blast-radius + empty-folder wording), create preset (full flow incl. + editor + tab switch), new subfolder, rename folder (selection follows, + editTarget path rewritten), import .zip end-to-end (DataTransfer dispatch + into the hidden input -> 96 presets/8 folders -> toast -> cleanup), + randomize persistence + scope line + invalid-delay revert + ON badge. + Library restored to pristine 95 presets / 7 folders after testing. +- bun run lint: clean. tsc: clean on both touched files (pre-existing errors + elsewhere in examples/, skills/, audio.ts, script.ts untouched). + dev.log: no runtime errors. + +Stage Summary: +- Files modified: src/components/ava/presets-tab.tsx (full rewrite), + src/components/ava/script-tab.tsx (chip + Save/Save As/New edits). + No changes to page.tsx, store.ts, API routes, or other tabs. +- Key decisions: subtree (not direct) preset counts on folder badges + (labeled via tooltip); NameDialog fully controlled to pass the new + react-hooks lint rules; rename keeps editTarget/randomScope/selection + pointing at the renamed paths client-side (refreshPresets prunes stale + ones server-side); delay input commits valid values live but only reverts + invalid text on blur so typing isn't fighted; export/import allowed on + protected folders (rename/delete disabled) exactly per spec. +- Store/API notes for Task 11: none needed — the Task 9 contract covered + everything. One cosmetic gap: setRandomIntervalSec is typed (sec: number) + though the store handles numeric strings at runtime; UI converts with + Number() before calling. +- Left for Task 11 verification: visual polish pass in a real browser + (spacing at 420 vs 440px panel widths, mobile stacking), touch-device + check of the hover-only .avs download button, and double-click-to-run + ergonomics (agent-browser cannot dblclick, so that path was verified via + the identical Load Preset code path). + +--- +Task ID: 11 +Agent: main (Z.ai Code) +Task: E2E verification of the new folder-tree Presets page + final cleanup + +Work Log: +- bun run lint: zero errors. tsc clean on all files touched by Tasks 9–10. + dev.log: no runtime errors, no hydration warnings. +- Agent-browser E2E golden path, ALL VERIFIED: + * Tree renders exactly as requested: root/ → embedded/ (Synth-C 5, Amp Picks 66, + Dynamics 9, Envision 15) + user created/ 0; subtree count badges; depth≤2 + folders default-expanded; chevron-only expand/collapse; role=tree/treeitem. + * Preset select → action panel switches to preset context (Load/Rename/Delete); + folder select → full management set; protected folders (root/embedded/user + created) have Rename+Delete disabled. + * Load Preset: applied, toast, emerald Play-icon running indicator in tree, + footer SCRIPT updated, engine running. + * Create Preset: dialog → created in selected folder → auto-switch to Script + tab with starter template → "Saving to: root/user created/…/Name" chip → + edit → Save (PUT) persisted server-side (verified content round-trip) → + Save As… created "Club Ring v2" in the same folder and retargeted the chip. + * New Subfolder → "My Fav Presets" created + selected + toast. + * Rename folder "My Fav Presets" → "Club Night": both presets' paths updated. + * Delete folder: AlertDialog with blast radius ("removes 2 presets"), confirmed + → folder + presets gone, library back to 95. + * Export .zip: link href /api/presets/export?path=… correct; zip contains + "root/user created/Round Trip/RT Preset.ava" (structure preserved from root). + * Import .zip via the UI file input (DataTransfer dispatch): toast + "Imported 1 presets — 1 folders created", tree restored at the exact path. + * Randomize: invalid delay 2 and 61 both rejected with the "3 and 60 seconds" + error + revert; 4 accepted; checkbox on → "ON · every 4s" badge; with Demo + audio running, the timer switched to a random preset from the selected + scope after ~4s (toast + footer PTS 333 matching the loaded script); + scope line follows folder selection; settings persist to localStorage and + do NOT auto-restart after reload unless enabled. + * Mobile 390×844: no horizontal scroll, tree/actions stack, footer pushed + naturally and sticks to viewport bottom when scrolled to the end. + * Fresh reload: startup falls back to the first root/embedded/Synth-C preset + (Gap The Mind), tree repopulates, zero console errors. +- Cleanup: deleted the Round Trip / Club Ring test data (library back to the + pristine 95 presets / 7 folders); test artifacts in tool-results/. + +Stage Summary: +- The requested Presets page redesign is COMPLETE and browser-verified: + left folder/file tree (root → embedded {Synth-C, Amp Picks, Dynamics, + Envision} + user created) with counts, expansion, selection, running + indicator and .avs original downloads; right management panel (Load Preset, + Create Preset → Script tab → Save/Save As, New Subfolder, Rename, Delete with + confirmation + blast radius, Export .zip preserving the tree, Import .zip + restoring the tree) and Randomize (3–60 s validated) scoped to the selected + folder. All flows verified end-to-end with zero console errors. + +--- +Task ID: 12 +Agent: main (Z.ai Code) +Task: LOST-WORK RECOVERY — restore the folder-tree Presets page (Tasks 9–11) +from the user's Google Drive project download after the sandbox was rebuilt +from an earlier snapshot + +Context: +- The sandbox running Tasks 9–11 was replaced by a rebuilt environment whose + git history ended at the "link source" commit (51e07079 → local 4caaa3e). + The two commits containing the Google-Drive preset import (b33474e) and the + folder-tree implementation (2a23db4) existed only in the user's downloaded + copy of the project — which the user kindly re-hosted on Google Drive. + +Work Log: +- Downloaded the user's archive (drive file 1yUmimOpLyz7l8BQO6keUHPCvLaV6TW5k, + 86 MB tar.gz, includes .git with the two lost commits) to /tmp/ava-restore. +- Verified the archive's base commit (fb12d97, subject 51e07079) is + content-identical to the workspace HEAD 4caaa3e (git diff empty except + .zscripts/tool-results) → archive HEAD is a strict superset, safe to restore. +- Stopped the dev server, then surgically checked out all 200 files changed + between fb12d97 → 2a23db4 (excluding .zscripts/dev.pid, tool-results, + dev.log): prisma/schema.prisma (Folder model + Preset.folderPath/originalFile), + db/custom.db (pre-seeded 95 presets / 7 folders), src/lib/server/preset-tree.ts, + src/lib/ava/embedded-presets.ts, src/lib/ava/{engine,expression,script}.ts + (functions needed by the converted presets), src/app/api/{folders,presets/** + export,import}/route.ts, presets-tab.tsx (full tree rewrite), script-tab.tsx + (Save/Save As chip), store.ts (tree + randomize state), page.tsx, plus the + 95 .avs originals under public/avs-originals/… and tools/convert-avs.ts. +- Ran `bun run db:generate` to rebuild the Prisma client for the new schema + (the restored db already matched it — no db:push needed, no data loss). +- Restarted the dev server (port 3000, single instance). +- VERIFIED via API: GET /api/presets → 95 presets / 7 folders + (Synth-C 5, Amp Picks 66, Dynamics 9, Envision 15); export + root/embedded/Synth-C → zip with full "root/embedded/Synth-C/*.ava" paths; + import of the same zip → 0 imported / 5 skipped (duplicate paths) — round + trip intact. +- VERIFIED in agent-browser (desktop 1440×900 + mobile 390×844): tree renders + (root 95 → embedded {Amp Picks 66, Dynamics 9, Envision 15, Synth-C 5} + + user created), preset select → Load Preset → engine running (footer + "SRC: DEMO, FPS: 60, PTS: 480, BPM: 126.8, SCRIPT: Gap The Mind, engine ok"), + New Subfolder ("My Fav Presets" created + selected), Rename (→ "Club Night", + toast shows new path), Delete (alertdialog + server 200, tree cleaned), + Randomize delay validation ("2" → red "Delay must be between 3 and 60 + seconds." hint; "4" commits), .avs original HEAD → 200, reload → startup + fallback loads Gap The Mind, zero console/page errors, mobile: no horizontal + scroll, footer pushed naturally and flush with the viewport bottom when + scrolled to end. bun run lint: clean. +- Test data cleaned up (Club Night folder deleted; library back to 95/7). +- Committed the restoration to git and left the recovery remote/objects + out of the repo (temp files remain in /tmp/ava-restore only). + +Stage Summary: +- The folder-tree Presets page (Tasks 9–11) is fully restored and re-verified + end-to-end. Root cause of the "revert": the environment was rebuilt from a + snapshot predating the morning's commits; the work itself was never lost in + the user's download. All state (schema, data, API, UI, presets, tooling) + now matches the archive's HEAD (2a23db4) on top of the current workspace. +- NOTE for the future: commit snapshots are the source of truth when an + environment rebuild is suspected — compare git log before re-implementing.