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
47 changed files with 327 additions and 4372 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 -2
View File
@@ -6,8 +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",
"snapshot": "cd repl && npm run snapshot",
"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",
+3 -6
View File
@@ -750,22 +750,19 @@ const generic_params = [
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _setter = (func, name) =>
const _setter = (func) =>
function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
}
return this.set(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(controls[name], name);
Pattern.prototype[name] = _setter(controls[name]);
});
// create custom param
controls.createParam = (name) => {
Pattern.prototype[name] = _setter(controls[name], name);
Pattern.prototype[name] = _setter(controls[name]);
return (...pats) => _name(name, ...pats);
};
+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,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
// this is a shortcut to eval code from a gist
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
export default (route, cache = true) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
export default (route) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
.then((res) => res.text())
.then((code) => eval(code));
+5 -3
View File
@@ -86,9 +86,11 @@ export class Hap {
}
showWhole() {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
}
showWhole() {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
}
combineContext(b) {
+1 -1
View File
@@ -15,5 +15,5 @@ export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export { default as gist } from './gist.js';
export * as gist from './gist.js';
// export * from './value.mjs';
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/core",
"version": "0.1.1",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"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"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.1.1",
"version": "0.1.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
-4
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..
@@ -1389,7 +1386,6 @@ export function pr(args) {
}
export const add = curry((a, pat) => pat.add(a));
export const chop = curry((a, pat) => pat.chop(a))
export const chunk = curry((a, pat) => pat.chunk(a));
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
export const div = curry((a, pat) => pat.div(a));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/embed",
"version": "0.1.1",
"version": "0.1.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+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);
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/eval",
"version": "0.1.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"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",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.1.2",
"version": "0.1.1",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"type": "module",
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/core": "^0.1.0",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
+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);
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/midi",
"version": "0.1.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"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 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.1.2",
"version": "0.1.1",
"description": "Midi API for strudel",
"main": "index.mjs",
"repository": {
@@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.1.2",
"@strudel.cycles/tone": "^0.1.1",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.1.2",
"version": "0.1.1",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -25,8 +25,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/eval": "^0.1.2",
"@strudel.cycles/tone": "^0.1.2"
"@strudel.cycles/core": "^0.1.0",
"@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.1"
}
}
+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);
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/osc",
"version": "0.1.1",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"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"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.1.1",
"version": "0.1.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
-3984
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.1.3",
"version": "0.1.2",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -38,9 +38,9 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/eval": "^0.1.2",
"@strudel.cycles/tone": "^0.1.2",
"@strudel.cycles/core": "*",
"@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.1",
"react-codemirror6": "^1.1.0",
"react-hook-inview": "^4.5.0"
},
+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);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.1.1",
"version": "0.1.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.1.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"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
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.1.2",
"version": "0.1.1",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"type": "module",
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/core": "^0.1.0",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/tone",
"version": "0.1.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"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",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.1.2",
"version": "0.1.1",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/core": "^0.1.0",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
-1
View File
@@ -7,4 +7,3 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './clockworker.mjs';
export * from './scheduler.mjs';
export * from './webaudio.mjs';
export * from './sampler.mjs';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.1.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.1.2",
"version": "0.1.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -28,6 +28,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1"
"@strudel.cycles/core": "^0.1.0"
}
}
+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);
}
});
},
+64 -123
View File
@@ -6,11 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import * as strudel from '@strudel.cycles/core';
import { fromMidi } from '@strudel.cycles/core';
import { loadBuffer } from './sampler.mjs';
const { Pattern } = strudel;
// export const getAudioContext = () => Tone.getContext().rawContext;
import { Tone } from '@strudel.cycles/tone';
const { Pattern, getFrequency, patternify2 } = strudel;
let audioContext;
export const getAudioContext = () => {
@@ -20,15 +17,9 @@ export const getAudioContext = () => {
return audioContext;
};
const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
const lookahead = 0.2;
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
@@ -39,115 +30,65 @@ const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
return gainNode;
};
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
n = 0,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0,
sustain = 1,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
if (!freq && typeof n === 'string') {
freq = fromMidi(toMidi(n));
}
// make oscillator
const o = ac.createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + hap.duration + release);
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
g.gain.value = 0.5;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
chain.push(adsr);
} else {
// load sample
const samples = getLoadedSamples();
if (!samples) {
console.warn('no samples loaded');
return;
}
const bank = samples?.[s];
if (!bank) {
console.warn('sample not found:', s, 'try one of ' + Object.keys(samples));
return;
} else {
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const bank = samples[s];
const sampleUrl = bank[n % bank.length];
let buffer = await loadBuffer(sampleUrl, ac);
if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
const src = ac.createBufferSource();
src.buffer = buffer;
src.playbackRate.value = Math.abs(speed);
// TODO: nudge, unit, cut, loop
let duration = src.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
src.start(t, offset, duration);
src.stop(t + duration);
chain.push(src);
}
}
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// master out
const master = ac.createGain();
master.gain.value = 0.8 * gain;
chain.push(master);
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
Pattern.prototype.withAudioNode = function (createAudioNode) {
return this._withHap((hap) => {
return hap.setContext({
...hap.context,
createAudioNode: (t, e) => createAudioNode(t, e, hap.context.createAudioNode?.(t, hap)),
});
});
};
Pattern.prototype._wave = function (type) {
return this.withAudioNode((t, e) => {
const osc = getAudioContext().createOscillator();
osc.type = type;
const f = getFrequency(e);
osc.frequency.value = f; // expects frequency..
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration.valueOf();
osc.start(begin);
osc.stop(end); // release?
return osc;
});
};
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
return this.withAudioNode((t, e, node) => {
const velocity = e.context?.velocity || 1;
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration.valueOf() + lookahead;
const envelope = adsr(a, d, s, r, velocity, begin, end);
node?.connect(envelope);
return envelope;
});
};
Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) {
return this.withAudioNode((t, e, node) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
node?.connect(filter);
return filter;
});
};
Pattern.prototype.filter = function (type, frequency) {
return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this);
};
Pattern.prototype.out = function () {
const master = getAudioContext().createGain();
master.gain.value = 0.1;
master.connect(getAudioContext().destination);
return this.withAudioNode((t, e, node) => {
if (!node) {
console.warn('out: no source! call .osc() first');
}
node?.connect(master);
})._withHap((hap) => {
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
return hap.setContext({ ...hap.context, onTrigger });
});
};
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });
+1
View File
@@ -1 +1,2 @@
export * from './webdirt.mjs';
export * from './sampler.mjs';
-28
View File
@@ -1,28 +0,0 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.1.1",
"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"
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.1.1",
"version": "0.1.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1",
"@strudel.cycles/core": "^0.1.0",
"WebDirt": "github:dktr0/WebDirt"
}
}
@@ -1,8 +1,6 @@
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
export const getCachedBuffer = (url) => bufferCache[url];
export const loadBuffer = (url, ac) => {
if (!loadCache[url]) {
loadCache[url] = fetch(url)
@@ -93,7 +91,7 @@ export const loadGithubSamples = async (path, nameFn) => {
*
*/
export const samples = (sampleMap, baseUrl = sampleMap._base) => {
export const samples = (sampleMap, baseUrl = '') => {
sampleCache.current = {
...sampleCache.current,
...Object.fromEntries(
+1 -1
View File
@@ -1,7 +1,7 @@
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
import * as WebDirt from 'WebDirt';
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from './sampler.mjs';
let webDirt;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.1.2",
"version": "0.1.1",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"scripts": {
@@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.1.1"
"@strudel.cycles/core": "^0.1.0"
}
}
-2
View File
@@ -22,5 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
oldtunes.mjs
+1 -1
View File
@@ -3,7 +3,7 @@
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --host",
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
+2 -9
View File
@@ -13,8 +13,7 @@ import './App.css';
import logo from './logo.svg';
import * as tunes from './tunes.mjs';
import * as WebDirt from 'WebDirt';
import { loadWebDirt } from '@strudel.cycles/webdirt';
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
import { loadWebDirt, resetLoadedSamples } from '@strudel.cycles/webdirt';
evalScope(
Tone,
@@ -140,13 +139,7 @@ function App() {
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
</div>
<div className="flex">
<button
onClick={() => {
getAudioContext().resume(); // fixes no sound in ios webkit
togglePlay();
}}
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
>
<button onClick={() => togglePlay()} className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
{!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? (
+1 -9
View File
@@ -6,8 +6,6 @@
import { evaluate } from '@strudel.cycles/eval';
import { extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
import * as webaudio from '@strudel.cycles/webaudio';
import controls from '@strudel.cycles/core/controls.mjs';
// import gist from '@strudel.cycles/core/gist.js';
import { mini } from '@strudel.cycles/mini/mini.mjs';
// import { Tone } from '@strudel.cycles/tone';
@@ -85,9 +83,6 @@ const toneHelpersMocked = {
strudel.Pattern.prototype.tone = function () {
return this;
};
strudel.Pattern.prototype.webdirt = function () {
return this;
};
// draw mock
strudel.Pattern.prototype.pianoroll = function () {
@@ -121,7 +116,6 @@ const uiHelpersMocked = {
backgroundImage: id,
};
// TODO: refactor to evalScope
extend(
// Tone,
@@ -129,8 +123,6 @@ extend(
strudel.Pattern.prototype.bootstrap(),
toneHelpersMocked,
uiHelpersMocked,
controls,
webaudio,
/* controls,
toneHelpers,
voicingHelpers,
@@ -189,6 +181,6 @@ export const testCycles = {
randomBells: 24,
waa: 16,
waar: 16,
hyperpop: 10,
hyperpop: 60,
festivalOfFingers3: 16,
};
+24 -114
View File
@@ -728,7 +728,7 @@ bell = bell.chain(vol(0.6).connect(delay),out());
.slow(6)
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`;
/* export const waa = `n("a4 [a3 c3] a3 c3")
export const waa = `"a4 [a3 c3] a3 c3"
.sub("<7 12>/2")
.off(1/8, add("12"))
.off(1/4, add("7"))
@@ -736,37 +736,23 @@ bell = bell.chain(vol(0.6).connect(delay),out());
.slow(2)
.wave("sawtooth square")
.filter('lowpass', "<2000 1000 500>")
.out()`; */
.out()`;
export const waa = `n(
"a4 [a3 c3] a3 c3"
.sub("<7 12>/2")
.off(1/8, add("12"))
.off(1/4, add("7"))
)
.legato(.5)
.slow(2)
.s("sawtooth square")
.cutoff("<2000 1000 500>")
.out()
`;
export const waa2 = `n(
"a4 [a3 c3] a3 c3"
.sub("<7 12 5 12>".slow(2))
.off(1/4,x=>x.add(7))
.off(1/8,x=>x.add(12))
)
.slow(2)
.legato(sine.range(0.3, 2).slow(28))
.s("sawtooth square".fast(2))
.cutoff(cosine.range(500,4000).slow(16))
.out()`;
export const waar = `"a4 [a3 c3] a3 c3".color('#F9D649')
.sub("<7 12 5 12>".slow(2))
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
.off(1/8,x=>x.add(12).color('#215CB6'))
.slow(2)
.legato(sine.range(0.3, 2).slow(28))
.wave("sawtooth square".fast(2))
.filter('lowpass', cosine.range(500,4000).slow(16))
.out()
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})`;
export const hyperpop = `const lfo = cosine.slow(15);
const lfo2 = sine.slow(16);
const filter1 = x=>x.cutoff(lfo2.range(300,3000));
const filter2 = x=>x.hcutoff(lfo.range(1000,6000)).cutoff(4000)
const filter1 = x=>x.filter('lowpass', lfo2.range(300,3000));
const filter2 = x=>x.filter('highpass', lfo.range(1000,6000)).filter('lowpass',4000)
const scales = cat('D3 major', 'G3 major').slow(8)
const drums = await players({
@@ -779,30 +765,24 @@ const drums = await players({
stack(
"-7 0 -7 7".struct("x(5,8,2)").fast(2).sub(7)
.scale(scales)
.n()
.s("sawtooth,square")
.gain(.3).attack(0.01).decay(0.1).sustain(.5)
.scale(scales).wave("sawtooth,square").velocity(.3).adsr(0.01,0.1,.5,0)
.apply(filter1),
"~@3 [<2 3>,<4 5>]"
.echo(4,1/16,.7)
.echo(8,1/16,.7)
.scale(scales)
.n()
.s('square').gain(.7)
.attack(0.01).decay(0.1).sustain(0)
.apply(filter1),
"6 4 2".add(14)
.wave('square').velocity(.7).adsr(0.01,0.1,0).apply(filter1),
"6 5 4".add(14)
.superimpose(sub("5"))
.fast(1).euclidLegato(3,8)
.mask("<1 0@7>")
.fast(2)
.echo(32, 1/8, .8)
.echo(32, 1/8, .9)
.scale(scales)
.n()
.s("sawtooth")
.gain(sine.range(.1,.4).slow(8))
.attack(.001).decay(.2).sustain(0)
.wave("sawtooth")
.velocity(.2)
.adsr(.01,.5,0)
.apply(filter2)
//.echo(4,1/16,.5)
).out().stack(
stack(
"bd <~@7 [~ bd]>".fast(2),
@@ -810,7 +790,7 @@ stack(
"[~ hh3]*2"
).tone(drums.chain(vol(.18),out())).fast(2)
).slow(2)
//.pianoroll({minMidi:20, maxMidi:160})
// strudel disable-highlighting`;
@@ -836,73 +816,3 @@ stack(
"<Cm7 [Dm7b5 G7b9] Bbm7 [Cm7b5 F7b9]>".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings(),
"[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25)
).tone((await piano()).toDestination()).slow(2)`;
export const customTrigger = `stack(
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))),
freq("440(5,8)".legato(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
).s("<sawtooth square>/2")
.onTrigger((t,hap,ct)=>{
const ac = Tone.getContext().rawContext;
t = ac.currentTime + t - ct;
const { freq, s, gain = 1 } = hap.value;
const master = ac.createGain();
master.gain.value = 0.1 * gain;
master.connect(ac.destination);
const o = ac.createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.connect(master);
o.start(t);
o.stop(t + hap.duration);
}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`;
export const bornagain = `stack(
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)")
.layer(x=>x.mul("1.006,.995"))), // detune
freq("440(5,8)".legato(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
).s("<sawtooth square>/2")
.cutoff(perlin.range(100,4000).slow(4))
.jux(rev)
.out()
.stack(s("bd(3,8),hh*4,~ sd").webdirt())`;
export const meltingsubmarine = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
//.hush()
,"<a1 b1*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
//.hush()
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.out()
.slow(3/2)`;
File diff suppressed because one or more lines are too long
+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?