From 5054756ca21a44749fe37829abd18786eddbbdfd Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 28 Feb 2025 17:20:28 +0000 Subject: [PATCH 01/10] experiment --- website/src/pages/cycler.astro | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 website/src/pages/cycler.astro diff --git a/website/src/pages/cycler.astro b/website/src/pages/cycler.astro new file mode 100644 index 000000000..bd689d0a4 --- /dev/null +++ b/website/src/pages/cycler.astro @@ -0,0 +1,13 @@ +--- +import HeadCommon from '../components/HeadCommon.astro'; +--- + + + + + Strudel Cycler + + + Hello + + From c16511dfbdf6d9f6501c5be10b6febc48dd23234 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 1 Mar 2025 10:55:37 +0000 Subject: [PATCH 02/10] towards spinning a disk matching cycles inside strudel --- website/public/cycler/main.js | 50 ++++++++++++++ website/public/cycler/style.css | 107 +++++++++++++++++++++++++++++ website/src/components/Vinyl.astro | 48 +++++++++++++ website/src/pages/cycler.astro | 4 +- 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 website/public/cycler/main.js create mode 100644 website/public/cycler/style.css create mode 100644 website/src/components/Vinyl.astro diff --git a/website/public/cycler/main.js b/website/public/cycler/main.js new file mode 100644 index 000000000..cad93e840 --- /dev/null +++ b/website/public/cycler/main.js @@ -0,0 +1,50 @@ +import './style.css' +import { NeoCyclist } from '../../strudel/packages/core/neocyclist.mjs'; +import { saw } from '@strudel/core'; +// import { getTrigger } from '@strudel/core/repl.mjs'; +import { setInterval, clearInterval } from 'worker-timers'; +import { getAudioContext } from '@strudel/webaudio'; + + +document.querySelector('#app').innerHTML = ` +
+

Cycler

+
+ 💿 +
+
+` + +const record = document.getElementById('record'); + +const schedulerOptions = { + onTrigger: x => { }, + getTime: () => getAudioContext().currentTime, + onToggle: (started) => console.log("started: ", started), + setInterval, + clearInterval, + // beforeStart, +}; +const cyclist = new NeoCyclist(schedulerOptions); + +record.onclick = () => { + + if (!cyclist.started) { + cyclist.start(); + } + console.log('aha', cyclist.getTime(), cyclist.cycle) + cyclist.setPattern(saw.segment(16)); + +} + +const timer = setInterval(function () { + if (cyclist && cyclist.started) { + // console.log(cyclist.cycle); + const deg = (cyclist.cycle % 1) * 360; + record.style.webkitTransform = 'rotate(' + deg + 'deg)'; + record.style.mozTransform = 'rotate(' + deg + 'deg)'; + record.style.msTransform = 'rotate(' + deg + 'deg)'; + record.style.oTransform = 'rotate(' + deg + 'deg)'; + record.style.transform = 'rotate(' + deg + 'deg)'; + } +}, 50); diff --git a/website/public/cycler/style.css b/website/public/cycler/style.css new file mode 100644 index 000000000..9d1823092 --- /dev/null +++ b/website/public/cycler/style.css @@ -0,0 +1,107 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + +#record { + font-size: 32em; +} \ No newline at end of file diff --git a/website/src/components/Vinyl.astro b/website/src/components/Vinyl.astro new file mode 100644 index 000000000..beb9188ad --- /dev/null +++ b/website/src/components/Vinyl.astro @@ -0,0 +1,48 @@ +--- +/* +Vinyl.astro - +Copyright (C) 2025 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { setInterval, clearInterval } from 'worker-timers'; +import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; +import { getAudioContext } from '@strudel/webaudio'; + +const schedulerOptions = { + onTrigger: (x) => {}, + getTime: () => getAudioContext().currentTime, + onToggle: (started) => console.log('started: ', started), + setInterval, + clearInterval, + // beforeStart, +}; +const cyclist = new NeoCyclist(schedulerOptions); + +const record = document.getElementById('record'); + +if (record) { + record.onclick = () => { + if (!cyclist.started) { + cyclist.start(); + } + console.log('aha', cyclist.getTime(), cyclist.cycle); + cyclist.setPattern(saw.segment(16)); + }; +} +--- + + +
💿
diff --git a/website/src/pages/cycler.astro b/website/src/pages/cycler.astro index bd689d0a4..ad4b734c3 100644 --- a/website/src/pages/cycler.astro +++ b/website/src/pages/cycler.astro @@ -1,5 +1,7 @@ --- import HeadCommon from '../components/HeadCommon.astro'; +import Vinyl from '../components/Vinyl.astro'; +// import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; --- @@ -8,6 +10,6 @@ import HeadCommon from '../components/HeadCommon.astro'; Strudel Cycler - Hello + From 02bb88bde4a7aad5dba878573d3c8d34fe80d157 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 1 Mar 2025 10:57:39 +0000 Subject: [PATCH 03/10] rm --- website/public/cycler/main.js | 50 --------------- website/public/cycler/style.css | 107 -------------------------------- 2 files changed, 157 deletions(-) delete mode 100644 website/public/cycler/main.js delete mode 100644 website/public/cycler/style.css diff --git a/website/public/cycler/main.js b/website/public/cycler/main.js deleted file mode 100644 index cad93e840..000000000 --- a/website/public/cycler/main.js +++ /dev/null @@ -1,50 +0,0 @@ -import './style.css' -import { NeoCyclist } from '../../strudel/packages/core/neocyclist.mjs'; -import { saw } from '@strudel/core'; -// import { getTrigger } from '@strudel/core/repl.mjs'; -import { setInterval, clearInterval } from 'worker-timers'; -import { getAudioContext } from '@strudel/webaudio'; - - -document.querySelector('#app').innerHTML = ` -
-

