Compare commits

..

4 Commits

Author SHA1 Message Date
Felix Roos 95ecd73d49 some notes 2022-06-16 11:48:09 +02:00
Felix Roos a9995d185f dont add location if addLocations is false 2022-06-16 11:48:00 +02:00
Felix Roos 5da2085436 count calls and log every second 2022-06-16 03:52:54 +02:00
Felix Roos ae9820b39f some testing with Fraction mock class 2022-06-16 03:17:35 +02:00
18 changed files with 184 additions and 4055 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
- Try it here: <https://strudel.tidalcycles.org/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
+92 -10
View File
@@ -4,9 +4,87 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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 Fraction from 'fraction.js';
// import Fraction from 'fraction.js';
import { TimeSpan } from './timespan.mjs';
let instances = 0;
let strings = 0;
let fractions = 0;
let numbers = 0;
setInterval(() => {
console.log(`${instances} calls = ${numbers} numbers + ${fractions} fractions + ${strings} strings`);
instances = 0;
strings = 0;
fractions = 0;
numbers = 0;
}, 1000);
// http://localhost:3000/#c3RhY2soCiAgImUzLGJiMyxkNCIuc3RydWN0KCJ4KDMsOCwtMSkiKS5vZmYoMS84LHg9PngudHJhbnNwb3NlKDEyKS52ZWxvY2l0eSguMikpLAogICJjMiIuc3RydWN0KCJ4KDQsOCwtMikiKSwKICAiYzMiLnN0cnVjdCgieCgzLDgsLTIpIi5mYXN0KDIpKQopLnNsb3coMikKIC5lY2hvKDQsLjEyNSwuOCkKIC52ZWxvY2l0eShzaW5lLnN0cnVjdCgieCo4IikuYWRkKDMvNSkubXVsKDIvNSkuZmFzdCg4KSkKIC8vIC5waWFub3JvbGwoKQovLyBzdHJ1ZGVsIGRpc2FibGUtaGlnaGxpZ2h0aW5n
// ~400k/s
// this is a "mock" for fraction.js, using just floats without any rational arithmetic
// to test if the performance gets better without fraction.js
// result: it seems to get better but not by much
// the main jankyness remains for some complicated patterns
class Fraction {
value; // number
constructor(value) {
instances++;
if (value instanceof Fraction) {
// TODO: return this?
this.value = value.value;
fractions++;
} else if (typeof value === 'string') {
const [n, d] = value.split('/');
this.value = n / (d || 1);
strings++;
} else if (typeof value !== 'number' || isNaN(value)) {
console.warn('Fraction got NaN', value);
} else {
numbers++;
this.value = Number(value);
if (isNaN(this.value)) {
console.warn('Fraction parsed NaN from', value);
}
}
}
add(other) {
return new Fraction(this.value + other);
}
sub(other) {
return new Fraction(this.value - other);
}
mul(other) {
return new Fraction(this.value * other);
}
div(other) {
return new Fraction(this.value / other);
}
toString() {
return this.value + '';
}
valueOf() {
return this.value;
}
floor() {
return new Fraction(Math.floor(this.value));
}
abs() {
return new Fraction(Math.abs(this.value));
}
inverse() {
return new Fraction(1 / this.value);
}
compare(other) {
return this.value - other;
}
equals(other) {
return this.value.valueOf() === other.valueOf();
}
// TODO: toFraction
}
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
return this.floor();
@@ -14,12 +92,16 @@ Fraction.prototype.sam = function () {
// Returns the start of the next cycle.
Fraction.prototype.nextSam = function () {
// return new Fraction(Math.floor(this.value) + 1);
return this.sam().add(1);
};
// Returns a TimeSpan representing the begin and end of the Time value's cycle
Fraction.prototype.wholeCycle = function () {
return new TimeSpan(this.sam(), this.nextSam());
/* const begin = Math.floor(this.value);
const end = begin + 1;
return new TimeSpan(begin, end); */
};
// The position of a time value relative to the start of its cycle.
@@ -63,20 +145,20 @@ Fraction.prototype.or = function (other) {
return this.eq(0) ? other : this;
};
const fraction = (n) => {
/* const fraction = (n) => {
if (typeof n === 'number') {
/*
https://github.com/infusion/Fraction.js/#doubles
„If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences."
„If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations“
// https://github.com/infusion/Fraction.js/#doubles
// „If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences."
// „If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations“
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
*/
// -> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
// -> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
n = String(n);
}
return Fraction(n);
};
}; */
const fraction = (n) => new Fraction(n);
export const gcd = (...fractions) => {
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/core",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
-3
View File
@@ -1049,9 +1049,6 @@ export class Pattern {
.unit('c')
.slow(factor);
}
onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
}
}
// TODO - adopt value.mjs fully..
+2 -4
View File
@@ -15,11 +15,9 @@ npm i @strudel.cycles/eval --save
```js
import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
evalScope(
import('@strudel.cycles/core'),
// import other strudel packages here
); // add strudel to eval scope
extend(strudel); // add strudel to eval scope
async function run(code) {
const { pattern } = await evaluate(code);
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/eval",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
+1 -1
View File
@@ -89,7 +89,7 @@ export default (_code) => {
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
if (addLocations && node.type === 'CallExpression' && node.callee.name === 'pure') {
const literal = node.arguments[0];
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/midi",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"version": "0.0.4",
"license": "GPL-3.0-or-later",
"dependencies": {
"tone": "^14.7.77",
"webmidi": "^2.5.2"
+2 -4
View File
@@ -22,15 +22,13 @@ let startedAt = -1;
*/
Pattern.prototype.osc = function () {
return this._withHap((hap) => {
const onTrigger = (time, hap, currentTime, cps) => {
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => {
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000;
}
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value);
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + (time + latency) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals);
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/osc",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc-js": "^2.3.2"
}
-3984
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -57,7 +57,14 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime, 1 /* cps */);
onTrigger(
time,
event,
currentTime,
1 /* cps */,
event.wholeOrPart().begin.valueOf(),
event.duration.valueOf(),
);
}
} catch (err) {
console.warn(err);
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/tonal",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
+2 -2
View File
@@ -6,8 +6,8 @@
"packages": {
"": {
"name": "@strudel.cycles/tone",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"version": "0.0.4",
"license": "GPL-3.0-or-later",
"dependencies": {
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
+4 -6
View File
@@ -10,20 +10,18 @@ import { State, TimeSpan } from '@strudel.cycles/core';
export class Scheduler {
worker;
pattern;
constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) {
constructor({ audioContext, interval = 0.2, onEvent }) {
this.worker = new ClockWorker(
audioContext,
(begin, end) => {
this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => {
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (e.context.onTrigger) {
// TODO: kill first param, as it's contained in e
e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */);
}
if (onEvent) {
onEvent?.(e);
} else {
console.warn('unplayable event: no audio node nor onEvent callback', e);
}
});
},
-28
View File
@@ -1,28 +0,0 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/webdirt",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"WebDirt": "github:dktr0/WebDirt"
}
},
"node_modules/WebDirt": {
"name": "webdirt",
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"license": "ISC"
}
},
"dependencies": {
"WebDirt": {
"version": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
"from": "WebDirt@github:dktr0/WebDirt"
}
}
}
+61
View File
@@ -0,0 +1,61 @@
# study: why are there so many calls?
- `pure('c3')` => 746 calls per second...
- shapeshifted: `(async()=>{return reify("c3").withLocation([1,5,20],[1,9,24])})()`Ï => same # of calls with or without shapeshifting
- without highlighting (// strudel disable-highlighting), there are only 15 calls
## call stack
this is how the ~15 calls are made for the first query:
- keypress -> activateCode -> evaluate -> safeEval -> pure -> new Pattern (pretty simple)
- query 0
- const timespan = new TimeSpan(0,1)
- Fraction(0), Fraction(1)
- onQuery(new State(timespan))
- pattern.query(state)
- pure.query(state)
- state.span.spanCycles
- end.sam() -> this.floor -> new Fraction
- begin.sam -> this.floor -> new Fraction
- begin.nextSam
- begin.sam -> this.floor -> new Fraction
- .add(1) -> new Fraction
- Fraction(0).wholeCycle -> new TimeSpan(0, 1)
- this.sam -> new Fraction(0)
- this.nextSam -> new Fraction(1)
- new Hap(TimeSpan(0,1), TimeSpan(0,1), 'c3')
- Tone.getTransport().cancel(0);
- queryNextTime = 0.5
- t = 0.6
## from simple to complicated
(all without highlighting)
- `pure('c3')`: 15 calls
- `pure('c3').fast(1)`: 74 calls
- `pure('c3').fast(1).fast(1)`: 133 calls
- `pure('c3').fast(1).fast(1).fast(1)`: 192 calls
- `pure('c3').fast(1).fast(1).fast(1).fast(1)`: 251 calls
- `pure('c3').fast(1).fast(1).fast(1).fast(1).fast(1)`: 310 calls
- `pure('c3').fast(2)`: 94 calls
- `pure('c3').fast(2).fast(2)`: 264 calls
- `pure('c3').fast(2).fast(2).fast(2)`: 636 calls
- `pure('c3').fast(4)`: 134 calls
## WIL
- Fraction.wholeCycle: returns a timespan for the whole cycle the given fraction is in. Fraction(n).wholeCycle -> TimeSpan(n.floor, n.floor+1)
- e.g. `Fraction(0.5).wholeCycle` -> `TimeSpan(0, 1)`
- TimeSpan.spanCycles: returns an array of whole cycle timespans that intersect with the given timespan
- e.g. `TimeSpan(0.5, 1.5)` -> `[TimeSpan(0, 1), TimeSpan(1, 2)]`
- pure: returns one Hap for spanCycles of query span. Hap will get wholeCycle as whole and query span as part
- reify: turns non patterns into patterns using pure. makes sure you get a pattern
## notes
- slowcat -> sequence -> fastcat -> slowcat is a somewhat hidden recursion
- slowcat -> pat_n can be negative and will then return an empty array. is that good? shouldn't pat_n be always a positive index?
- slowcat offset: how to think about this?
- slowcat: why add offset and sub it from the query span?