mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 06:43:47 -04:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fbcbf60ed | |||
| 481838c712 | |||
| a6c8c0814a | |||
| 70c932a760 | |||
| f5631a01ae | |||
| 989b5dcc3a | |||
| 5de891f31d | |||
| 325e348bbf | |||
| fe117e23fd | |||
| 69a82ddc5b | |||
| 4209506fd0 | |||
| ff790c803d | |||
| f0efaabe20 | |||
| 5097ae8fc0 | |||
| 6b1e6cc7af | |||
| 9c6eca9a59 | |||
| 02bb88bde4 | |||
| c16511dfbd | |||
| 5054756ca2 |
@@ -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);
|
||||
|
||||
@@ -14,7 +14,7 @@ export class Cyclist {
|
||||
onToggle,
|
||||
onError,
|
||||
getTime,
|
||||
latency = 0.1,
|
||||
latency = 0.03,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
beforeStart,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/neocyclist.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
id="Layer_1"
|
||||
width="284.46"
|
||||
height="284.46"
|
||||
viewBox="0 0 284.46 284.46"
|
||||
overflow="visible"
|
||||
enable-background="new 0 0 284.46 284.46"
|
||||
xml:space="preserve"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
|
||||
sodipodi:docname="encoder_disc.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata2068"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs2066">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</defs><sodipodi:namedview
|
||||
inkscape:window-height="1131"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:zoom="1.0549412"
|
||||
inkscape:cx="195.27154"
|
||||
inkscape:cy="-101.90141"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:window-maximized="1" />
|
||||
<path
|
||||
id="path1"
|
||||
style="fill:#f9f9f9;stroke:none;stroke-width:1.23072"
|
||||
d="M 142.22902,0.61574073 A 141.61462,141.61462 0 0 0 0.61574173,142.22901 141.61462,141.61462 0 0 0 142.22902,283.84425 141.61462,141.61462 0 0 0 283.84425,142.22901 141.61462,141.61462 0 0 0 142.22902,0.61574073 Z m 0,106.22069927 a 35.393924,35.393924 0 0 1 35.39453,35.39257 35.393924,35.393924 0 0 1 -35.39453,35.39455 35.393924,35.393924 0 0 1 -35.39258,-35.39455 35.393924,35.393924 0 0 1 35.39258,-35.39257 z" /><g
|
||||
id="g2"><path
|
||||
d="M 242.47,41.99 242.441,42.02 217.4,67.06 C 198.17,47.83 171.589,35.93 142.23,35.93 V 0.5 c 39.17,0 74.6,15.85 100.24,41.49 z"
|
||||
id="path2015" /><path
|
||||
d="m 142.23,0.5 v 35.43 c -29.37,0 -55.93,11.89 -75.17,31.13 L 42.02,42.02 41.99,41.99 C 67.63,16.35 103.06,0.5 142.23,0.5 Z"
|
||||
id="path2017" /><path
|
||||
d="m 142.23,35.93 v 35.43 c -19.59,0 -37.3,7.92 -50.12,20.75 L 67.06,67.06 C 86.3,47.82 112.86,35.93 142.23,35.93 Z"
|
||||
id="path2019" /><path
|
||||
d="M 67.06,67.06 92.11,92.11 C 79.28,104.93 71.36,122.64 71.36,142.23 H 35.93 c 0,-29.36 11.89,-55.94 31.13,-75.17 z"
|
||||
id="path2021" /><path
|
||||
d="M 92.11,192.35 67.06,217.4 C 47.82,198.171 35.93,171.59 35.93,142.23 h 35.43 c 0,19.589 7.92,37.3 20.75,50.12 z"
|
||||
id="path2023" /><path
|
||||
d="m 142.23,213.09 v 35.44 c -29.37,0 -55.93,-11.891 -75.17,-31.131 l 25.05,-25.05 c 12.82,12.821 30.53,20.741 50.12,20.741 z"
|
||||
id="path2025" /><path
|
||||
d="m 142.23,248.53 v 35.43 c -39.17,0 -74.6,-15.851 -100.24,-41.49 l 0.03,-0.03 25.04,-25.04 c 19.24,19.24 45.8,31.13 75.17,31.13 z"
|
||||
id="path2027" /><path
|
||||
d="m 142.23,283.96 v -35.43 c 29.36,0 55.94,-11.9 75.17,-31.131 l 25.04,25.04 0.029,0.03 C 216.83,268.109 181.4,283.96 142.23,283.96 Z"
|
||||
id="path2029" /><path
|
||||
d="m 177.66,142.229 h 35.43 c 0,19.59 -7.92,37.301 -20.74,50.12 L 167.3,167.3 c 6.4,-6.401 10.36,-15.25 10.36,-25.071 z"
|
||||
id="path2031" /><path
|
||||
d="m 167.3,167.3 25.05,25.05 c -12.819,12.82 -30.529,20.74 -50.12,20.74 v -35.43 c 9.8,0 18.66,-3.95 25.07,-10.36 z"
|
||||
id="path2033" /><path
|
||||
d="m 142.23,177.66 v 35.43 c -19.59,0 -37.3,-7.92 -50.12,-20.74 l 25.05,-25.04 c 6.42,6.41 15.28,10.35 25.07,10.35 z"
|
||||
id="path2035" /><path
|
||||
d="m 117.16,167.3 v 0.01 L 92.11,192.35 C 79.28,179.531 71.36,161.82 71.36,142.23 h 35.43 c 0,9.82 3.96,18.669 10.37,25.07 z"
|
||||
id="path2037" /><path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
d="m 142.23,283.96 c -39.17,0 -74.6,-15.851 -100.24,-41.49 C 16.35,216.83 0.5,181.399 0.5,142.229 0.5,103.059 16.35,67.629 41.99,41.989 67.63,16.35 103.06,0.5 142.23,0.5 c 39.17,0 74.6,15.85 100.24,41.49 25.641,25.64 41.49,61.07 41.49,100.24 0,39.17 -15.85,74.601 -41.49,100.24 -25.64,25.639 -61.07,41.49 -100.24,41.49 z"
|
||||
id="path2041" /><path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
d="m 248.53,142.229 c 0,-29.35 -11.891,-55.93 -31.13,-75.17 -19.23,-19.23 -45.811,-31.13 -75.17,-31.13 -29.37,0 -55.93,11.89 -75.17,31.13 -19.24,19.23 -31.13,45.81 -31.13,75.17 0,29.36 11.89,55.94 31.13,75.17 19.24,19.24 45.8,31.131 75.17,31.131 29.36,0 55.94,-11.9 75.17,-31.131 19.24,-19.239 31.13,-45.819 31.13,-75.17 z"
|
||||
id="path2043" /><path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
d="m 142.23,213.09 c -19.59,0 -37.3,-7.92 -50.12,-20.74 -12.83,-12.819 -20.75,-30.53 -20.75,-50.12 0,-19.59 7.92,-37.3 20.75,-50.12 12.82,-12.83 30.53,-20.75 50.12,-20.75 19.59,0 37.3,7.92 50.12,20.75 12.82,12.82 20.74,30.53 20.74,50.12 0,19.59 -7.92,37.301 -20.74,50.12 -12.82,12.82 -30.53,20.74 -50.12,20.74 z"
|
||||
id="path2045" /><path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
d="m 117.16,167.31 c 6.42,6.41 15.28,10.351 25.07,10.351 9.8,0 18.66,-3.95 25.07,-10.36 6.4,-6.4 10.36,-15.25 10.36,-25.07 0,-9.819 -3.95,-18.67 -10.36,-25.07 -6.399,-6.41 -15.27,-10.36 -25.07,-10.36 -9.8,0 -18.66,3.95 -25.08,10.35 -6.4,6.4 -10.36,15.26 -10.36,25.08 0,9.82 3.96,18.67 10.37,25.07"
|
||||
id="path2047" /><path
|
||||
id="polyline2049"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="m 142.23,177.66 v 35.43 35.44 35.43"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2051"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="m 167.3,167.3 25.05,25.05 25.05,25.049 25.04,25.04"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2053"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="m 177.66,142.229 h 35.43 35.44 35.43"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2055"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="M 167.3,117.16 192.35,92.11 217.4,67.06 242.44,42.02"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2057"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="M 142.23,106.8 V 71.36 35.93 0.5"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2059"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="M 117.15,117.15 92.11,92.11 67.06,67.06 42.02,42.02"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2061"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="M 106.79,142.229 H 71.36 35.93 0.5"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
id="polyline2063"
|
||||
style="fill:none;stroke:#000000"
|
||||
d="m 117.16,167.3 v 0.01 l -25.05,25.04 -25.05,25.049 -25.04,25.04"
|
||||
sodipodi:nodetypes="ccccc" /></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,68 @@
|
||||
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 (
|
||||
<div>
|
||||
<div class="flex flex-col items-center p-7 font-medium text-white">
|
||||
<div>{cps.toFixed(2)} cps</div>
|
||||
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={handleTap}>
|
||||
Tap
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Vinyl.jsx - <short description TODO>
|
||||
Copyright (C) 2025 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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) => {},
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
onToggle: (started) => console.log('started: ', started),
|
||||
setInterval,
|
||||
clearInterval,
|
||||
// beforeStart,
|
||||
};
|
||||
const cyclist = new NeoCyclist(schedulerOptions);
|
||||
|
||||
export function Vinyl() {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [cyclepos, setCyclepos] = useState(0);
|
||||
const activate = () => setIsActive(!isActive);
|
||||
|
||||
if (isActive) {
|
||||
if (!cyclist.started) {
|
||||
cyclist.start();
|
||||
// cyclist.setPattern(saw.segment(8));
|
||||
}
|
||||
} else {
|
||||
cyclist.stop();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setCyclepos(cyclist.cycle);
|
||||
}, 10);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const deg = (cyclepos % 1) * 360;
|
||||
|
||||
const style = {
|
||||
fontSize: '20em',
|
||||
transform: 'rotate(' + deg + 'deg)',
|
||||
};
|
||||
return (
|
||||
<div onClick={activate} style={style}>
|
||||
<center>
|
||||
<img src={encoder.src} />
|
||||
</center>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import HeadCommon from '../components/HeadCommon.astro';
|
||||
import { Vinyl } from '../components/Vinyl.jsx';
|
||||
import Tap from '../components/Tap.jsx';
|
||||
---
|
||||
|
||||
<html lang="en" class="m-0 dark">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<title>Strudel Cycler</title>
|
||||
</head>
|
||||
<body class="h-app-height bg-background m-0">
|
||||
<div>Hello there.</div>
|
||||
<Vinyl client:only="react" />
|
||||
<Tap client:only="react" />
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user