Cycler

-
- 💿 -
-
-` - -const record = document.getElementById('record'); - -const schedulerOptions = { - onTrigger: x => { }, - getTime: () => getAudioContext().currentTime, - onToggle: (started) => console.log("started: ", started), - setInterval, - clearInterval, - // beforeStart, -}; -const cyclist = new NeoCyclist(schedulerOptions); - -record.onclick = () => { - - if (!cyclist.started) { - cyclist.start(); - } - console.log('aha', cyclist.getTime(), cyclist.cycle) - cyclist.setPattern(saw.segment(16)); - -} - -const timer = setInterval(function () { - if (cyclist && cyclist.started) { - // console.log(cyclist.cycle); - const deg = (cyclist.cycle % 1) * 360; - record.style.webkitTransform = 'rotate(' + deg + 'deg)'; - record.style.mozTransform = 'rotate(' + deg + 'deg)'; - record.style.msTransform = 'rotate(' + deg + 'deg)'; - record.style.oTransform = 'rotate(' + deg + 'deg)'; - record.style.transform = 'rotate(' + deg + 'deg)'; - } -}, 50); diff --git a/website/public/cycler/style.css b/website/public/cycler/style.css deleted file mode 100644 index 9d1823092..000000000 --- a/website/public/cycler/style.css +++ /dev/null @@ -1,107 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} - -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} - -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #f7df1eaa); -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} - -button:hover { - border-color: #646cff; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} - -#record { - font-size: 32em; -} \ No newline at end of file From 9c6eca9a59ef99994b9c3624f7b4aeb4dff0a891 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 1 Mar 2025 15:55:10 +0000 Subject: [PATCH 04/10] it works --- .../src/components/{Vinyl.astro => Vinyl.jsx} | 55 ++++++++++--------- website/src/pages/cycler.astro | 8 ++- 2 files changed, 34 insertions(+), 29 deletions(-) rename website/src/components/{Vinyl.astro => Vinyl.jsx} (56%) diff --git a/website/src/components/Vinyl.astro b/website/src/components/Vinyl.jsx similarity index 56% rename from website/src/components/Vinyl.astro rename to website/src/components/Vinyl.jsx index beb9188ad..9b4c0a847 100644 --- a/website/src/components/Vinyl.astro +++ b/website/src/components/Vinyl.jsx @@ -1,6 +1,5 @@ ---- /* -Vinyl.astro - +Vinyl.jsx - Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -8,41 +7,45 @@ This program is free software: you can redistribute it and/or modify it under th import { setInterval, clearInterval } from 'worker-timers'; import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; import { getAudioContext } from '@strudel/webaudio'; +import { saw } from '@strudel/core'; const schedulerOptions = { - onTrigger: (x) => {}, + onTrigger: (x) => { }, getTime: () => getAudioContext().currentTime, onToggle: (started) => console.log('started: ', started), setInterval, clearInterval, // beforeStart, }; -const cyclist = new NeoCyclist(schedulerOptions); +// const cyclist = new NeoCyclist(schedulerOptions); -const record = document.getElementById('record'); -if (record) { - record.onclick = () => { - if (!cyclist.started) { - cyclist.start(); - } - console.log('aha', cyclist.getTime(), cyclist.cycle); - cyclist.setPattern(saw.segment(16)); - }; -} ---- +export function Vinyl() { + const record = document.getElementById('record'); + const cyclist = new NeoCyclist(schedulerOptions); + if (record) { + record.onclick = () => { + if (!cyclist.started) { + cyclist.start(); + } + console.log('aha', cyclist.getTime(), cyclist.cycle); + cyclist.setPattern(saw.segment(16)); + }; + } - -
💿
+ + if (cyclist && cyclist.started) { + // console.log(cyclist.cycle); + const deg = (cyclist.cycle % 1) * 360; + record.style.webkitTransform = 'rotate(' + deg + 'deg)'; + record.style.mozTransform = 'rotate(' + deg + 'deg)'; + record.style.msTransform = 'rotate(' + deg + 'deg)'; + record.style.oTransform = 'rotate(' + deg + 'deg)'; + record.style.transform = 'rotate(' + deg + 'deg)'; + } + + }, 20); +} \ No newline at end of file diff --git a/website/src/pages/cycler.astro b/website/src/pages/cycler.astro index ad4b734c3..b4e025e98 100644 --- a/website/src/pages/cycler.astro +++ b/website/src/pages/cycler.astro @@ -1,7 +1,6 @@ --- import HeadCommon from '../components/HeadCommon.astro'; -import Vinyl from '../components/Vinyl.astro'; -// import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; +import { Vinyl } from '../components/Vinyl.jsx'; --- @@ -10,6 +9,9 @@ import Vinyl from '../components/Vinyl.astro'; Strudel Cycler - +
Hello there.
+
💿
+ + From 6b1e6cc7afe10faf942866fde565cfa099268b1f Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 1 Mar 2025 16:53:13 +0000 Subject: [PATCH 05/10] try to make more reactish --- website/src/components/Vinyl.jsx | 47 +++++++++++++++----------------- website/src/pages/cycler.astro | 2 -- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/website/src/components/Vinyl.jsx b/website/src/components/Vinyl.jsx index 9b4c0a847..c3a7d0ab6 100644 --- a/website/src/components/Vinyl.jsx +++ b/website/src/components/Vinyl.jsx @@ -3,7 +3,7 @@ Vinyl.jsx - Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ - +import React, { useState, useEffect } from "react"; import { setInterval, clearInterval } from 'worker-timers'; import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; import { getAudioContext } from '@strudel/webaudio'; @@ -17,35 +17,32 @@ const schedulerOptions = { clearInterval, // beforeStart, }; -// const cyclist = new NeoCyclist(schedulerOptions); - +const cyclist = new NeoCyclist(schedulerOptions); export function Vinyl() { - const record = document.getElementById('record'); - const cyclist = new NeoCyclist(schedulerOptions); - if (record) { - record.onclick = () => { - if (!cyclist.started) { - cyclist.start(); - } - console.log('aha', cyclist.getTime(), cyclist.cycle); + const [isActive, setIsActive] = useState(false); + const [cyclepos, setCyclepos] = useState(0); + const activate = () => setIsActive(true); + + if (isActive) { + if (!cyclist.started) { + cyclist.start(); cyclist.setPattern(saw.segment(16)); - }; + } } - let ticks = 0; - const timer = setInterval(function () { - const record = document.getElementById('record'); + useEffect(() => { + const intervalId = setInterval(() => { + setCyclepos(cyclist.cycle); + }, 20); + return () => clearInterval(intervalId); + }, []); - if (cyclist && cyclist.started) { - // console.log(cyclist.cycle); - const deg = (cyclist.cycle % 1) * 360; - record.style.webkitTransform = 'rotate(' + deg + 'deg)'; - record.style.mozTransform = 'rotate(' + deg + 'deg)'; - record.style.msTransform = 'rotate(' + deg + 'deg)'; - record.style.oTransform = 'rotate(' + deg + 'deg)'; - record.style.transform = 'rotate(' + deg + 'deg)'; - } + const deg = (cyclepos % 1) * 360; - }, 20); + const style = { + fontSize: "20em", + transform: 'rotate(' + deg + 'deg)' + }; + return
💿
} \ No newline at end of file diff --git a/website/src/pages/cycler.astro b/website/src/pages/cycler.astro index b4e025e98..06bfb15f1 100644 --- a/website/src/pages/cycler.astro +++ b/website/src/pages/cycler.astro @@ -10,8 +10,6 @@ import { Vinyl } from '../components/Vinyl.jsx';
Hello there.
-
💿
- From 5097ae8fc02546ec71865c67bfa8bb3c1d2a9f06 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 1 Mar 2025 18:07:25 +0000 Subject: [PATCH 06/10] use encoder disc --- website/public/encoder_disc.svg | 155 +++++++++++++++++++++++++++++++ website/src/components/Vinyl.jsx | 7 +- 2 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 website/public/encoder_disc.svg diff --git a/website/public/encoder_disc.svg b/website/public/encoder_disc.svg new file mode 100644 index 000000000..c4317947d --- /dev/null +++ b/website/public/encoder_disc.svg @@ -0,0 +1,155 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/src/components/Vinyl.jsx b/website/src/components/Vinyl.jsx index c3a7d0ab6..06ed16aa3 100644 --- a/website/src/components/Vinyl.jsx +++ b/website/src/components/Vinyl.jsx @@ -8,6 +8,9 @@ import { setInterval, clearInterval } from 'worker-timers'; import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; import { getAudioContext } from '@strudel/webaudio'; import { saw } from '@strudel/core'; +import encoder from '../../public/encoder_disc.svg'; + +console.log(encoder); const schedulerOptions = { onTrigger: (x) => { }, @@ -34,7 +37,7 @@ export function Vinyl() { useEffect(() => { const intervalId = setInterval(() => { setCyclepos(cyclist.cycle); - }, 20); + }, 10); return () => clearInterval(intervalId); }, []); @@ -44,5 +47,5 @@ export function Vinyl() { fontSize: "20em", transform: 'rotate(' + deg + 'deg)' }; - return
💿
+ return
} \ No newline at end of file From ff790c803d9273dffda6358c7e4a3e89ddecb794 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 1 Oct 2025 12:01:19 +0100 Subject: [PATCH 07/10] format --- website/src/components/Vinyl.jsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/website/src/components/Vinyl.jsx b/website/src/components/Vinyl.jsx index 06ed16aa3..fb2665cef 100644 --- a/website/src/components/Vinyl.jsx +++ b/website/src/components/Vinyl.jsx @@ -3,7 +3,7 @@ Vinyl.jsx - Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react'; import { setInterval, clearInterval } from 'worker-timers'; import { NeoCyclist } from '@strudel/core/neocyclist.mjs'; import { getAudioContext } from '@strudel/webaudio'; @@ -13,7 +13,7 @@ import encoder from '../../public/encoder_disc.svg'; console.log(encoder); const schedulerOptions = { - onTrigger: (x) => { }, + onTrigger: (x) => {}, getTime: () => getAudioContext().currentTime, onToggle: (started) => console.log('started: ', started), setInterval, @@ -44,8 +44,14 @@ export function Vinyl() { const deg = (cyclepos % 1) * 360; const style = { - fontSize: "20em", - transform: 'rotate(' + deg + 'deg)' + fontSize: '20em', + transform: 'rotate(' + deg + 'deg)', }; - return
-} \ No newline at end of file + return ( +
+
+ +
+
+ ); +} From 69a82ddc5b01120781086a68c88971c05ba932cf Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 12:18:32 +0100 Subject: [PATCH 08/10] low latency clock control experimentation --- packages/core/clockworker.js | 18 +++++++++++++++--- packages/core/cyclist.mjs | 2 +- packages/core/logger.mjs | 2 +- packages/core/neocyclist.mjs | 9 ++++++--- website/src/components/Vinyl.jsx | 7 +++++-- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 77a45362e..c616b89dc 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -12,6 +12,8 @@ function getTime() { let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; let num_seconds_at_cps_change = 0; +let max_cycle = null; + let cps = 0.5; // {id: {started: boolean}} const clients = new Map(); @@ -39,6 +41,7 @@ const sendTick = (phase, duration, tick, time) => { cps, time, cycle, + max_cycle, }); num_ticks_since_cps_change++; }; @@ -60,9 +63,10 @@ const stopClock = async (id) => { const otherClientStarted = Array.from(clients.values()).some((c) => c.started); //dont stop the clock if other instances are running... - if (!started || otherClientStarted) { - return; - } + // actually do stop it + // if (!started || otherClientStarted) { + // return; + // } clock.stop(); setCycle(0); @@ -74,6 +78,10 @@ const setCycle = (cycle) => { num_cycles_at_cps_change = cycle; }; +const setMaxCycle = (cycle) => { + max_cycle = cycle; +}; + const processMessage = (message) => { const { type, payload } = message; @@ -92,6 +100,10 @@ const processMessage = (message) => { setCycle(payload.cycle); break; } + case 'setmaxcycle': { + setMaxCycle(payload.maxcycle); + break; + } case 'toggle': { if (payload.started) { startClock(message.id); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ad0961032..91cd6c209 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -14,7 +14,7 @@ export class Cyclist { onToggle, onError, getTime, - latency = 0.1, + latency = 0.03, setInterval, clearInterval, beforeStart, diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index 6727c2422..7d46b5fe8 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -18,7 +18,7 @@ export function logger(message, type, data = {}) { } lastMessage = message; lastTime = t; - console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px'); + console.log(`${t} %c${message}`, 'background-color: black;color:white;border-radius:15px'); if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') { document.dispatchEvent( new CustomEvent(logKey, { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08acb..d050655ba 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,6 +1,6 @@ /* neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -11,6 +11,7 @@ export class NeoCyclist { constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; + this.lastTick = 0; // absolute time when last tick (clock callback) happened this.getTime = getTime; // get absolute time this.time_at_last_tick_message = 0; // the clock of the worker and the audio context clock can drift apart over time @@ -19,16 +20,18 @@ export class NeoCyclist { // in order to schedule events consistently. this.collator = new ClockCollator({ getTargetClockTime: getTime }); this.onToggle = onToggle; - this.latency = 0.1; // fixed trigger time offset + this.latency = -0.1; // fixed trigger time offset this.cycle = 0; + this.maxcycle = null; this.id = Math.round(Date.now() * Math.random()); this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); this.channel = new BroadcastChannel('strudeltick'); const tickCallback = (payload) => { - const { cps, begin, end, cycle, time } = payload; + const { cps, begin, end, cycle, maxcycle, time } = payload; this.cps = cps; this.cycle = cycle; + this.maxcycle = maxcycle; const currentTime = this.collator.calculateOffset(time) + time; processHaps(begin, end, currentTime); this.time_at_last_tick_message = currentTime; diff --git a/website/src/components/Vinyl.jsx b/website/src/components/Vinyl.jsx index fb2665cef..1a5dd9b70 100644 --- a/website/src/components/Vinyl.jsx +++ b/website/src/components/Vinyl.jsx @@ -25,14 +25,17 @@ const cyclist = new NeoCyclist(schedulerOptions); export function Vinyl() { const [isActive, setIsActive] = useState(false); const [cyclepos, setCyclepos] = useState(0); - const activate = () => setIsActive(true); + const activate = () => setIsActive(!isActive); if (isActive) { if (!cyclist.started) { cyclist.start(); - cyclist.setPattern(saw.segment(16)); + // cyclist.setPattern(saw.segment(8)); } } + else { + cyclist.stop(); + } useEffect(() => { const intervalId = setInterval(() => { From fe117e23fd8cada4688011092e8ea792105645e5 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 08:56:11 +0100 Subject: [PATCH 09/10] start of tap tempo --- website/src/components/Tap.jsx | 71 ++++++++++++++++++++++++++++++++++ website/src/pages/cycler.astro | 2 + 2 files changed, 73 insertions(+) create mode 100644 website/src/components/Tap.jsx diff --git a/website/src/components/Tap.jsx b/website/src/components/Tap.jsx new file mode 100644 index 000000000..c84ce9feb --- /dev/null +++ b/website/src/components/Tap.jsx @@ -0,0 +1,71 @@ +import React, { useState, useRef, useEffect } from "react"; + +export default function Tap({ initialCps = 0.4, maxSamples = 3}) { + const [timestamps, setTimestamps] = useState([]); + const [cps, setCps] = useState(initialCps); + + function addTap(ts = Date.now()) { + setTimestamps(prev => { + const next = [...prev, ts].slice(-(maxSamples + 1)); + calcCps(next); + return next; + }); + } + + function calcCps(times) { + if (!times || times.length < 2) return; + const intervals = []; + for (let i = 1; i < times.length; i++) { + intervals.push(times[i] - times[i - 1]); + } + + const avgMs = intervals.reduce((a, b) => a + b, 0) / intervals.length; + const newCps = 1000 / avgMs; + if (Number.isFinite(newCps) && newCps > 0 && newCps < 1000) { + setCps(newCps); + } + } + + function handleTap(e) { + e && e.preventDefault(); + addTap(); + } + + function handleReset(e) { + e && e.preventDefault(); + reset(); + } + + useEffect(() => { + function onKey(e) { + if (e.code === "Space") { + e.preventDefault(); + addTap(); + } else if (e.code === "Backspace") { + e.preventDefault(); + reset(); + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + function reset() { + setTimestamps([]); + // setCps(initialCps); + } + + + return ( +
+
+
{cps.toFixed(2)} cps
+ +
+
+ ); +} diff --git a/website/src/pages/cycler.astro b/website/src/pages/cycler.astro index 06bfb15f1..eb3d5ed2a 100644 --- a/website/src/pages/cycler.astro +++ b/website/src/pages/cycler.astro @@ -1,6 +1,7 @@ --- import HeadCommon from '../components/HeadCommon.astro'; import { Vinyl } from '../components/Vinyl.jsx'; +import Tap from '../components/Tap.jsx'; --- @@ -11,5 +12,6 @@ import { Vinyl } from '../components/Vinyl.jsx';
Hello there.
+ From 5de891f31d90f6f382714ea06588d2e3d5ff97e9 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 08:57:25 +0100 Subject: [PATCH 10/10] format --- website/src/components/Tap.jsx | 19 ++++++++----------- website/src/components/Vinyl.jsx | 3 +-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/website/src/components/Tap.jsx b/website/src/components/Tap.jsx index c84ce9feb..4fa9b5336 100644 --- a/website/src/components/Tap.jsx +++ b/website/src/components/Tap.jsx @@ -1,11 +1,11 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect } from 'react'; -export default function Tap({ initialCps = 0.4, maxSamples = 3}) { +export default function Tap({ initialCps = 0.4, maxSamples = 3 }) { const [timestamps, setTimestamps] = useState([]); const [cps, setCps] = useState(initialCps); function addTap(ts = Date.now()) { - setTimestamps(prev => { + setTimestamps((prev) => { const next = [...prev, ts].slice(-(maxSamples + 1)); calcCps(next); return next; @@ -38,16 +38,16 @@ export default function Tap({ initialCps = 0.4, maxSamples = 3}) { useEffect(() => { function onKey(e) { - if (e.code === "Space") { + if (e.code === 'Space') { e.preventDefault(); addTap(); - } else if (e.code === "Backspace") { + } else if (e.code === 'Backspace') { e.preventDefault(); reset(); } } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); }, []); function reset() { @@ -55,14 +55,11 @@ export default function Tap({ initialCps = 0.4, maxSamples = 3}) { // setCps(initialCps); } - return (
{cps.toFixed(2)} cps
-
diff --git a/website/src/components/Vinyl.jsx b/website/src/components/Vinyl.jsx index 1a5dd9b70..a0a651d37 100644 --- a/website/src/components/Vinyl.jsx +++ b/website/src/components/Vinyl.jsx @@ -32,8 +32,7 @@ export function Vinyl() { cyclist.start(); // cyclist.setPattern(saw.segment(8)); } - } - else { + } else { cyclist.stop(); }