Compare commits

..

3 Commits

Author SHA1 Message Date
Felix Roos cd1c4632d7 build to single file 2022-11-23 14:00:31 +01:00
Felix Roos f1a850e133 forgot tonal pkg import 2022-11-22 21:43:30 +01:00
Felix Roos 53b61b8d85 add workshop seed project 2022-11-22 21:33:47 +01:00
368 changed files with 46143 additions and 43828 deletions
-17
View File
@@ -1,17 +0,0 @@
krill-parser.js
krill.pegjs
.eslintrc.json
server.js
tidal-sniffer.js
*.jsx
tunejs.js
out/**
postcss.config.js
postcss.config.cjs
tailwind.config.js
tailwind.config.cjs
vite.config.js
/**/dist/**/*
!**/*.mjs
**/*.tsx
**/*.ts
-16
View File
@@ -1,16 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended"],
"overrides": [],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [],
"rules": {
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }]
}
}
+3 -3
View File
@@ -24,10 +24,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
cache: "npm"
- name: Install Dependencies
run: npm ci && cd website && npm ci
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
- name: Build
run: npm run build
@@ -39,7 +39,7 @@ jobs:
uses: actions/upload-pages-artifact@v1
with:
# Upload entire repository
path: "./website/dist"
path: "./out"
- name: Deploy to GitHub Pages
id: deployment
+7 -9
View File
@@ -10,12 +10,10 @@ jobs:
node-version: [18]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm install
- run: npm run format-check
- run: npm run lint
- run: npm test
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm test
-9
View File
@@ -1,9 +0,0 @@
*.md
*.json
*.yml
*.
**/out
**/dist
packages/mini/krill-parser.js
packages/xen/tunejs.js
paper
+15 -14
View File
@@ -1,15 +1,16 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
+1 -2
View File
@@ -5,6 +5,5 @@
],
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
},
"testing.automaticallyOpenPeekView": "never"
}
}
+7 -5
View File
@@ -12,8 +12,8 @@ To get in touch with the contributors, either
## Ask a Question
If you have any questions about strudel, make sure you've glanced through the
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
If you have any questions about strudel, make sure you've read the
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
If not, use one of the Communication Channels above!
Don't be afraid to ask! Your question might be of great value for other people too.
@@ -29,10 +29,12 @@ If you made some music with strudel, you can give back some love and share what
Your creation could also be part of the random selection in the REPL if you want.
Use one of the Communication Channels listed above.
## Improve the Docs
## Improve the Tutorial
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
This will even work without setting up a development environment, only a github account is required.
## Propose a Feature
+1 -1
View File
@@ -5,7 +5,7 @@
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.
- Try it here: <https://strudel.tidalcycles.org/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
## Running Locally
-20
View File
@@ -1,20 +0,0 @@
# my-patterns
This directory can be used to save your own patterns, which then get
made into a pattern swatch.
0. fork and clone the strudel repository
1. run `npm run setup` in the strudel folder
1. Save one or more .txt files in this folder
2. run `npm run repl` in the top-level strudel folder
3. open `http://localhost:3000/swatch/` !
## deploy
1. in your fork, go to settings -> pages and select "Github Actions" as source
2. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
3. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`
4. go to Actions -> `Build and Deploy` and click `Run workflow`
5. view your patterns at `<your-username>.github.io/strudel/swatch/`
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
+5524 -3161
View File
File diff suppressed because it is too large Load Diff
+9 -18
View File
@@ -1,29 +1,23 @@
{
"name": "@strudel.cycles/monorepo",
"version": "0.5.0",
"version": "0.0.4",
"private": true,
"description": "Port of tidalcycles to javascript",
"scripts": {
"pretest": "npm run jsdoc-json",
"prebuild": "npm run jsdoc-json",
"pretest": "cd tutorial && npm run jsdoc-json",
"test": "vitest run --version",
"test-ui": "vitest --ui",
"test-coverage": "vitest --coverage",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run jsdoc-json && npm run bootstrap && cd website && npm i",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"snapshot": "vitest run -u --silent",
"repl": "cd website && npm run dev",
"repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "cd website && npm run build",
"preview": "cd website && npm run preview",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
"preview": "npx serve ./out",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet",
"codeformat": "prettier --write .",
"format-check": "prettier --check .",
"check": "npm run format-check && npm run lint && npm run test",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
},
"workspaces": [
"packages/*"
@@ -46,18 +40,15 @@
},
"homepage": "https://strudel.tidalcycles.org",
"devDependencies": {
"@vitest/ui": "^0.25.7",
"@vitest/ui": "^0.21.1",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"eslint": "^8.28.0",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0",
"prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.25.7"
"vitest": "^0.21.1"
}
}
-67
View File
@@ -1,67 +0,0 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
const { createParams } = controls;
let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext();
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`;
const render = (t) => {
let frame;
/* if (sync) {
t = scheduler.now();
frame = this.queryArc(t, t);
} else { */
t = Math.round(t);
frame = this.slow(1000).queryArc(t, t);
// }
ctx.fillStyle = clearColor;
ctx.fillRect(0, 0, ww, wh);
frame.forEach((f) => {
let { x, y, w, h, s, r, a = 0, fill = 'darkseagreen' } = f.value;
w *= ww;
h *= wh;
if (r !== undefined && a !== undefined) {
const radians = a * 2 * Math.PI;
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
x = cx + Math.cos(radians) * r * cx;
y = cy + Math.sin(radians) * r * cy;
} else {
x *= ww - w;
y *= wh - h;
}
const val = { ...f.value, x, y, w, h };
ctx.fillStyle = fill;
if (s === 'rect') {
ctx.fillRect(x, y, w, h);
} else if (s === 'ellipse') {
ctx.beginPath();
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
ctx.fill();
}
callback && callback(ctx, val, f);
});
window.frame = requestAnimationFrame(render);
};
window.frame = requestAnimationFrame(render);
return silence;
};
export const { x, y, w, h, a, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'a', 'r', 'fill', 'smear');
export const rescale = register('rescale', function (f, pat) {
return pat.mul(x(f).w(f).y(f).h(f));
});
export const moveXY = register('moveXY', function (dx, dy, pat) {
return pat.add(x(dx).y(dy));
});
export const zoomIn = register('zoomIn', function (f, pat) {
const d = pure(1).sub(f).div(2);
return pat.rescale(f).move(d, d);
});
-175
View File
@@ -1,175 +0,0 @@
export const colorMap = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
};
export function convertColorToNumber(color) {
// Convert color to lowercase for easier matching
color = color.toLowerCase();
// If the color is a hex code, convert it to a number
if (color[0] === '#') {
return convertHexToNumber(color);
}
// If the color is a named color, return the corresponding number
if (colorMap[color] !== undefined) {
return convertHexToNumber(colorMap[color]);
}
// If the color is not recognized, return null
return -1;
}
export function convertHexToNumber(hex) {
// Remove the leading '#' from the hex code
hex = hex.slice(1);
// Convert the hex code to a number
return parseInt(hex, 16);
}
+35 -79
View File
@@ -51,7 +51,6 @@ const generic_params = [
*
* @name accelerate
* @param {number | Pattern} amount acceleration.
* @superdirtOnly
* @example
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
*
@@ -76,20 +75,19 @@ const generic_params = [
*
* @name amp
* @param {number | Pattern} amount gain.
* @superdirtOnly
* @example
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
*
*/
['f', 'amp', 'like @gain@, but linear.'],
/**
// TODO: find out why 0 does not work, and it generally seems not right
/*
* A pattern of numbers to specify the attack time of an envelope applied to each sample.
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name attack
* @param {number | Pattern} attack time in seconds.
* @example
* note("c3 e3").attack("<0 .1 .5>")
* n("c5 e5").s('superpiano').attack("<0 .1>").osc()
*
*/
[
@@ -109,36 +107,18 @@ const generic_params = [
*/
['f', 'bank', 'selects sound bank to use'],
/**
* Gain envelope decay time = the time it takes after the attack time to reach the sustain level.
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
// TODO: find out how this works?
/*
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
*
* @name decay
* @param {number | Pattern} time decay time in seconds
* @example
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
* s("sax").cut(1).decay("<.1 .2 .3 .4>").sustain(0).osc()
*
*/
['f', 'decay', ''],
/**
* Gain envelope sustain level. [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name sustain
* @param {number | Pattern} gain sustain level between 0 and 1
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['f', 'sustain', ''],
/**
* Gain envelope release time. [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name release
* @param {number | Pattern} time release time in seconds
* @example
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
*
*/
[
'f',
'release',
@@ -155,26 +135,22 @@ const generic_params = [
*
* @name bandf
* @param {number | Pattern} frequency center frequency
* @synonyms bpf
* @example
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
*
*/
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
['f', 'bpf', ''],
// TODO: in tidal, it seems to be normalized
/**
* Sets the q-factor of the band-pass filter
*
* @name bandq
* @param {number | Pattern} q q factor
* @synonyms bpq
* @example
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
*
*/
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
['f', 'bpq', ''],
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
@@ -228,7 +204,7 @@ const generic_params = [
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
*/
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
// ['f', 'clhatdecay', ''],
/**
* bit crusher effect.
@@ -273,7 +249,7 @@ const generic_params = [
* @name cut
* @param {number | Pattern} group cut group number
* @example
* s("rd*4").cut(1)
* s("bd sax").cut(1).osc()
*
*/
[
@@ -286,50 +262,53 @@ const generic_params = [
*
* @name cutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms lpf
* @example
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
*
*/
// TODO: add lpf synonym
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
['f', 'lpf'],
/**
* Applies the cutoff frequency of the high-pass filter.
*
* @name hcutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hpf
* @example
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
*
*/
['f', 'hcutoff', ''],
['f', 'hpf', ''],
// TODO: add hpf synonym
[
'f',
'hcutoff',
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
],
/**
* Applies the resonance of the high-pass filter.
*
* @name hresonance
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hpq
* @example
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
*
*/
['f', 'hpq', ''],
['f', 'hresonance', ''],
[
'f',
'hresonance',
'a pattern of numbers from 0 to 1. Applies the resonance of the high-pass filter. Has alias @hpq@',
],
// TODO: add hpq synonym
/**
* Applies the cutoff frequency of the low-pass filter.
*
* @name resonance
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms lpq
* @example
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
*
*/
['f', 'lpq'],
['f', 'resonance', ''],
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
// TODO: add lpq synonym?
/**
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
@@ -342,36 +321,17 @@ const generic_params = [
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
// ['f', 'cutoffegint', ''],
// TODO: does not seem to work
/**
/*
* Sets the level of the delay signal.
*
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .25 .5 1>")
* s("bd").delay("<0 .5 .75 1>").osc()
*
*/
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
*
*/
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
*
*/
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
/* // TODO: test
* Specifies whether delaytime is calculated relative to cps.
@@ -440,7 +400,7 @@ const generic_params = [
* @example
* freq("220 110 440 110").s("superzow").osc()
* @example
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
* freq("110".mulOut(".5 1.5 .6 [2 3]")).s("superzow").osc()
*
*/
['f', 'freq', ''],
@@ -524,15 +484,11 @@ const generic_params = [
// ['f', 'ophatdecay', ''],
// TODO: example
/**
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
* a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.
*
* @name orbit
* @param {number | Pattern} number
* @example
* stack(
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*
*/
[
'i',
@@ -606,7 +562,7 @@ const generic_params = [
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'semitone', ''],
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['f', 'velocity', ''],
['f', 'velocity', ''],
['f', 'voice', ''], // TODO: synth param
/**
* Sets the level of reverb.
@@ -614,7 +570,7 @@ const generic_params = [
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
* s("bd sd").room("<0 .2 .4 .6 .8 1>").osc()
*
*/
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
@@ -622,9 +578,9 @@ const generic_params = [
* Sets the room size of the reverb, see {@link room}.
*
* @name size
* @param {number | Pattern} size between 0 and 10
* @param {number | Pattern} size between 0 and 1
* @example
* s("bd sd").room(.8).size("<0 1 2 4 8>")
* s("bd sd").room(.8).size("<0 .2 .4 .6 .8 1>").osc()
*
*/
// TODO: find out why :
@@ -664,9 +620,9 @@ const generic_params = [
* @name speed
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
* @example
* s("bd").speed("<1 2 4 1 -2 -4>")
* s("bd").speed("<1 2 4 1 -2 -4>").osc()
* @example
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
* speed("1 1.5*2 [2 1.1]").s("sax").cut(1).osc()
*
*/
[
@@ -831,6 +787,6 @@ controls.createParam = (name) => {
};
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
names.reduce((acc, name) => Object.assign(acc, { [name]: createParam(name) }), {});
export default controls;
-3
View File
@@ -49,9 +49,6 @@ export class Cyclist {
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() {
return this.getTime() - this.origin + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);
+1 -1
View File
@@ -4,7 +4,7 @@ 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 { Pattern, getTime, State, TimeSpan } from './index.mjs';
import { Pattern, getTime } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
+29 -102
View File
@@ -1,97 +1,47 @@
/*
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
Copyright (C) 2023 Rohan Drape and strudel contributors
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
https://rohandrape.net/?t=hmt
euclid.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.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/>.
*/
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten } from './util.mjs';
import { Pattern, timeCat } from './pattern.mjs';
import bjork from 'bjork';
import { rotate } from './util.mjs';
import Fraction from './fraction.mjs';
const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_xs, __xs] = splitAt(offs, xs);
return [
[offs, ons - offs],
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
];
};
const right = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_ys, __ys] = splitAt(ons, ys);
const result = [
[ons, offs - ons],
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
];
return result;
};
const _bjork = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
const bjork = function (ons, steps) {
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
return flatten(result[1][0]).concat(flatten(result[1][1]));
const euclid = (pulses, steps, rotation = 0) => {
const b = bjork(steps, pulses);
if (rotation) {
return rotate(b, -rotation);
}
return b;
};
/**
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common
* divisor of two numbers. They were described in 2004 by Godfried
* Toussaint, a canadian computer scientist. Euclidian rhythms are
* really useful for computer/algorithmic music because they can
* describe a large number of rhythms with a couple of numbers.
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
* describe a large number of rhythms used in the most important music world traditions.
*
* @memberof Pattern
* @name euclid
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation (optional) offset in steps
* @returns Pattern
* @example
* // The Cuban tresillo pattern.
* note("c3").euclid(3,8)
*/
/**
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @returns Pattern
* @example
* // A Samba rhythm necklace from Brazil
* note("c3").euclidRot(3,16,14)
*/
/**
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* note("c3").euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* note("c3").euclid(3,4)
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
* note("c3").euclidRot(3,5,2)
* note("c3").euclid(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* note("c3").euclid(3,7)
* @example // The Cuban tresillo pattern.
@@ -121,57 +71,34 @@ const bjork = function (ons, steps) {
* @example // A common West African bell pattern.
* note("c3").euclid(7,12)
* @example // A Samba rhythm necklace from Brazil.
* note("c3").euclidRot(7,16,14)
* note("c3").euclid(7,16,14)
* @example // A rhythm necklace used in the Central African Republic.
* note("c3").euclid(9,16)
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
* note("c3").euclidRot(11,24,14)
* note("c3").euclid(11,24,14)
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
* note("c3").euclidRot(13,24,5)
* note("c3").euclid(13,24,5)
*/
const _euclidRot = function (pulses, steps, rotation) {
const b = bjork(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
return b;
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
return this.struct(euclid(pulses, steps, rotation));
};
export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation));
});
/**
* Similar to `euclid`, but each pulse is held until the next pulse,
* so there will be no gaps.
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/
const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, rotation);
const gapless = bin_pat
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
const bin_pat = euclid(pulses, steps, rotation);
const firstOne = bin_pat.indexOf(1);
const gapless = rotate(bin_pat, firstOne)
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless));
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
return _euclidLegato(pulses, steps, 0, pat);
});
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
export default euclid;
+2 -9
View File
@@ -4,7 +4,7 @@ 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 { isPattern } from './index.mjs';
import { isPattern, Pattern } from './index.mjs';
let scoped = false;
export const evalScope = async (...args) => {
@@ -19,14 +19,7 @@ export const evalScope = async (...args) => {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
// Object.assign(globalThis, ...modules);
// below is a fix for above commented out line
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
modules.forEach((module) => {
Object.entries(module).forEach(([name, value]) => {
globalThis[name] = value;
});
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str, options = {}) {
@@ -16,7 +16,7 @@ evalScope(
import('@strudel.cycles/tonal'),
);
setStringParser(mini);
setStringParser(mini)
const { evaluate } = repl({
defaultOutput: webaudioOutput,
-65
View File
@@ -1,65 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Buildless Vanilla Strudel REPL</title>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
>
// LOADING</textarea
>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
<script type="module">
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.4.1';
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.4.1';
const modules = [
import('https://cdn.skypack.dev/@strudel.cycles/core@0.4.1'),
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.4.1'),
];
const input = document.getElementById('text');
Promise.all(modules).then(() => {
input.innerHTML = `note("<c3 [d3 e3]>").cutoff(1000)`;
document.getElementById('start').addEventListener('click', () => {
evaluate(input.value);
});
});
evalScope(controls, ...modules);
const { evaluate } = repl({
defaultOutput: (hap, deadline, duration) => {
console.log(deadline, duration, hap.value);
},
getTime: () => Date.now() / 1000,
transpiler,
beforeEval: (code) => console.log('evaluate', code),
afterEval: (code) => {},
});
</script>
</body>
</html>
+4 -27
View File
@@ -79,33 +79,10 @@ export class Hap {
);
}
show(compact = false) {
const value =
typeof this.value === 'object'
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value;
var spans = '';
if (this.whole == undefined) {
spans = '~' + this.part.show;
} else {
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
if (!this.whole.begin.equals(this.part.begin)) {
spans = this.whole.begin.show() + ' ⇜ ';
}
if (!is_whole) {
spans += '(';
}
spans += this.part.show();
if (!is_whole) {
spans += ')';
}
if (!this.whole.end.equals(this.part.end)) {
spans += ' ⇝ ' + this.whole.end.show();
}
}
return '[ ' + spans + ' | ' + value + ' ]';
show() {
return (
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
);
}
showWhole(compact = false) {
-1
View File
@@ -21,7 +21,6 @@ export * from './repl.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
+1 -1
View File
@@ -2,7 +2,7 @@ export const logKey = 'strudel.log';
export function logger(message, type, data = {}) {
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
if (typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
detail: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.5.0",
"version": "0.4.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -25,6 +25,7 @@
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
+924 -935
View File
File diff suppressed because it is too large Load Diff
+8 -146
View File
@@ -4,27 +4,13 @@ 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 { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { Pattern } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
let { value } = e;
if (typeof e.value !== 'object') {
value = { value };
}
let { note, n, freq, s } = value;
if (freq) {
return freqToMidi(freq);
}
note = note ?? n;
if (typeof note === 'string') {
return toMidi(note);
}
if (typeof note === 'number') {
return note;
}
if (s) {
return '_' + s;
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
if (typeof value === 'string') {
value = toMidi(value);
}
return value;
};
@@ -62,6 +48,9 @@ Pattern.prototype.pianoroll = function ({
from = 0;
to = timeframeProp;
}
if (!autorange && fold) {
console.warn('disabling autorange has no effect when fold is enabled');
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
@@ -149,137 +138,10 @@ Pattern.prototype.pianoroll = function ({
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
foldValues = values.sort((a, b) => a - b);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
};
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
export function pianoroll({
time,
haps,
cycles = 4,
playhead = 0.5,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
ctx,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
// onQuery
const { min, max, values } = haps.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
return this;
}
+3 -6
View File
@@ -12,7 +12,6 @@ export function repl({
afterEval,
getTime,
transpiler,
editPattern,
onToggle,
}) {
const scheduler = new Cyclist({
@@ -41,13 +40,11 @@ export function repl({
throw new Error('no code to evaluate');
}
try {
beforeEval?.({ code });
let { pattern } = await _evaluate(code, transpiler);
beforeEval({ code });
const { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern });
afterEval({ code, pattern });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
+83 -49
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import { Pattern, fastcat, reify, silence, stack, isPattern } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id } from './util.mjs';
@@ -258,9 +258,9 @@ export const perlinWith = (pat) => {
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
);
Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x));
};
/**
* Randomly removes events from the pattern by a given amount.
@@ -276,9 +276,9 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
* @example
* s("[hh?0.2]*8")
*/
export const degradeBy = register('degradeBy', function (x, pat) {
return pat._degradeByWith(rand, x);
});
Pattern.prototype._degradeBy = function (x) {
return this._degradeByWith(rand, x);
};
/**
*
@@ -292,7 +292,9 @@ export const degradeBy = register('degradeBy', function (x, pat) {
* @example
* s("[hh?]*8")
*/
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
Pattern.prototype.degrade = function () {
return this._degradeBy(0.5);
};
/**
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
@@ -307,14 +309,26 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* @example
* s("hh*8").undegradeBy(0.2)
*/
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
Pattern.prototype._undegradeBy = function (x) {
return this._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
});
};
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
Pattern.prototype.undegrade = function () {
return this._undegradeBy(0.5);
};
Pattern.prototype._sometimesBy = function (x, func) {
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
};
// https://github.com/tidalcycles/strudel/discussions/198
/* Pattern.prototype._sometimesBy = function (x, other) {
other = typeof other === 'function' ? other(this._undegradeBy(1 - x)) : reify(other)._undegradeBy(1 - x);
return stack(this._degradeBy(x), other);
}; */
/**
*
@@ -329,12 +343,24 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
* @example
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
*/
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
Pattern.prototype.sometimesBy = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
.fmap((x) => pat._sometimesBy(x, func))
.innerJoin();
});
};
// why does this exist? it is identical to sometimesBy
Pattern.prototype._sometimesByPre = function (x, func) {
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
};
Pattern.prototype.sometimesByPre = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._sometimesByPre(x, func))
.innerJoin();
};
/**
*
@@ -347,9 +373,20 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
* @example
* s("hh*4").sometimes(x=>x.speed("0.5"))
*/
export const sometimes = register('sometimes', function (func, pat) {
return pat._sometimesBy(0.5, func);
});
Pattern.prototype.sometimes = function (func) {
return this._sometimesBy(0.5, func);
};
Pattern.prototype.sometimesPre = function (func) {
return this._sometimesByPre(0.5, func);
};
Pattern.prototype._someCyclesBy = function (x, func) {
return stack(
this._degradeByWith(rand._segment(1), x),
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
);
};
/**
*
@@ -364,17 +401,12 @@ export const sometimes = register('sometimes', function (func, pat) {
* @example
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
*/
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
Pattern.prototype.someCyclesBy = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) =>
stack(
pat._degradeByWith(rand._segment(1), x),
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
),
)
.fmap((x) => pat._someCyclesBy(x, func))
.innerJoin();
});
};
/**
*
@@ -386,9 +418,9 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @example
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
*/
export const someCycles = register('someCycles', function (func, pat) {
return pat._someCyclesBy(0.5, func);
});
Pattern.prototype.someCycles = function (func) {
return this._someCyclesBy(0.5, func);
};
/**
*
@@ -400,9 +432,9 @@ export const someCycles = register('someCycles', function (func, pat) {
* @example
* s("hh*8").often(x=>x.speed("0.5"))
*/
export const often = register('often', function (func, pat) {
return pat.sometimesBy(0.75, func);
});
Pattern.prototype.often = function (func) {
return this.sometimesBy(0.75, func);
};
/**
*
@@ -414,9 +446,9 @@ export const often = register('often', function (func, pat) {
* @example
* s("hh*8").rarely(x=>x.speed("0.5"))
*/
export const rarely = register('rarely', function (func, pat) {
return pat.sometimesBy(0.25, func);
});
Pattern.prototype.rarely = function (func) {
return this.sometimesBy(0.25, func);
};
/**
*
@@ -428,9 +460,9 @@ export const rarely = register('rarely', function (func, pat) {
* @example
* s("hh*8").almostNever(x=>x.speed("0.5"))
*/
export const almostNever = register('almostNever', function (func, pat) {
return pat.sometimesBy(0.1, func);
});
Pattern.prototype.almostNever = function (func) {
return this.sometimesBy(0.1, func);
};
/**
*
@@ -442,9 +474,9 @@ export const almostNever = register('almostNever', function (func, pat) {
* @example
* s("hh*8").almostAlways(x=>x.speed("0.5"))
*/
export const almostAlways = register('almostAlways', function (func, pat) {
return pat.sometimesBy(0.9, func);
});
Pattern.prototype.almostAlways = function (func) {
return this.sometimesBy(0.9, func);
};
/**
*
@@ -456,9 +488,9 @@ export const almostAlways = register('almostAlways', function (func, pat) {
* @example
* s("hh*8").never(x=>x.speed("0.5"))
*/
export const never = register('never', function (_, pat) {
return pat;
});
Pattern.prototype.never = function (func) {
return this;
};
/**
*
@@ -470,6 +502,8 @@ export const never = register('never', function (_, pat) {
* @example
* s("hh*8").always(x=>x.speed("0.5"))
*/
export const always = register('always', function (func, pat) {
return func(pat);
});
Pattern.prototype.always = function (func) {
return func(this);
};
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
+10 -6
View File
@@ -4,7 +4,7 @@ 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 { register } from './index.mjs';
import { Pattern, patternify2 } from './index.mjs';
let synth;
try {
@@ -16,7 +16,7 @@ try {
let allVoices = synth?.getVoices();
// console.log('voices', allVoices);
function triggerSpeech(words, lang, voice) {
function speak(words, lang, voice) {
synth.cancel();
const utterance = new SpeechSynthesisUtterance(words);
utterance.lang = lang;
@@ -31,8 +31,12 @@ function triggerSpeech(words, lang, voice) {
speechSynthesis.speak(utterance);
}
export const speak = register('speak', function (lang, voice, pat) {
return pat.onTrigger((_, hap) => {
triggerSpeech(hap.value, lang, voice);
Pattern.prototype._speak = function (lang, voice) {
return this.onTrigger((_, hap) => {
speak(hap.value, lang, voice);
});
});
};
Pattern.prototype.speak = function (lang, voice) {
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
};
+28 -64
View File
@@ -16,7 +16,6 @@ import {
pure,
stack,
fastcat,
firstOf,
slowcat,
cat,
sequence,
@@ -154,46 +153,35 @@ describe('Pattern', () => {
).toBe(7);
});
});
describe('out()', () => {
it('is an alias for set.out()', () => {
sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
});
});
describe('add()', () => {
it('works as toplevel function', () => {
expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9);
});
it('works as toplevel function, with bare values for arguments', () => {
expect(add(4, 5).query(st(0, 1))[0].value).toBe(9);
});
it('can structure In()', () => {
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
expect(pure(3).add.in(pure(4)).query(st(0, 1))[0].value).toBe(7);
expect(pure(3).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
});
it('can structure Out()', () => {
sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]);
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrig(20, 30).early(2),
sequence(26, 27, 36, 37),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrigzero(20, 30).early(2),
sequence(21, 22, 31, 32),
);
});
it('can Squeeze() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
sequence(
[11, 21, 31],
[
@@ -205,7 +193,7 @@ describe('Pattern', () => {
});
it('can SqueezeOut() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
sequence(1, [2, 3]).addSqueezeOut(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
);
});
@@ -216,32 +204,32 @@ describe('Pattern', () => {
describe('keep()', () => {
it('can structure In()', () => {
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
});
it('can structure Out()', () => {
sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrig(20, 30).early(2),
sequence(6, 7, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrigzero(20, 30).early(2),
sequence(1, 2, 1, 2),
);
});
it('can Squeeze() structure', () => {
sameFirst(
sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
sequence(1, [2, 3]).keepSqueeze(sequence(10, 20, 30)),
sequence(
[1, 1, 1],
[
@@ -252,38 +240,38 @@ describe('Pattern', () => {
);
});
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
describe('keepif()', () => {
it('can structure In()', () => {
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
});
it('can structure Out()', () => {
sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrig(false, true).early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrigzero(false, true).early(2),
sequence(silence, silence, 1, 2),
);
});
it('can Squeeze() structure', () => {
sameFirst(
sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
sequence(1, [2, 3]).keepifSqueeze(sequence(true, true, false)),
sequence(
[1, 1, silence],
[
@@ -294,7 +282,7 @@ describe('Pattern', () => {
);
});
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
describe('sub()', () => {
@@ -334,13 +322,13 @@ describe('Pattern', () => {
});
describe('setOut()', () => {
it('Can set things with structure from second pattern', () => {
sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
sameFirst(sequence(1, 2).setOut(4), pure(4).mask(true, true));
});
});
describe('setSqueeze()', () => {
it('Can squeeze one pattern inside the haps of another', () => {
sameFirst(
sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
sequence(
['a', 'b', 'c'],
[
@@ -350,7 +338,7 @@ describe('Pattern', () => {
),
);
sameFirst(
sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
sequence(
['a', 'b', 'c'],
[
@@ -418,8 +406,8 @@ describe('Pattern', () => {
);
});
it('defaults to accepting sequences', () => {
expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
);
});
it('works as a static function', () => {
@@ -598,16 +586,6 @@ describe('Pattern', () => {
.firstCycle(),
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
});
it('Works as a toplevel function', () => {
expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
});
it('Works as a toplevel function, with a patterned first argument', () => {
expect(firstOf(pure(3), fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
});
it('works with currying', () => {
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
@@ -917,23 +895,9 @@ describe('Pattern', () => {
});
describe('alignments', () => {
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
});
});
describe('defragmentHaps', () => {
it('Can merge two touching haps with same whole and value', () => {
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
});
it('Doesnt merge two overlapping haps', () => {
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
2,
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(
sequence(5, 6, 6, 7).firstCycle()
);
});
it('Doesnt merge two touching haps with different values', () => {
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
});
it('Doesnt merge two touching haps with different wholes', () => {
expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
});
});
});
+15 -26
View File
@@ -10,8 +10,7 @@ import {
tokenizeNote,
toMidi,
fromMidi,
freqToMidi,
_mod,
mod,
compose,
getFrequency,
getPlayableNoteValue,
@@ -33,12 +32,9 @@ describe('isNote', () => {
expect(isNote(note)).toBe(true);
});
});
it('should recognize notes without octave', () => {
expect(isNote('C')).toBe(true);
expect(isNote('Bb')).toBe(true);
});
it('should not recognize invalid notes', () => {
expect(isNote('H5')).toBe(false);
expect(isNote('C')).toBe(false);
expect(isNote('X')).toBe(false);
expect(isNote(1)).toBe(false);
});
@@ -98,12 +94,6 @@ describe('fromMidi', () => {
expect(fromMidi(57)).toEqual(220);
});
});
describe('freqToMidi', () => {
it('should turn frequency into midi', () => {
expect(freqToMidi(440)).toEqual(69);
expect(freqToMidi(220)).toEqual(57);
});
});
describe('getFrequency', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should turn note into frequency', () => {
@@ -128,22 +118,22 @@ describe('getFrequency', () => {
});
});
describe('_mod', () => {
describe('mod', () => {
it('should work like regular modulo with positive numbers', () => {
expect(_mod(0, 3)).toEqual(0);
expect(_mod(1, 3)).toEqual(1);
expect(_mod(2, 3)).toEqual(2);
expect(_mod(3, 3)).toEqual(0);
expect(_mod(4, 3)).toEqual(1);
expect(_mod(4, 2)).toEqual(0);
expect(mod(0, 3)).toEqual(0);
expect(mod(1, 3)).toEqual(1);
expect(mod(2, 3)).toEqual(2);
expect(mod(3, 3)).toEqual(0);
expect(mod(4, 3)).toEqual(1);
expect(mod(4, 2)).toEqual(0);
});
it('should work with negative numbers', () => {
expect(_mod(-1, 3)).toEqual(2);
expect(_mod(-2, 3)).toEqual(1);
expect(_mod(-3, 3)).toEqual(0);
expect(_mod(-4, 3)).toEqual(2);
expect(_mod(-5, 3)).toEqual(1);
expect(_mod(-3, 2)).toEqual(1);
expect(mod(-1, 3)).toEqual(2);
expect(mod(-2, 3)).toEqual(1);
expect(mod(-3, 3)).toEqual(0);
expect(mod(-4, 3)).toEqual(2);
expect(mod(-5, 3)).toEqual(1);
expect(mod(-3, 2)).toEqual(1);
});
});
@@ -192,7 +182,6 @@ describe('parseNumeral', () => {
expect(parseNumeral(1.5)).toBe(1.5);
});
it('should parse notes', () => {
expect(parseNumeral('c')).toBe(48);
expect(parseNumeral('c4')).toBe(60);
expect(parseNumeral('c#4')).toBe(61);
expect(parseNumeral('db4')).toBe(61);
-6
View File
@@ -6,8 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { describe, it, expect } from 'vitest';
import { map, valued, mul } from '../value.mjs';
import controls from '../controls.mjs';
const { n } = controls;
describe('Value', () => {
it('unionWith', () => {
@@ -23,8 +21,4 @@ describe('Value', () => {
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9);
});
it('union bare numbers for numeral props', () => {
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
});
});
+3 -3
View File
@@ -20,9 +20,9 @@ export class TimeSpan {
// Support zero-width timespans
if (begin.equals(end)) {
return [new TimeSpan(begin, end)];
return([new TimeSpan(begin, end)]);
}
while (end.gt(begin)) {
// If begin and end are in the same cycle, we're done.
if (begin.sam().equals(end_sam)) {
@@ -110,7 +110,7 @@ export class TimeSpan {
}
show() {
return this.begin.show() + ' ' + this.end.show();
return this.begin.show() + ' -> ' + this.end.show();
}
}
+7 -31
View File
@@ -5,8 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
@@ -20,7 +19,7 @@ export const tokenizeNote = (note) => {
// turns the given note into its midi number representation
export const toMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
const [pc, acc, oct] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
@@ -32,30 +31,6 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const freqToMidi = (freq) => {
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
};
export const valueToMidi = (value, fallbackValue) => {
if (typeof value !== 'object') {
throw new Error('valueToMidi: expected object value');
}
let { freq, note } = value;
if (typeof freq === 'number') {
return freqToMidi(freq);
}
if (typeof note === 'string') {
return toMidi(note);
}
if (typeof note === 'number') {
return note;
}
if (!fallbackValue) {
throw new Error('valueToMidi: expected freq or note to be set');
}
return fallbackValue;
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
*/
@@ -75,8 +50,9 @@ export const midi2note = (n) => {
return pc + oct;
};
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m;
export const getPlayableNoteValue = (hap) => {
let { value, context } = hap;
@@ -141,9 +117,9 @@ export const constant = (a, b) => a;
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
export function curry(func, overload, arity = func.length) {
export function curry(func, overload) {
const fn = function curried(...args) {
if (args.length >= arity) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
const partial = function (...args2) {
-7
View File
@@ -7,13 +7,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { curry } from './util.mjs';
export function unionWithObj(a, b, func) {
if (typeof b?.value === 'number') {
// https://github.com/tidalcycles/strudel/issues/262
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
b = Object.assign(b, numerals);
delete b.value;
}
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
}
+1 -1
View File
@@ -44,6 +44,6 @@ function createClock(
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
return { setDuration, start, stop, pause, duration, getPhase };
}
export default createClock;
-170
View File
@@ -1,170 +0,0 @@
import { getFrequency, logger, register } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import csd from './project.csd?raw';
// import livecodeOrc from './livecode.orc?raw';
import presetsOrc from './presets.orc?raw';
let csoundLoader, _csound;
// initializes csound + can be used to reevaluate given instrument code
export async function loadCSound(code = '') {
await init();
if (code) {
code = `${code}`;
// ^ ^
// wrapping in backticks makes sure it works when calling as templated function
await _csound?.evalCode(code);
}
}
export const loadcsound = loadCSound;
export const loadCsound = loadCSound;
export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((time, hap) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
}
if (typeof hap.value !== 'object') {
throw new Error('csound only support objects as hap values');
}
let { gain = 0.8 } = hap.value;
gain *= 0.2;
const freq = Math.round(getFrequency(hap));
const controls = Object.entries({ ...hap.value, freq })
.flat()
.join('/');
// TODO: find out how to send a precise ctx based time
// http://www.csounds.com/manual/html/i.html
const params = [
`"${instrument}"`, // p1: instrument name
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
hap.duration + 0, // p3: duration in beats
// instrument specific params:
freq, //.toFixed(precision), // p4: frequency
gain, // p5: gain
`"${controls}"`, // p6 controls as string (like superdirt osc message)
];
const msg = `i ${params.join(' ')}`;
_csound.inputMessage(msg);
});
});
function eventLogger(type, args) {
const [msg] = args;
if (
type === 'message' &&
(['[commit: HEAD]'].includes(msg) ||
msg.startsWith('--Csound version') ||
msg.startsWith('libsndfile') ||
msg.startsWith('sr =') ||
msg.startsWith('0dBFS') ||
msg.startsWith('audio buffered') ||
msg.startsWith('writing') ||
msg.startsWith('SECTION 1:'))
) {
// ignore
return;
}
let logType = 'info';
if (msg.startsWith('error:')) {
logType = 'error';
}
logger(`[csound] ${msg || ''}`, logType);
}
async function load() {
if (window.__csound__) {
// Allows using some other csound instance.
// In that case, the external Csound is responsible
// for compiling an orchestra and starting to perform.
logger('[load] Using external Csound', 'warning');
_csound = window.__csound__;
return _csound;
} else {
const { Csound } = await import('@csound/browser');
_csound = await Csound({ audioContext: getAudioContext() });
_csound.removeAllListeners('message');
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
await _csound.setOption('--sample-accurate');
await _csound.compileCsdText(csd);
// await _csound.compileOrc(livecodeOrc);
await _csound.compileOrc(presetsOrc);
await _csound.start();
return _csound;
}
}
async function init() {
csoundLoader = csoundLoader || load();
return csoundLoader;
}
let orcCache = {};
export async function loadOrc(url) {
await init();
if (typeof url !== 'string') {
throw new Error('loadOrc: expected url string');
}
if (url.startsWith('github:')) {
const [_, path] = url.split('github:');
url = `https://raw.githubusercontent.com/${path}`;
}
if (!orcCache[url]) {
orcCache[url] = fetch(url)
.then((res) => res.text())
.then((code) => _csound.compileOrc(code));
}
await orcCache[url];
}
/**
* Sends notes to Csound for rendering with MIDI semantics. The hap value is
* translated to these Csound pfields:
*
* p1 -- Csound instrument either as a number (1-based, can be a fraction),
* or as a string name.
* p2 -- time in beats (usually seconds) from start of performance.
* p3 -- duration in beats (usually seconds).
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
* p6 -- Strudel controls, as a string.
*/
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
if (typeof instrument === 'string') {
p1 = `"{instrument}"`;
}
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((tidal_time, hap) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
}
if (typeof hap.value !== 'object') {
throw new Error('csound only support objects as hap values');
}
// Time in seconds counting from now.
const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
// Translate frequency to MIDI key number _without_ rounding.
const C4 = 261.62558;
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
const p4 = octave * 12.0 - 36.0;
// We prefer floating point precision, but over the MIDI range [0, 127].
const p5 = 127 * (hap.context?.velocity ?? 0.9);
// The Strudel controls as a string.
const p6 = Object.entries({ ...hap.value, frequency })
.flat()
.join('/');
const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
console.log('[csoundm]:', i_statement);
_csound.inputMessage(i_statement);
});
});
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
{
"name": "@strudel.cycles/csound",
"version": "0.5.1",
"description": "csound bindings for strudel",
"main": "csound.mjs",
"scripts": {
"test": "echo \"No tests present.\" && exit 0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "^6.18.3"
}
}
-96
View File
@@ -1,96 +0,0 @@
; returns value of given key in given "string map"
; keymap("freq", "note/c3/freq/220/gain/0.5")
; yields "220"
opcode keymap, S, SS
Skey, Smap xin
idelimiter = strindex(Smap, strcat(Skey, "/"))
ifrom = idelimiter + strlen(Skey) + 1
Svalue = strsub(Smap, ifrom, strlen(Smap))
Svalue = strsub(Svalue, 0, strindex(Svalue, "/"))
xout Svalue
endop
; TODO add incredibly dope synths
instr organ
iduration = p3
ifreq = p4
igain = p5
ioct = octcps(ifreq)
asig = vco2(igain, ifreq, 12, .5) ; my edit
kpwm = oscili(.1, 5)
asig = vco2(igain, ifreq, 4, .5 + kpwm)
asig += vco2(igain/4, ifreq * 2)
; filter
; idepth = 2
; acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
; asig = zdf_2pole(asig, cpsoct(ioct + acut), 0.5)
; amp envelope
iattack = .001
irelease = .05
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
out(asig, asig)
endin
instr triangle
iduration = p3
ifreq = p4
igain = p5
ioct = octcps(ifreq)
asig = vco2(igain, ifreq, 12, .5)
; amp envelope
iattack = .001
irelease = .05
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
out(asig, asig)
endin
instr pad
iduration = p3
ifreq = p4
igain = p5
ioct = octcps(ifreq)
asig = vco2(igain, ifreq, 0)
; amp envelope
iattack = .5
irelease = .1
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
idepth = 2
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0)
asig = zdf_2pole(asig, 1000, 2)
out(asig, asig)
endin
gisine ftgen 0, 0, 4096, 10, 1
instr bow
kpres = 2
krat = 0.16
kvibf = 6.12723
kvib linseg 0, 0.5, 0, 1, 1, p3-0.5, 1
kvamp = kvib * 0.01
asig wgbow .7, p4, kpres, krat, kvibf, kvamp, gisine
asig = asig*p5
outs asig, asig
endin
instr Meta
Smap = strget(p6)
Sinstrument = keymap("s", Smap)
schedule(Sinstrument, 0, p3, p4, p5)
; TODO find a way to pipe Sinstrument through effects
endin
-10
View File
@@ -1,10 +0,0 @@
<CsoundSynthesizer>
<CsInstruments>
sr=48000
ksmps=64
nchnls=2
0dbfs=1
</CsInstruments>
</CsoundSynthesizer>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.5.0",
"version": "0.4.0",
"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.5.0",
"@strudel.cycles/core": "^0.4.0",
"estraverse": "^5.3.0",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
+3 -2
View File
@@ -225,8 +225,9 @@ function isPatternArg(parents) {
function hasModifierCall(parent) {
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
// need all prototypes of Pattern
return parent?.type === 'StaticMemberExpression';
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
return (
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
);
}
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
+20 -34
View File
@@ -11,14 +11,10 @@ import { getAudioContext } from '@strudel.cycles/webaudio';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected } = options;
if (!supportsMidi()) {
if (typeof navigator.requestMIDIAccess !== 'function') {
throw new Error('Your Browser does not support WebMIDI.');
}
return new Promise((resolve, reject) => {
@@ -46,43 +42,23 @@ export function enableWebMidi(options = {}) {
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
let midiReady;
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof output === 'number') {
return outputs[output];
}
if (typeof output === 'string') {
return outputByName(output);
}
return outputs[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output, channel = 1) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
/* await */ enableWebMidi({
Pattern.prototype.midi = async function (output, channel = 1) {
await enableWebMidi({
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const device = getDevice(output, outputs);
const chosenOutput = output ?? outputs[0];
const otherOutputs = outputs
.filter((o) => o.name !== device.name)
.filter((o) => o.name !== chosenOutput.name)
.map((o) => `'${o.name}'`)
.join(' | ');
midiReady = true;
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
});
if (isPattern(output)) {
if (isPattern(output?.constructor?.name)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
@@ -95,10 +71,20 @@ Pattern.prototype.midi = function (output, channel = 1) {
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
if (!midiReady) {
return;
if (!WebMidi.enabled) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
if (!WebMidi.outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
let device;
if (typeof output === 'number') {
device = WebMidi.outputs[output];
} else if (typeof output === 'string') {
device = outputByName(output);
} else {
device = WebMidi.outputs[0];
}
const device = getDevice(output, WebMidi.outputs);
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.5.0",
"version": "0.4.0",
"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.5.0",
"@strudel.cycles/tone": "^0.4.0",
"tone": "^14.7.77",
"webmidi": "^3.0.21"
}
+1 -1
View File
@@ -32,7 +32,7 @@ yields:
## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Building the Parser
File diff suppressed because it is too large Load Diff
+56 -80
View File
@@ -5,20 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th
*/
// Some terminology:
// mini(notation) = a series of elements placed between quotes
// a stack = a series of vertically aligned slices sharing the same overall length
// a sequence = a series of horizontally aligned elements
// a choose = a series of elements, one of which is chosen at random
// a sequence = a serie of elements placed between quotes
// a stack = a serie of vertically aligned slices sharing the same overall length
// a slice = a serie of horizontally aligned elements
// a choose = a serie of elements, one of which is chosen at random
{
var AtomStub = function(source)
{
this.type_ = "atom";
this.source_ = source;
this.location_ = location();
}
var PatternStub = function(source, alignment)
{
this.type_ = "pattern";
@@ -97,99 +90,82 @@ quote = '"' / "'"
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
step = ws chars:step_char+ ws { return chars.join("") }
// define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
// define a polymeter e.g. {1 2, 3 4 5}
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
polymeter_steps = "%"a:slice
{ return a }
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
// change the alignment to slowcat
slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'slowcat'; return s; }
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
timeline = ws "<" ws sc:single_cycle ws ">" ws
{ sc.arguments_.alignment = "t"; return sc;}
// a slice is either a single step or a sub cycle
slice = step / sub_cycle / polymeter / slow_sequence
slice = step / sub_cycle / timeline
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
slice_weight = "@" a:number
{ return { weight: a} }
op_replicate = "!"a:number
{ return x => x.options_['reps'] = a }
slice_replicate = "!"a:number
{ return { replicate: a } }
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
op_slow = "/"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }
slice_slow = "/"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
slice_fast = "*"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
slice_fixed_step = "%"a:number
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
slice_degrade = "?"a:number?
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
}
slice_with_modifier = s:slice o:slice_modifier?
{ return new ElementStub(s, o);}
// a sequence is a combination of one or more successive slices (as an array)
sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat'); }
// a single cycle is a combination of one or more successive slices (as an array). If we
// have only one element, we skip the array and return the element itself
single_cycle = s:(slice_with_modifier)+
{ return new PatternStub(s,"h"); }
// a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+
{ return { alignment: 'stack', list: tail }; }
// a stack is a serie of vertically aligned single cycles, separated by a comma
stack_tail = tail:(comma @single_cycle)+
{ return { alignment: 'v', list: tail }; }
// a choose is a series of pipe-separated sequence, one of which is
// chosen at random, each cycle
choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail }; }
// a choose is a serie of pipe-separated single cycles, one of which is chosen
// at random each time through the pattern
choose_tail = tail:(pipe @single_cycle)+
{ return { alignment: 'r', list: tail }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
// Mini-notation innards ends
// ---------->8---------->8---------->8---------->8---------->8----------
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
// a sequence is a quoted stack
sequence = ws quote sc:stack_or_choose quote
{ return sc; }
// ------------------ operators ---------------------------
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
struct = "struct" ws s:mini_or_operator
{ return { name: "struct", args: { mini:s }}}
struct = "struct" ws s:sequence_or_operator
{ return { name: "struct", args: { sequence:s }}}
target = "target" ws quote s:step quote
{ return { name: "target", args : { name:s}}}
bjorklund = "euclid" ws p:int ws s:int ws r:int?
{ return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
slow = "slow" ws a:number
{ return { name: "stretch", args :{ amount: a}}}
@@ -213,27 +189,27 @@ comment = '//' p:([^\n]*)
group_operator = cat
// cat is another form of timeline
cat = "cat" ws "[" ws s:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss,"t"); }
// ------------------ high level mini ---------------------------
// ------------------ high level sequence ---------------------------
mini_or_group =
sequence_or_group =
group_operator /
mini
sequence
mini_or_operator =
sg:mini_or_group ws (comment)*
sequence_or_operator =
sg:sequence_or_group ws (comment)*
{return sg}
/ o:operator ws "$" ws soc:mini_or_operator
/ o:operator ws "$" ws soc:sequence_or_operator
{ return new OperatorStub(o.name,o.args,soc)}
sequ_or_operator_or_comment =
sc: mini_or_operator
sc: sequence_or_operator
{ return sc }
/ comment
mini_definition = s:sequ_or_operator_or_comment
sequence_definition = s:sequ_or_operator_or_comment
// ---------------------- statements ----------------------------
@@ -251,4 +227,4 @@ hush = "hush"
// ---------------------- statements ----------------------------
statement = mini_definition / command
statement = sequence_definition / command
+104 -127
View File
@@ -6,158 +6,131 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
/* var _seedState = 0;
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
} */
}
const applyOptions = (parent, code) => (pat, i) => {
const applyOptions = (parent) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
case 'stretch': {
const legalTypes = ['fast', 'slow'];
const { type, amount } = op.arguments_;
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(
patternifyAST(op.arguments_.pulse, code),
patternifyAST(op.arguments_.step, code),
patternifyAST(op.arguments_.rotation, code),
);
} else {
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
}
break;
}
case 'degradeBy': {
// TODO: find out what is right here
// example:
/*
stack(
s("hh*8").degrade(),
s("[ht*8]?")
)
*/
// above example will only be in sync when _degradeBy is used...
// it also seems that the nextSeed will create undeterministic behaviour
// as it uses a global _seedState. This is probably the reason for
// https://github.com/tidalcycles/strudel/issues/245
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
const operator = options?.operator;
if (operator) {
switch (operator.type_) {
case 'stretch':
const speed = Fraction(operator.arguments_.amount).inverse();
return reify(pat).fast(speed);
case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy':
return reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
op.arguments_.amount ?? 0.5,
);
*/
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
}
operator.arguments_.amount,
);
// TODO: case 'fixed-step': "%"
}
console.warn(`operator "${operator.type_}" not implemented`);
}
if (options?.weight) {
// weight is handled by parent
return pat;
}
// TODO: bjorklund e.g. "c3(5,8)"
const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator');
if (unimplemented.length) {
console.warn(
`option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented`,
);
}
return pat;
};
function resolveReplications(ast) {
ast.source_ = strudel.flatten(
ast.source_.map((child) => {
const { reps } = child.options_ || {};
if (!reps) {
return [child];
}
delete child.options_.reps;
return Array(reps).fill(child);
}),
);
// the general idea here: x!3 = [x*3]@3
// could this be made easier?!
ast.source_ = ast.source_.map((child) => {
const { replicate, ...options } = child.options_ || {};
if (replicate) {
return {
...child,
options_: { ...options, weight: replicate },
source_: {
type_: 'pattern',
arguments_: {
alignment: 'h',
},
source_: [
{
type_: 'element',
source_: child.source_,
location_: child.location_,
options_: {
operator: {
type_: 'stretch',
arguments_: { amount: Fraction(replicate).inverse().toString() },
},
},
},
],
},
};
}
return child;
});
}
export function patternifyAST(ast, code) {
export function patternifyAST(ast) {
switch (ast.type_) {
case 'pattern': {
case 'pattern':
resolveReplications(ast);
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
const children = ast.source_.map(patternifyAST).map(applyOptions(ast));
const alignment = ast.arguments_.alignment;
if (alignment === 'stack') {
return strudel.stack(...children);
if (alignment === 'v') {
return stack(...children);
}
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
return strudel.stack(...aligned);
}
if (alignment === 'rand') {
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children);
if (alignment === 'r') {
return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
return strudel.slowcat(...children);
if (!weightedChildren && alignment === 't') {
return slowcat(...children);
}
if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 'slowcat') {
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 't') {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
return pat._slow(weightSum); // timecat + slow
}
pat.__weight = weightSum;
return pat;
}
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat;
}
case 'element': {
return patternifyAST(ast.source_, code);
}
case 'atom': {
return sequence(...children);
case 'element':
if (ast.source_ === '~') {
return strudel.silence;
return silence;
}
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
if (typeof ast.source_ !== 'object') {
/* if (!addMiniLocations) {
return ast.source_;
} */
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
return pure(value).withLocation([start.line, start.column, start.offset], [end.line, end.column, end.offset]);
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
// make sure whitespaces are not part of the highlight:
const actual = code?.split('').slice(start.offset, end.offset).join('');
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return strudel
.pure(value)
.withLocation(
[start.line, start.column + offsetStart, start.offset + offsetStart],
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
);
}
return patternifyAST(ast.source_);
case 'stretch':
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
/* case 'scale':
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
const intervals = Scale.get(scale).intervals;
@@ -179,33 +152,37 @@ export function patternifyAST(ast, code) {
}); */
/* case 'struct':
// TODO:
return strudel.silence; */
return silence; */
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return strudel.silence;
return silence;
}
}
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const code = `"${str}"`;
const ast = krill.parse(code);
return patternifyAST(ast, code);
const ast = krill.parse(`"${str}"`);
return patternifyAST(ast);
});
return strudel.sequence(...pats);
return sequence(...pats);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = krill.parse(string);
// console.log('ast', ast);
return patternifyAST(ast, string);
return patternifyAST(ast);
};
// shorthand for mini
Pattern.prototype.define('mini', mini, { composable: true });
Pattern.prototype.define('m', mini, { composable: true });
Pattern.prototype.define('h', h, { composable: true });
export function minify(thing) {
if (typeof thing === 'string') {
return mini(thing);
}
return strudel.reify(thing);
return reify(thing);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.5.0",
"version": "0.4.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -26,9 +26,9 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/eval": "^0.5.0",
"@strudel.cycles/tone": "^0.5.0"
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/eval": "^0.4.0",
"@strudel.cycles/tone": "^0.4.0"
},
"devDependencies": {
"peggy": "^2.0.1"
+4 -67
View File
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
const minV = (v) => mini(v).sortHapsByPart().firstCycleValues;
const minS = (v) => mini(v).sortHapsByPart().showFirstCycle;
const minV = (v) => mini(v).firstCycleValues;
const minS = (v) => mini(v).showFirstCycle;
it('supports single elements', () => {
expect(minV('a')).toEqual(['a']);
});
@@ -21,21 +21,6 @@ describe('mini', () => {
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
});
it('supports fast', () => {
expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
});
it('supports patterned fast', () => {
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
});
it('supports slow', () => {
expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
});
it('supports patterned slow', () => {
expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
});
it('supports patterned fast', () => {
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
});
it('supports slowcat', () => {
expect(minV('<a b>')).toEqual(['a']);
});
@@ -51,16 +36,6 @@ describe('mini', () => {
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
});
it('supports curly brackets', () => {
expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
});
it('supports curly brackets with explicit step-per-cycle', () => {
expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
});
it('supports commas', () => {
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
@@ -71,48 +46,10 @@ describe('mini', () => {
});
it('supports replication', () => {
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
});
it('supports euclidean rhythms', () => {
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
});
it('supports patterning euclidean rhythms', () => {
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
});
it("reproduces Toussaint's example euclidean algorithms", () => {
const checkEuclid = function (spec, target) {
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
};
checkEuclid([1, 2], 'x ~');
checkEuclid([1, 3], 'x ~ ~');
checkEuclid([1, 4], 'x ~ ~ ~');
checkEuclid([4, 12], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~');
checkEuclid([2, 5], 'x ~ x ~ ~');
// checkEuclid([3, 4], "x ~ x x"); // Toussaint is wrong..
checkEuclid([3, 4], 'x x x ~'); // correction
checkEuclid([3, 5], 'x ~ x ~ x');
checkEuclid([3, 7], 'x ~ x ~ x ~ ~');
checkEuclid([3, 8], 'x ~ ~ x ~ ~ x ~');
checkEuclid([4, 7], 'x ~ x ~ x ~ x');
checkEuclid([4, 9], 'x ~ x ~ x ~ x ~ ~');
checkEuclid([4, 11], 'x ~ ~ x ~ ~ x ~ ~ x ~');
// checkEuclid([5, 6], "x ~ x x x x"); // Toussaint is wrong..
checkEuclid([5, 6], 'x x x x x ~'); // correction
checkEuclid([5, 7], 'x ~ x x ~ x x');
checkEuclid([5, 8], 'x ~ x x ~ x x ~');
checkEuclid([5, 9], 'x ~ x ~ x ~ x ~ x');
checkEuclid([5, 11], 'x ~ x ~ x ~ x ~ x ~ ~');
checkEuclid([5, 12], 'x ~ ~ x ~ x ~ ~ x ~ x ~');
// checkEuclid([5, 16], "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~ ~"); // Toussaint is wrong..
checkEuclid([5, 16], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~'); // correction
// checkEuclid([7, 8], "x ~ x x x x x x"); // Toussaint is wrong..
checkEuclid([7, 8], 'x x x x x x x ~'); // Correction
checkEuclid([7, 12], 'x ~ x x ~ x ~ x x ~ x ~');
checkEuclid([7, 16], 'x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~');
checkEuclid([9, 16], 'x ~ x x ~ x ~ x ~ x x ~ x ~ x ~');
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
});
it('supports the ? operator', () => {
expect(
mini('a?')
@@ -140,7 +77,7 @@ describe('mini', () => {
expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8');
});
/*it('supports the random choice operator ("|") with nesting', () => {
it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
@@ -166,5 +103,5 @@ describe('mini', () => {
// PRNG, this test should succeed
expect(chisq <= 15.086).toBe(true);
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
});*/
});
});
+1 -1
View File
@@ -36,4 +36,4 @@ s("<bd sd> hh").osc()
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/outputs#superdirt-api)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.4.0",
"version": "0.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.4.0",
"version": "0.3.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
+2
View File
@@ -25,6 +25,8 @@ const config = {
},
};
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
osc.open(); // start a WebSocket server on port 8080
+1 -1
View File
File diff suppressed because one or more lines are too long
+241 -322
View File
@@ -1,15 +1,15 @@
import l, { useCallback as N, useRef as k, useEffect as _, useMemo as K, useState as w, useLayoutEffect as G } from "react";
import Z from "@uiw/react-codemirror";
import { Decoration as y, EditorView as J } from "@codemirror/view";
import { StateEffect as Q, StateField as X } from "@codemirror/state";
import { javascript as ee } from "@codemirror/lang-javascript";
import { tags as s } from "@lezer/highlight";
import { createTheme as te } from "@uiw/codemirror-themes";
import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core";
import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio";
import { useInView as se } from "react-hook-inview";
import { transpiler as ie } from "@strudel.cycles/transpiler";
const le = te({
import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react";
import X from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core";
import { transpiler as ne } from "@strudel.cycles/transpiler";
const ae = Z({
theme: "dark",
settings: {
background: "#222",
@@ -22,380 +22,299 @@ const le = te({
gutterForeground: "#8a919966"
},
styles: [
{ tag: s.keyword, color: "#c792ea" },
{ tag: s.operator, color: "#89ddff" },
{ tag: s.special(s.variableName), color: "#eeffff" },
{ tag: s.typeName, color: "#c3e88d" },
{ tag: s.atom, color: "#f78c6c" },
{ tag: s.number, color: "#c3e88d" },
{ tag: s.definition(s.variableName), color: "#82aaff" },
{ tag: s.string, color: "#c3e88d" },
{ tag: s.special(s.string), color: "#c3e88d" },
{ tag: s.comment, color: "#7d8799" },
{ tag: s.variableName, color: "#c792ea" },
{ tag: s.tagName, color: "#c3e88d" },
{ tag: s.bracket, color: "#525154" },
{ tag: s.meta, color: "#ffcb6b" },
{ tag: s.attributeName, color: "#c792ea" },
{ tag: s.propertyName, color: "#c792ea" },
{ tag: s.className, color: "#decb6b" },
{ tag: s.invalid, color: "#ffffff" }
{ tag: r.keyword, color: "#c792ea" },
{ tag: r.operator, color: "#89ddff" },
{ tag: r.special(r.variableName), color: "#eeffff" },
{ tag: r.typeName, color: "#c3e88d" },
{ tag: r.atom, color: "#f78c6c" },
{ tag: r.number, color: "#c3e88d" },
{ tag: r.definition(r.variableName), color: "#82aaff" },
{ tag: r.string, color: "#c3e88d" },
{ tag: r.special(r.string), color: "#c3e88d" },
{ tag: r.comment, color: "#7d8799" },
{ tag: r.variableName, color: "#c792ea" },
{ tag: r.tagName, color: "#c3e88d" },
{ tag: r.bracket, color: "#525154" },
{ tag: r.meta, color: "#ffcb6b" },
{ tag: r.attributeName, color: "#c792ea" },
{ tag: r.propertyName, color: "#c792ea" },
{ tag: r.className, color: "#decb6b" },
{ tag: r.invalid, color: "#ffffff" }
]
});
const j = Q.define(), ue = X.define({
const B = $.define(), se = G.define({
create() {
return y.none;
return E.none;
},
update(e, r) {
update(e, t) {
try {
for (let t of r.effects)
if (t.is(j))
if (t.value) {
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = y.set([a.range(0, r.newDoc.length)]);
for (let o of t.effects)
if (o.is(B))
if (o.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]);
} else
e = y.set([]);
e = E.set([]);
return e;
} catch (t) {
return console.warn("flash error", t), e;
} catch (o) {
return console.warn("flash error", o), e;
}
},
provide: (e) => J.decorations.from(e)
}), de = (e) => {
e.dispatch({ effects: j.of(!0) }), setTimeout(() => {
e.dispatch({ effects: j.of(!1) });
provide: (e) => U.decorations.from(e)
}), ce = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) });
}, 200);
}, z = Q.define(), fe = X.define({
}, z = $.define(), ie = G.define({
create() {
return y.none;
return E.none;
},
update(e, r) {
update(e, t) {
try {
for (let t of r.effects)
if (t.is(z)) {
const a = t.value.map(
(o) => (o.context.locations || []).map(({ start: i, end: u }) => {
const m = o.context.color || "#FFCA28";
let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column;
const v = r.newDoc.length;
return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d);
for (let o of t.effects)
if (o.is(z)) {
const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: f, end: d }) => {
const u = s.context.color || "#FFCA28";
let c = t.newDoc.line(f.line).from + f.column, i = t.newDoc.line(d.line).from + d.column;
const m = t.newDoc.length;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${u};` } }).range(c, i);
})
).flat().filter(Boolean) || [];
e = y.set(a, !0);
e = E.set(a, !0);
}
return e;
} catch {
return y.set([]);
return E.set([]);
}
},
provide: (e) => J.decorations.from(e)
}), me = [ee(), le, fe, ue];
function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) {
const u = N(
(d) => {
r?.(d);
},
[r]
), m = N(
(d) => {
t?.(d);
provide: (e) => U.decorations.from(e)
}), le = [Y(), ae, ie, se];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: f }) {
const d = _(
(i) => {
t?.(i);
},
[t]
), n = N(
(d) => {
d.selectionSet && a && a?.(d.state.selection);
), u = _(
(i) => {
o?.(i);
},
[o]
), c = _(
(i) => {
i.selectionSet && a && a?.(i.state.selection);
},
[a]
);
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, {
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, {
value: e,
onChange: u,
onCreateEditor: m,
onUpdate: n,
extensions: me
onChange: d,
onCreateEditor: u,
onUpdate: c,
extensions: le
}));
}
function W(...e) {
function K(...e) {
return e.filter(Boolean).join(" ");
}
function pe({ view: e, pattern: r, active: t, getTime: a }) {
const o = k([]), i = k();
_(() => {
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), f = H();
L(() => {
if (e)
if (r && t) {
let u = requestAnimationFrame(function m() {
if (t && o) {
let u = function() {
try {
const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60];
i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n);
const h = r.queryArc(...v).filter((p) => p.hasOnset());
o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) });
const c = a(), m = [Math.max(f.current || c, c - 1 / 10, 0), c + 1 / 60];
f.current = m[1], s.current = s.current.filter((g) => g.whole.end > c);
const h = t.queryArc(...m).filter((g) => g.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) });
} catch {
e.dispatch({ effects: z.of([]) });
}
u = requestAnimationFrame(m);
});
d = requestAnimationFrame(u);
}, d = requestAnimationFrame(u);
return () => {
cancelAnimationFrame(u);
cancelAnimationFrame(d);
};
} else
o.current = [], e.dispatch({ effects: z.of([]) });
}, [r, t, e]);
s.current = [], e.dispatch({ effects: z.of([]) });
}, [t, o, e]);
}
function he(e, r = !1) {
const t = k(), a = k(), o = (m) => {
if (a.current !== void 0) {
const n = m - a.current;
e(m, n);
}
a.current = m, t.current = requestAnimationFrame(o);
}, i = () => {
t.current = requestAnimationFrame(o);
}, u = () => {
t.current && cancelAnimationFrame(t.current), delete t.current;
};
return _(() => {
t.current && (u(), i());
}, [e]), _(() => (r && i(), u), []), {
start: i,
stop: u
};
}
function ve({ pattern: e, started: r, getTime: t, onDraw: a }) {
let o = k([]), i = k(null);
const { start: u, stop: m } = he(
N(() => {
const n = t();
if (i.current === null) {
i.current = n;
return;
}
const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4;
i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current);
}, [e])
);
_(() => {
r ? u() : (o.current = [], m());
}, [r]);
}
function be(e) {
return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []);
}
function Ee({
defaultOutput: e,
interval: r,
getTime: t,
evalOnMount: a = !1,
initialCode: o = "",
autolink: i = !1,
beforeEval: u,
afterEval: m,
editPattern: n,
onEvalError: d,
onToggle: v,
canvasId: h
}) {
const p = K(() => we(), []);
h = h || `canvas-${p}`;
const [F, A] = w(), [P, D] = w(), [b, q] = w(o), [x, V] = w(), [I, R] = w(), [L, B] = w(!1), H = b !== x, { scheduler: M, evaluate: c, start: f, stop: C, pause: O } = K(
() => re({
interval: r,
defaultOutput: e,
onSchedulerError: A,
onEvalError: (g) => {
D(g), d?.(g);
},
getTime: t,
transpiler: ie,
beforeEval: ({ code: g }) => {
q(g), u?.();
},
editPattern: n ? (g) => n(g, p) : void 0,
afterEval: ({ pattern: g, code: T }) => {
V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.();
},
onToggle: (g) => {
B(g), v?.(g);
}
}),
[e, r, t]
), Y = be(({ data: { from: g, type: T } }) => {
T === "start" && g !== p && C();
}), S = N(
async (g = !0) => {
await c(b, g), Y({ type: "start", from: p });
},
[c, b]
), U = k();
return _(() => {
!U.current && a && b && (U.current = !0, S());
}, [S, a, b]), _(() => () => {
M.stop();
}, [M]), {
id: p,
canvasId: h,
code: b,
setCode: q,
error: F || P,
schedulerError: F,
scheduler: M,
evalError: P,
evaluate: c,
activateCode: S,
activeCode: x,
isDirty: H,
pattern: I,
started: L,
start: f,
stop: C,
pause: O,
togglePlay: async () => {
L ? M.pause() : await S();
}
};
}
function we() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
function $({ type: e }) {
return /* @__PURE__ */ l.createElement("svg", {
const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = {
container: fe,
header: me,
buttons: ge,
button: pe,
buttonDisabled: he,
error: be,
body: ve
};
function O({ type: e }) {
return /* @__PURE__ */ n.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20",
fill: "currentColor"
}, {
refresh: /* @__PURE__ */ l.createElement("path", {
refresh: /* @__PURE__ */ n.createElement("path", {
fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd"
}),
play: /* @__PURE__ */ l.createElement("path", {
play: /* @__PURE__ */ n.createElement("path", {
fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd"
}),
pause: /* @__PURE__ */ l.createElement("path", {
pause: /* @__PURE__ */ n.createElement("path", {
fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd"
})
}[e]);
}
const ye = "_container_3i85k_1", ke = "_header_3i85k_5", _e = "_buttons_3i85k_9", Fe = "_button_3i85k_9", Ce = "_buttonDisabled_3i85k_17", Ne = "_error_3i85k_21", xe = "_body_3i85k_25", E = {
container: ye,
header: ke,
buttons: _e,
button: Fe,
buttonDisabled: Ce,
error: Ne,
body: xe
}, Me = () => ce().currentTime;
function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) {
const {
code: i,
setCode: u,
evaluate: m,
activateCode: n,
error: d,
isDirty: v,
activeCode: h,
pattern: p,
started: F,
function Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []);
}
function we({
defaultOutput: e,
interval: t,
getTime: o,
evalOnMount: a = !1,
initialCode: s = "",
autolink: f = !1,
beforeEval: d,
afterEval: u,
onEvalError: c,
onToggle: i
}) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), f && (window.location.hash = "#" + encodeURIComponent(btoa(P))), u?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
togglePlay: P,
stop: D,
canvasId: b,
id: q
} = Ee({
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const {
code: s,
setCode: f,
evaluate: d,
activateCode: u,
error: c,
isDirty: i,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
initialCode: e,
defaultOutput: ae,
getTime: Me,
editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f }))
});
ve({
pattern: p,
started: a && F,
getTime: () => A.now(),
onDraw: (c, f) => {
const C = document.querySelector("#" + b).getContext("2d");
oe({ ctx: C, time: c, haps: f, autorange: 1, fold: 1, playhead: 1 });
}
});
const [x, V] = w(), [I, R] = se({
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
threshold: 0.01
}), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]);
pe({
view: x,
pattern: p,
active: F && !h?.includes("strudel disable-highlighting"),
getTime: () => A.getPhase()
}), G(() => {
if (t) {
const c = async (f) => {
(f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault()));
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]);
return ue({
view: y,
pattern: h,
active: g && !m?.includes("strudel disable-highlighting"),
getTime: () => C.getPhase()
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await u()) : b.code === "Period" && (p(), b.preventDefault()));
};
return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0);
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
}
}, [t, p, i, m, D, x]);
const [H, M] = w([]);
return Ae(
N((c) => {
const { data: f } = c.detail;
f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10));
}, [])
), /* @__PURE__ */ l.createElement("div", {
className: E.container,
ref: I
}, /* @__PURE__ */ l.createElement("div", {
className: E.header
}, /* @__PURE__ */ l.createElement("div", {
className: E.buttons
}, /* @__PURE__ */ l.createElement("button", {
className: W(E.button, F ? "sc-animate-pulse" : ""),
onClick: () => P()
}, /* @__PURE__ */ l.createElement($, {
type: F ? "pause" : "play"
})), /* @__PURE__ */ l.createElement("button", {
className: W(v ? E.button : E.buttonDisabled),
onClick: () => n()
}, /* @__PURE__ */ l.createElement($, {
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", {
className: v.container,
ref: S
}, /* @__PURE__ */ n.createElement("div", {
className: v.header
}, /* @__PURE__ */ n.createElement("div", {
className: v.buttons
}, /* @__PURE__ */ n.createElement("button", {
className: K(v.button, g ? "sc-animate-pulse" : ""),
onClick: () => N()
}, /* @__PURE__ */ n.createElement(O, {
type: g ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", {
className: K(i ? v.button : v.buttonDisabled),
onClick: () => u()
}, /* @__PURE__ */ n.createElement(O, {
type: "refresh"
}))), d && /* @__PURE__ */ l.createElement("div", {
className: E.error
}, d.message)), /* @__PURE__ */ l.createElement("div", {
className: E.body
}, B && /* @__PURE__ */ l.createElement(ge, {
value: i,
onChange: u,
onViewChanged: V
})), a && /* @__PURE__ */ l.createElement("canvas", {
id: b,
className: "w-full pointer-events-none",
height: o,
ref: (c) => {
c && c.width !== c.clientWidth && (c.width = c.clientWidth);
}
}), !!H.length && /* @__PURE__ */ l.createElement("div", {
className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
}, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", {
key: f
}, c))));
}))), c && /* @__PURE__ */ n.createElement("div", {
className: v.error
}, c.message)), /* @__PURE__ */ n.createElement("div", {
className: v.body
}, F && /* @__PURE__ */ n.createElement(de, {
value: s,
onChange: f,
onViewChanged: M
})));
}
function Ae(e) {
De(ne.key, e);
}
function De(e, r, t = !1) {
_(() => (document.addEventListener(e, r, t), () => {
document.removeEventListener(e, r, t);
}), [r]);
}
const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export {
ge as CodeMirror,
je as MiniRepl,
W as cx,
de as flash,
pe as useHighlighting,
Ue as useKeydown,
be as usePostMessage,
Ee as useStrudel
de as CodeMirror,
Se as MiniRepl,
K as cx,
ce as flash,
ue as useHighlighting,
Te as useKeydown,
Ee as usePostMessage,
we as useStrudel
};
+1 -1
View File
@@ -1 +1 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.sc-rounded-md{border-radius:.375rem}.sc-bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.sc-p-2{padding:.5rem}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
plugins: [react()]
})
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> -->
<!-- <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel React Components</title>
</head>
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.5.0",
"version": "0.4.1",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -39,10 +39,10 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/tone": "^0.5.0",
"@strudel.cycles/transpiler": "^0.5.0",
"@strudel.cycles/webaudio": "^0.5.0",
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/tone": "^0.4.0",
"@strudel.cycles/transpiler": "^0.4.0",
"@strudel.cycles/webaudio": "^0.4.1",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0"
+1 -1
View File
@@ -9,4 +9,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
};
}
+7 -74
View File
@@ -1,21 +1,18 @@
import { pianoroll } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import usePatternFrame from '../hooks/usePatternFrame.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import 'tailwindcss/tailwind.css';
import './style.css';
import { logger } from '@strudel.cycles/core';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import useStrudel from '../hooks/useStrudel.mjs';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
const {
code,
setCode,
@@ -29,27 +26,11 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
scheduler,
togglePlay,
stop,
canvasId,
id: replId,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
getTime,
editPattern: (pat, id) => {
return pat.withContext((ctx) => ({ ...ctx, id }));
},
});
usePatternFrame({
pattern,
started: withCanvas && started,
getTime: () => scheduler.now(),
onDraw: (time, haps) => {
const ctx = document.querySelector('#' + canvasId).getContext('2d');
pianoroll({ ctx, time, haps, autorange: 1, fold: 1, playhead: 1 });
},
});
/* useEffect(() => {
init && activateCode();
}, [init, activateCode]); */
@@ -91,20 +72,6 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
const { data } = e.detail;
const logId = data?.hap?.context?.id;
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
});
}
}, []),
);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
@@ -121,40 +88,6 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
{withCanvas && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
{!!log.length && (
<div className="sc-bg-gray-800 sc-rounded-md sc-p-2">
{log.map(({ message }, i) => (
<div key={i}>{message}</div>
))}
</div>
)}
</div>
);
}
// TODO: dedupe
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
// TODO: dedupe
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
-43
View File
@@ -1,43 +0,0 @@
import { useEffect, useRef } from 'react';
function useFrame(callback, autostart = false) {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(time, deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
const start = () => {
requestRef.current = requestAnimationFrame(animate);
};
const stop = () => {
requestRef.current && cancelAnimationFrame(requestRef.current);
delete requestRef.current;
};
useEffect(() => {
if (requestRef.current) {
stop();
start();
}
}, [callback]);
useEffect(() => {
if (autostart) {
start();
}
return stop;
}, []); // Make sure the effect only runs once
return {
start,
stop,
};
}
export default useFrame;
+5 -2
View File
@@ -7,7 +7,9 @@ function useHighlighting({ view, pattern, active, getTime }) {
useEffect(() => {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(function updateHighlights() {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
try {
const audioTime = getTime();
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
@@ -23,7 +25,8 @@ function useHighlighting({ view, pattern, active, getTime }) {
view.dispatch({ effects: setHighlights.of([]) });
}
frame = requestAnimationFrame(updateHighlights);
});
}
return () => {
cancelAnimationFrame(frame);
};
@@ -1,34 +0,0 @@
import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw }) {
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime();
if (lastFrame.current === null) {
lastFrame.current = phase;
return;
}
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
const cycles = 4;
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end > phase - cycles) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(phase, visibleHaps.current);
}, [pattern]),
);
useEffect(() => {
if (started) {
startFrame();
} else {
visibleHaps.current = [];
stopFrame();
}
}, [started]);
}
export default usePatternFrame;
-6
View File
@@ -12,13 +12,10 @@ function useStrudel({
autolink = false,
beforeEval,
afterEval,
editPattern,
onEvalError,
onToggle,
canvasId,
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
@@ -45,7 +42,6 @@ function useStrudel({
setCode(code);
beforeEval?.();
},
editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined,
afterEval: ({ pattern: _pattern, code }) => {
setActiveCode(code);
setPattern(_pattern);
@@ -101,8 +97,6 @@ function useStrudel({
};
const error = schedulerError || evalError;
return {
id,
canvasId,
code,
setCode,
error,
+4 -9
View File
@@ -1,10 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.3.0",
"version": "0.2.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
+17 -61
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, isPattern } from '@strudel.cycles/core';
var writeMessage;
var serialWriter;
var choosing = false;
export async function getWriter(br = 38400) {
@@ -14,29 +14,17 @@ export async function getWriter(br = 38400) {
return;
}
choosing = true;
if (writeMessage) {
return writeMessage;
if (serialWriter) {
return serialWriter;
}
if ('serial' in navigator) {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: br });
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
writeMessage = function (message, chk) {
const encoded = encoder.encode(message);
if (!chk) {
writer.write(encoded);
} else {
const bytes = new Uint8Array(4);
bytes[0] = 124; // | symbol
bytes[1] = (chk >> 8) & 0xff;
bytes[2] = chk & 0xff;
bytes[3] = 59; // semicolon
const withchk = new Uint8Array(encoded.length + 4);
withchk.set(encoded);
withchk.set(bytes, encoded.length);
writer.write(withchk);
}
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
serialWriter = function (message) {
writer.write(message);
};
} else {
throw 'Webserial is not available in this browser.';
@@ -45,41 +33,18 @@ export async function getWriter(br = 38400) {
const latency = 0.1;
// crc16 (CCITT-FALSE) https://gist.github.com/tijnkooijmans/10981093
function crc16(data) {
const length = data.length;
if (length == 0) {
return 0;
}
var crc = 0xffff;
for (var i = 0; i < length; ++i) {
crc ^= data.charCodeAt(i) << 8;
for (var j = 0; j < 8; ++j) {
crc = (crc & 0x8000) > 0 ? (crc << 1) ^ 0x1021 : crc << 1;
}
}
return crc & 0xffff;
}
Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids = false) {
return this.withHap((hap) => {
if (!writeMessage) {
getWriter(br);
Pattern.prototype.serial = function (...args) {
return this._withHap((hap) => {
if (!serialWriter) {
getWriter(...args);
}
const onTrigger = (time, hap, currentTime) => {
var message = '';
var chk = 0;
if (typeof hap.value === 'object') {
if ('action' in hap.value) {
var action = hap.value['action'];
if (singlecharids) {
action = action.charAt(0);
}
message += action + '(';
message += hap.value['action'] + '(';
var first = true;
for (var [key, val] of Object.entries(hap.value)) {
for (const [key, val] of Object.entries(hap.value)) {
if (key === 'action') {
continue;
}
@@ -88,28 +53,19 @@ Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids
} else {
message += ',';
}
if (singlecharids) {
key = key.charAt(0);
}
message += key + ':' + val;
message += `${key}:${val}`;
}
message += ')';
if (sendcrc) {
chk = crc16(message);
}
} else {
for (const [key, val] of Object.entries(hap.value)) {
message += `${key}:${val}`;
message += `${key}:${val};`;
}
}
} else {
message = hap.value;
}
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(function () {
writeMessage(message, chk);
}, offset);
window.setTimeout(serialWriter, offset, message);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
});
-2
View File
@@ -1,5 +1,3 @@
import { toMidi } from '@strudel.cycles/core';
let loadCache = {};
async function loadFont(name) {
if (loadCache[name]) {
+3 -3
View File
@@ -42,7 +42,7 @@ export const instruments = [
'0031_FluidR3_GM_sf2_file',
'0031_GeneralUserGS_sf2_file',
'0031_SoundBlasterOld_sf2', // pianos until here
'0040_Aspirin_sf2_file',
'0040_Aspirin_sf2_file',
'0040_Chaos_sf2_file',
'0040_FluidR3_GM_sf2_file', // rhodes
'0040_GeneralUserGS_sf2_file', // staccato rhodes
@@ -59,7 +59,7 @@ export const instruments = [
'0046_GeneralUserGS_sf2_file', // ?
'0050_Aspirin_sf2_file', // glass organ
'0050_Chaos_sf2_file', // short glass organ
'0050_FluidR3_GM_sf2_file', // long glass organ !
'0050_FluidR3_GM_sf2_file',// long glass organ !
'0050_GeneralUserGS_sf2_file', // short glass organ
'0050_JCLive_sf2_file', // glass organ
'0050_SBLive_sf2', // ?
@@ -1635,7 +1635,7 @@ export const drums = [
'81_4_Chaos_sf2_file',
];
// see https://www.midi.org/specifications-old/item/gm-level-1-sound-set
// see https://www.midi.org/specifications-old/item/gm-level-1-sound-set
export const instrumentNames = [];
instrumentNames[0] = 'Acoustic Grand Piano: Piano';
instrumentNames[1] = 'Bright Acoustic Piano: Piano';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.5.0",
"version": "0.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.5.0",
"version": "0.4.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,8 +22,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/webaudio": "^0.5.0",
"@strudel.cycles/core": "^0.4.0",
"@strudel.cycles/webaudio": "^0.4.1",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
},
+1 -2
View File
@@ -1,5 +1,4 @@
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { Pattern } from '@strudel.cycles/core';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
+1 -1
View File
@@ -31,4 +31,4 @@ yields:
## Tonal API
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/tonal)
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
-3
View File
@@ -1,5 +1,2 @@
import './tonal.mjs';
import './voicings.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.5.0",
"version": "0.4.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"type": "module",
@@ -25,8 +25,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.5.0",
"@tonaljs/tonal": "^4.7.2",
"@strudel.cycles/core": "^0.4.0",
"@tonaljs/tonal": "^4.6.5",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
}
+1 -17
View File
@@ -7,27 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, controls, seq } from '@strudel.cycles/core';
import { pure } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
const { n } = controls;
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with n values', () => {
expect(
n(0, 1, 2)
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
});
+19 -14
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod } from '@strudel.cycles/core';
import { Pattern, mod } from '@strudel.cycles/core';
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
@@ -29,7 +29,7 @@ function scaleOffset(scale, offset, note) {
// TODO: find way to do this smarter
while (Math.abs(i - noteIndex) < Math.abs(offset)) {
i += direction;
const index = _mod(i, notes.length);
const index = mod(i, notes.length);
if (direction < 0 && n[0] === 'C') {
o += direction;
}
@@ -74,8 +74,8 @@ function scaleOffset(scale, offset, note) {
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
Pattern.prototype._transpose = function (intervalOrSemitones) {
return this.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
@@ -87,9 +87,11 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
});
});
};
// example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2))
// TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff
// TODO: find out how to patternify this function when it's standalone
// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
@@ -108,8 +110,8 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
* .note()
*/
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
@@ -118,7 +120,7 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
});
};
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
@@ -139,16 +141,19 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
* .note()
*/
export const scale = register('scale', function (scale /* : string */, pat) {
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
Pattern.prototype._scale = function (scale /* : string */) {
return this.withHap((hap) => {
let note = hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
}
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
return hap.withValue(() => note).setContext({ ...hap.context, scale });
});
});
};
Pattern.prototype.define('transpose', (a, pat) => pat.transpose(a), { composable: true, patternified: true });
Pattern.prototype.define('scale', (a, pat) => pat.scale(a), { composable: true, patternified: true });
Pattern.prototype.define('scaleTranspose', (a, pat) => pat.scaleTranspose(a), { composable: true, patternified: true });
+39 -63
View File
@@ -4,49 +4,31 @@ 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 { stack, register } from '@strudel.cycles/core';
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
/**
* Adds a new custom voicing dictionary.
*
* @name addVoicings
* @memberof Pattern
* @param {string} name identifier for the voicing dictionary
* @param {Object} dictionary maps chord symbol to possible voicings
* @param {Array} range min, max note
* @returns Pattern
* @example
* addVoicings('cookie', {
* 7: ['3M 7m 9M 12P 15P', '7m 10M 13M 16M 19P'],
* '^7': ['3M 6M 9M 12P 14M', '7M 10M 13M 16M 19P'],
* m7: ['8P 11P 14m 17m 19P', '5P 8P 11P 14m 17m'],
* m7b5: ['3m 5d 8P 11P 14m', '5d 8P 11P 14m 17m'],
* o7: ['3m 6M 9M 11A 15P'],
* '7alt': ['3M 7m 10m 13m 15P'],
* '7#11': ['7m 10m 13m 15P 17m'],
* }, ['C3', 'C6'])
* "<C^7 A7 Dm7 G7>".voicings('cookie').note()
*/
export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
Object.assign(voicingRegistry, { [name]: { dictionary, range } });
};
const getVoicing = (chord, dictionaryName, lastVoicing) => {
const { dictionary, range } = voicingRegistry[dictionaryName];
return dictionaryVoicing({
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) =>
dictionaryVoicing({
chord,
dictionary,
dictionary: lefthand,
range,
picker: minTopNoteDiff,
lastVoicing,
});
const Pattern = _Pattern;
Pattern.prototype.fmapNested = function (func) {
return new Pattern((span) =>
this.query(span)
.map((event) =>
reify(func(event))
.query(span)
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context)),
)
.flat(),
);
};
/**
@@ -55,38 +37,32 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
*
* @name voicings
* @memberof Pattern
* @param {string} dictionary which voicing dictionary to use.
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
* @returns Pattern
* @example
* stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
*/
let lastVoicing; // this now has to be global until another solution is found :-/
// it used to be local to the voicings function at evaluation time
// but since register will patternify by default, means that
// the function is called over and over again, resetting the lastVoicing variables
export const voicings = register('voicings', function (dictionary, pat) {
return pat
.fmap((value) => {
lastVoicing = getVoicing(value, dictionary, lastVoicing);
return stack(...lastVoicing);
})
.outerJoin();
});
Pattern.prototype.voicings = function (range) {
let lastVoicing;
if (!range?.length) {
// allows to pass empty array, if too lazy to specify range
range = ['F3', 'A4'];
}
return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing).withContext(() => ({
locations: event.context.locations || [],
}));
});
};
/**
* Maps the chords of the incoming pattern to root notes in the given octave.
*
* @name rootNotes
* @memberof Pattern
* @param {octave} octave octave to use
* @returns Pattern
* @example
* "<C^7 A7 Dm7 G7>".rootNotes(2).note()
*/
export const rootNotes = register('rootNotes', function (octave, pat) {
return pat.fmap((value) => {
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
Pattern.prototype._rootNotes = function (octave = 2) {
return this.fmap((value) => {
const [_, root] = value.match(/^([a-gA-G][b#]?).*$/);
return root + octave;
});
});
};
Pattern.prototype.define('voicings', (range, pat) => pat.voicings(range), { composable: true });
Pattern.prototype.define('rootNotes', (oct, pat) => pat.rootNotes(oct), { composable: true, patternified: true });
+8 -2
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/tone
Note: This package still works but is no longer maintained in favor of `@strudel.cycles/webaudio`.
This package adds Tone.js functions to strudel Patterns.
## Install
@@ -33,3 +31,11 @@ document.getElementById('play').addEventListener('click', async () => {
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
## API
See "Tone API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Dev Notes
- `@tonejs/piano` has peer dependency on `webmidi@^2.5.1`! A newer version of webmidi will break.
- If you use Tone in another package, make sure to `import { Tone } from '@strudel.cycles/tone`, to ensure you get the same AudioContext.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.5.0",
"version": "0.4.0",
"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.5.0",
"@strudel.cycles/core": "^0.4.0",
"tone": "^14.7.77"
}
}
+9 -4
View File
@@ -4,15 +4,18 @@ 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 { register } from '@strudel.cycles/core';
import { Pattern } from '@strudel.cycles/core';
import * as _Tone from 'tone';
// import Tone from here, to make sure to get the same AudioContext
export const Tone = _Tone;
const {
AutoFilter,
Destination,
Filter,
Gain,
isNote,
Synth,
PolySynth,
MembraneSynth,
@@ -46,8 +49,8 @@ export const getDefaultSynth = () => {
// https://www.charlie-roberts.com/gibberish/playground/
// with this function, you can play the pattern with any tone synth
export const tone = register('tone', function (instrument, pat) {
return pat.onTrigger((time, hap) => {
Pattern.prototype.tone = function (instrument) {
return this.onTrigger((time, hap) => {
let note;
let velocity = hap.context?.velocity ?? 0.75;
if (instrument instanceof PluckSynth) {
@@ -71,7 +74,9 @@ export const tone = register('tone', function (instrument, pat) {
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
}
});
});
};
Pattern.prototype.define('tone', (type, pat) => pat.tone(type), { composable: true, patternified: false });
// synth helpers
export const amsynth = (options) => new AMSynth(options);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.5.0",
"version": "0.4.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"scripts": {
@@ -24,7 +24,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/core": "^0.4.0",
"acorn": "^8.8.1",
"escodegen": "^2.0.0",
"estree-walker": "^3.0.1"
+3 -3
View File
@@ -1,7 +1,7 @@
import escodegen from 'escodegen';
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import { isNoteWithOctave } from '@strudel.cycles/core';
import { isNote } from '@strudel.cycles/core';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
@@ -25,11 +25,11 @@ export function transpiler(input, options = {}) {
this.skip();
return this.replace(miniWithLocation(value, node, simpleLocs));
}
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
if (node.type === 'Identifier' && isNote(node.name)) {
this.skip();
return this.replace({ type: 'Literal', value: node.name });
}
// TODO:
},
leave(node, parent, prop, index) {},
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.5.0",
"version": "0.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.5.0",
"version": "0.4.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -30,6 +30,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.5.0"
"@strudel.cycles/core": "^0.4.0"
}
}
-313
View File
@@ -1,313 +0,0 @@
import { toMidi, fromMidi } from '@strudel.cycles/core/util.mjs';
// import './feedbackdelay.mjs';
// import './reverb.mjs';
// TODO: reuse as much logic as possible between webaudio.mjs and render.mjs
const logger = console.log;
export const splitSN = (s, n) => {
if (!s.includes(':')) {
return [s, n];
}
let [s2, n2] = s.split(':');
if (isNaN(Number(n2))) {
return [s, n];
}
return [s2, n2];
};
const getOscillator = (ac, { s, freq, t, duration, release }) => {
// make oscillator
const o = ac.createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + duration + release);
return o;
};
const getADSR = (ac, attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = ac.createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
function gainNode(ac, value) {
const node = ac.createGain();
node.gain.value = value;
return node;
}
/* let delays = {};
function getDelay(ac, orbit, delaytime, delayfeedback, t) {
if (!delays[orbit]) {
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
dly.connect(ac.destination);
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
}
let reverbs = {};
function getReverb(ac, orbit, duration = 2) {
if (!reverbs[orbit]) {
const reverb = ac.createReverb(duration);
reverb.connect(ac.destination);
reverbs[orbit] = reverb;
}
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
} */
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
const getFilter = (ac, type, frequency, Q) => {
const filter = ac.createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
// export const webaudioOutput = async (t, hap, ct, cps) => {
export const renderHap = async (ac, hap, deadline) => {
const hapDuration = hap.duration;
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
logger(
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
'error',
);
/* throw new Error(
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
); */
return;
}
// calculate correct time (tone.js workaround)
let t = ac.currentTime + deadline;
// destructure value
let {
freq,
s,
bank,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 0.8,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
//
coarse,
crush,
shape,
pan,
speed = 1, // sample playback speed
begin = 0,
end = 1,
vowel,
delay = 0,
delayfeedback = 0.5,
delaytime = 0.25,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
orbit = 1,
room,
size = 2,
roomsize = size,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (bank && s) {
s = `${bank}_${s}`;
}
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator(ac, { t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
chain.push(gainNode(ac, 0.3));
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(ac, attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else {
// TODO: refactor sampler to node
/*
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
}
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * duration * bufferSource.playbackRate.value;
duration = (end - begin) * duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}
t += nudge;
bufferSource.start(t, offset);
if (cut !== undefined) {
cutGroups[cut]?.stop(t); // fade out?
cutGroups[cut] = bufferSource;
}
chain.push(bufferSource);
bufferSource.stop(t + duration + release);
const adsr = getADSR(ac, attack, decay, sustain, release, 1, t, t + duration);
chain.push(adsr); */
}
// gain stage
chain.push(gainNode(ac, gain));
// filters
cutoff !== undefined && chain.push(getFilter(ac, 'lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter(ac, 'highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter(ac, 'bandpass', bandf, bandq));
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
// effects
/* coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); */
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// last gain
const post = gainNode(ac, 1);
chain.push(post);
// post.connect(getDestination());
post.connect(ac.destination);
// TODO: refactor delay / reverb for node: extending nodes does not work..
// delay
/* let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(ac, orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(ac, orbit, roomsize);
reverbSend = effectSend(post, reverbNode, room);
} */
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// disconnect all nodes when source node has ended:
chain[0].onended = () =>
chain
.concat([
/*delaySend , reverbSend */
])
.forEach((n) => n?.disconnect());
};
+3 -7
View File
@@ -1,5 +1,4 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext } from './index.mjs';
import { logger } from '@strudel.cycles/core';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -20,12 +19,9 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
export const getSampleBufferSource = async (s, n, note, speed) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
}
let midi = valueToMidi({ freq, note }, 36);
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
+12 -26
View File
@@ -4,6 +4,7 @@ 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 { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import * as strudel from '@strudel.cycles/core';
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs';
@@ -195,14 +196,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
logger(
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
'error',
throw new Error(
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
);
/* throw new Error(
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
); */
return;
}
// calculate correct time (tone.js workaround)
let t = ac.currentTime + deadline;
@@ -216,22 +212,12 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
n = 0,
note,
gain = 0.8,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
//
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
coarse,
crush,
shape,
@@ -303,10 +289,10 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
bufferSource = await getSampleBufferSource(s, n, note, speed);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -396,7 +382,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
Pattern.prototype.webaudio = function () {
Pattern.prototype.out = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.5.0",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.5.0",
"version": "0.4.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.5.0",
"@strudel.cycles/core": "^0.4.0",
"WebDirt": "github:dktr0/WebDirt"
}
}

Some files were not shown because too many files have changed in this diff Show More