Merge branch 'main' into dirtify-plain-values

This commit is contained in:
Felix Roos
2022-08-03 22:29:49 +02:00
261 changed files with 29949 additions and 42963 deletions
+9 -1
View File
@@ -1,4 +1,5 @@
dist
out
!docs/dist
dist-*
cabal-dev
@@ -27,4 +28,11 @@ node_modules/
repl-parcel
mytunes.ts
doc
.parcel-cache
out
.parcel-cache
repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
server/samples/old
+3 -1
View File
@@ -32,7 +32,9 @@ Use one of the Communication Channels listed above.
## Improve the Tutorial
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/repl/src/tutorial/tutorial.mdx).
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
@@ -2,7 +2,7 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
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/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
View File
-1
View File
@@ -1 +0,0 @@
strudel.tidalcycles.org
-16
View File
@@ -1,16 +0,0 @@
{
"files": {
"main.css": "/static/css/main.13bac14b.css",
"main.js": "/static/js/main.289a0f18.js",
"static/js/787.1c52cb78.chunk.js": "/static/js/787.1c52cb78.chunk.js",
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
"index.html": "/index.html",
"main.13bac14b.css.map": "/static/css/main.13bac14b.css.map",
"main.289a0f18.js.map": "/static/js/main.289a0f18.js.map",
"787.1c52cb78.chunk.js.map": "/static/js/787.1c52cb78.chunk.js.map"
},
"entrypoints": [
"static/css/main.13bac14b.css",
"static/js/main.289a0f18.js"
]
}
-1
View File
@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.289a0f18.js"></script><link href="/static/css/main.13bac14b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
-15
View File
@@ -1,15 +0,0 @@
{
"short_name": "Strudel REPL",
"name": "Strudel REPL - Tidal Patterns in JavaScript",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
"use strict";(self.webpackChunk_strudel_cycles_repl=self.webpackChunk_strudel_cycles_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=787.1c52cb78.chunk.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-73
View File
@@ -1,73 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/*! @license ReactCodeMirror - MIT License - Tom Golden (8162045+tbjgolden@users.noreply.github.com) */
/**
* @license Fraction.js v4.2.0 05/03/2022
* https://www.xarg.org/2014/03/rational-numbers-in-javascript/
*
* Copyright (c) 2021, Robert Eisele (robert@xarg.org)
* Dual licensed under the MIT or GPL Version 2 licenses.
**/
/**
* Tone.js
* @author Yotam Mann
* @license http://opensource.org/licenses/MIT MIT License
* @copyright 2014-2019 Yotam Mann
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.912f00e3.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.239413ad.js" defer></script> </body></html>
+11
View File
@@ -0,0 +1,11 @@
{
"source": {
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
},
"plugins": ["plugins/markdown"],
"opts": {
"destination": "./out/",
"recurse": true
}
}
+4359 -229
View File
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -6,10 +6,15 @@
"scripts": {
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run start",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"snapshot": "cd repl && npm run snapshot",
"repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "cd repl && npm run build"
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
"preview": "npx serve ./out",
"deploy": "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"
},
"workspaces": [
"packages/*"
@@ -33,6 +38,10 @@
"homepage": "https://strudel.tidalcycles.org",
"devDependencies": {
"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",
"mocha": "^9.1.4"
}
+592 -109
View File
@@ -8,100 +8,433 @@ import { Pattern, sequence } from './pattern.mjs';
const controls = {};
const generic_params = [
/**
* Select a sound / sample by name.
*
* <details>
* <summary>show all sounds</summary>
*
* 808 (6) 808bd (25) 808cy (25) 808hc (5) 808ht (5) 808lc (5) 808lt (5) 808mc (5) 808mt (5) 808oh (5) 808sd (25) 909 (1) ab (12) ade (10) ades2 (9) ades3 (7) ades4 (6) alex (2) alphabet (26) amencutup (32) armora (7) arp (2) arpy (11) auto (11) baa (7) baa2 (7) bass (4) bass0 (3) bass1 (30) bass2 (5) bass3 (11) bassdm (24) bassfoo (3) battles (2) bd (24) bend (4) bev (2) bin (2) birds (10) birds3 (19) bleep (13) blip (2) blue (2) bottle (13) breaks125 (2) breaks152 (1) breaks157 (1) breaks165 (1) breath (1) bubble (8) can (14) casio (3) cb (1) cc (6) chin (4) circus (3) clak (2) click (4) clubkick (5) co (4) coins (1) control (2) cosmicg (15) cp (2) cr (6) crow (4) d (4) db (13) diphone (38) diphone2 (12) dist (16) dork2 (4) dorkbot (2) dr (42) dr2 (6) dr55 (4) dr_few (8) drum (6) drumtraks (13) e (8) east (9) electro1 (13) em2 (6) erk (1) f (1) feel (7) feelfx (8) fest (1) fire (1) flick (17) fm (17) foo (27) future (17) gab (10) gabba (4) gabbaloud (4) gabbalouder (4) glasstap (3) glitch (8) glitch2 (8) gretsch (24) gtr (3) h (7) hand (17) hardcore (12) hardkick (6) haw (6) hc (6) hh (13) hh27 (13) hit (6) hmm (1) ho (6) hoover (6) house (8) ht (16) if (5) ifdrums (3) incoming (8) industrial (32) insect (3) invaders (18) jazz (8) jungbass (20) jungle (13) juno (12) jvbass (13) kicklinn (1) koy (2) kurt (7) latibro (8) led (1) less (4) lighter (33) linnhats (6) lt (16) made (7) made2 (1) mash (2) mash2 (4) metal (10) miniyeah (4) monsterb (6) moog (7) mouth (15) mp3 (4) msg (9) mt (16) mute (28) newnotes (15) noise (1) noise2 (8) notes (15) numbers (9) oc (4) odx (15) off (1) outdoor (6) pad (3) padlong (1) pebbles (1) perc (6) peri (15) pluck (17) popkick (10) print (11) proc (2) procshort (8) psr (30) rave (8) rave2 (4) ravemono (2) realclaps (4) reverbkick (1) rm (2) rs (1) sax (22) sd (2) seawolf (3) sequential (8) sf (18) sheffield (1) short (5) sid (12) sine (6) sitar (8) sn (52) space (18) speakspell (12) speech (7) speechless (10) speedupdown (9) stab (23) stomp (10) subroc3d (11) sugar (2) sundance (6) tabla (26) tabla2 (46) tablex (3) tacscan (22) tech (13) techno (7) tink (5) tok (4) toys (13) trump (11) ul (10) ulgab (5) uxay (3) v (6) voodoo (5) wind (10) wobble (1) world (3) xmas (1) yeah (31)
*
* <a href="https://tidalcycles.org/docs/configuration/Audio%20Samples/default_library" target="_blank">more info</a>
*
* </details>
*
* @name s
* @param {string | Pattern} sound The sound / pattern of sounds to pick
* @example
* s("bd hh").osc()
*
*/
['s', 's', 'sound'],
/**
* The note or sample number to choose for a synth or sampleset
* Note names currently not working yet, but will hopefully soon. Just stick to numbers for now
*
* @name n
* @param {string | number | Pattern} value note name, note number or sample number
* @example
* s('superpiano').n("<0 1 2 3>").osc()
* @example
* s('superpiano').n("<c4 d4 e4 g4>").osc()
* @example
* n("0 1 2 3").s('east').osc()
*/
// TODO: nOut does not work
// TODO: notes don't work as expected
// current "workaround" for notes:
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
// also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'],
//['s', 'toArg', 'for internal sound routing'],
// ["f", "from", "for internal sound routing"),
//['f', 'to', 'for internal sound routing'],
/**
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
*
* @name accelerate
* @param {number | Pattern} amount acceleration.
* @example
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
*
*/
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
['f', 'amp', 'like @gain@, but linear.'],
[
'f',
'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
],
['f', 'bandf', 'a pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
[
'f',
'begin',
'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.',
],
['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
// ['f', 'clhatdecay', ''],
[
'f',
'crush',
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
[
'f',
'coarse',
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
],
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
[
'i',
'cut',
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
],
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
// ['f', 'cutoffegint', ''],
['f', 'decay', ''],
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
['f', 'detune', ''],
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
[
'f',
'dry',
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
],
[
'f',
'end',
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
],
[
'f',
'fadeTime',
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
],
[
'f',
'fadeInTime',
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
],
['f', 'freq', ''],
/**
* Like {@link amp}, but exponential.
*
* @name gain
* @param {number | Pattern} amount gain.
* @example
* s("bd*8").gain(".7*2 1 .7*2 1 .7 1").osc()
*
*/
[
'f',
'gain',
'a pattern of numbers that specify volume. Values less than 1 make the sound quieter. Values greater than 1 make the sound louder. For the linear equivalent, see @amp@.',
],
['f', 'gate', ''],
// ['f', 'hatgrain', ''],
/**
* Like {@link gain}, but linear.
*
* @name amp
* @param {number | Pattern} amount gain.
* @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.
*
* @name attack
* @param {number | Pattern} attack time in seconds.
* @example
* n("c5 e5").s('superpiano').attack("<0 .1>").osc()
*
*/
[
'f',
'hcutoff',
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
],
// 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
* s("sax").cut(1).decay("<.1 .2 .3 .4>").sustain(0).osc()
*
*/
['f', 'decay', ''],
['f', 'sustain', ''],
[
'f',
'release',
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
],
[
'f',
'hold',
'a pattern of numbers to specify the hold time (in seconds) of an envelope applied to each sample. Only takes effect if `attack` and `release` are also specified.',
],
// TODO: in tidal, it seems to be normalized
/**
* Sets the center frequency of the band-pass filter.
*
* @name bandf
* @param {number | Pattern} frequency center frequency
* @example
* s("bd sd").bandf("<1000 2000 4000 8000>").osc()
*
*/
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
// 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
* @example
* s("bd sd").bandf(2000).bandq("<.2 .9>").osc()
*
*/
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
/**
* 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.
*
* @name begin
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
* @example
* s("rave").begin("<0 .25 .5 .75>").osc()
*
*/
[
'f',
'begin',
'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.',
],
/**
* The same as {@link begin}, but cuts off the end off each sample.
*
* @name end
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
* @example
* s("bd*2,ho*4").end("<.1 .2 .5 1>").osc()
*
*/
[
'f',
'end',
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
],
/**
* Loops the sample (from `begin` to `end`) the specified number of times.
* Note that the tempo of the loop is not synced with the cycle tempo.
*
* @name loop
* @param {number | Pattern} times How often the sample is looped
* @example
* s("bd").loop("<1 2 3 4>").osc()
*
*/
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
* 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.
*
* @name legato
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @example
* "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', 'clhatdecay', ''],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3,jvbass*2").fast(2).crush("<16 8 7 6 5 4 3 2>").osc()
*
*/
[
'f',
'crush',
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
/**
* fake-resampling for lowering the sample rate
*
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("xmas").coarse("<1 4 8 16 32>").osc()
*
*/
[
'f',
'coarse',
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
],
/**
* choose the channel the pattern is sent to in superdirt
*
* @name channel
* @param {number | Pattern} channel channel number
*
*/
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
/**
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
*
* @name cut
* @param {number | Pattern} group cut group number
* @example
* s("bd sax").cut(1).osc()
*
*/
[
'i',
'cut',
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
],
/**
* Applies the cutoff frequency of the low-pass filter.
*
* @name cutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @example
* s("bd,hh*2,<~ sd>").fast(2).cutoff("<4000 2000 1000 500 200 100>").osc()
*
*/
// TODO: add lpf synonym
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
/**
* Applies the cutoff frequency of the high-pass filter.
*
* @name hcutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @example
* s("bd,hh*2,<~ sd>").fast(2).hcutoff("<4000 2000 1000 500 200 100>").osc()
*
*/
// 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 cutoff frequency of the high-pass filter.
*
* @name hresonance
* @param {number | Pattern} q resonance factor between 0 and 1
* @example
* s("bd,hh*2,<~ sd>").fast(2).hcutoff(2000).hresonance("<0 .2 .4 .6>").osc()
*
*/
[
'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 1
* @example
* s("bd,hh*2,<~ sd>").fast(2).cutoff(2000).resonance("<0 .2 .4 .6>").osc()
*
*/
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
// TODO: add lpq synonym?
/**
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
*
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
*
*/
['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 .5 .75 1>").osc()
*
*/
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
['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.
*
* @name lock
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
* @example
* s("sd").delay().lock(1).osc()
*
*/
[
'f',
'lock',
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
],
/**
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
*
* @name detune
* @param {number | Pattern} amount between 0 and 1
* @example
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
*
*/
['f', 'detune', ''],
/**
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
*
* @name dry
* @param {number | Pattern} dry 0 = wet, 1 = dry
* @example
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
*
*/
[
'f',
'dry',
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
],
// TODO: does not seem to do anything
/*
* Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("ho*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
*
*/
[
'f',
'fadeTime',
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
],
// TODO: see above
[
'f',
'fadeInTime',
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
],
/**
* Set frequency of sound.
*
* @name freq
* @param {number | Pattern} frequency in Hz. the audible range is between 20 and 20000 Hz
* @example
* freq("220 110 440 110").s("superzow").osc()
* @example
* freq("110".mulOut(".5 1.5 .6 [2 3]")).s("superzow").osc()
*
*/
['f', 'freq', ''],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['f', 'gate', ''],
// ['f', 'hatgrain', ''],
// ['f', 'lagogo', ''],
// ['f', 'lclap', ''],
// ['f', 'lclaves', ''],
// ['f', 'lclhat', ''],
// ['f', 'lcrash', ''],
// TODO:
// https://tidalcycles.org/docs/reference/audio_effects/#leslie-1
// https://tidalcycles.org/docs/reference/audio_effects/#leslie
/**
* Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet.
*
* @name leslie
* @param {number | Pattern} wet between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
*
*/
['f', 'leslie', ''],
/**
* Rate of modulation / rotation for leslie effect
*
* @name lrate
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
*
*/
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
['f', 'lrate', ''],
/**
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
*
* @name lsize
* @param {number | Pattern} meters somewhere between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
*
*/
['f', 'lsize', ''],
// ['f', 'lfo', ''],
// ['f', 'lfocutoffint', ''],
@@ -113,47 +446,86 @@ const generic_params = [
// ['f', 'lhitom', ''],
// ['f', 'lkick', ''],
// ['f', 'llotom', ''],
[
'f',
'lock',
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
],
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
// ['f', 'lophat', ''],
// ['f', 'lsnare', ''],
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'],
['f', 'degree', ''],
['f', 'mtranspose', ''],
['f', 'ctranspose', ''],
['f', 'harmonic', ''],
['f', 'stepsPerOctave', ''],
['f', 'octaveR', ''],
['f', 'degree', ''], // TODO: what is this? not found in tidal doc
['f', 'mtranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'ctranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'harmonic', ''], // TODO: what is this? not found in tidal doc
['f', 'stepsPerOctave', ''], // TODO: what is this? not found in tidal doc
['f', 'octaveR', ''], // TODO: what is this? not found in tidal doc
// TODO: why is this needed? what's the difference to late / early?
[
'f',
'nudge',
'Nudges events into the future by the specified number of seconds. Negative numbers work up to a point as well (due to internal latency)',
],
// TODO: the following doc is just a guess, it's not documented in tidal doc.
/**
* Sets the default octave of a synth.
*
* @name octave
* @param {number | Pattern} octave octave number
* @example
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
*/
['i', 'octave', ''],
['f', 'offset', ''],
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
// ['f', 'ophatdecay', ''],
// TODO: example
/**
* 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
*
*/
[
'i',
'orbit',
'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.',
],
['f', 'overgain', ''],
['f', 'overshape', ''],
['f', 'overgain', ''], // TODO: what is this? not found in tidal doc
['f', 'overshape', ''], // TODO: what is this? not found in tidal doc
/**
* Sets position in stereo.
*
* @name pan
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>").osc()
*
*/
[
'f',
'pan',
'a pattern of numbers between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)',
],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is fanned out
*
* @name panspan
* @param {number | Pattern} span between -inf and inf, negative is backwards ordering
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc()
*
*/
[
'f',
'panspan',
'a pattern of numbers between -inf and inf, which controls how much multichannel output is fanned out (negative is backwards ordering)',
],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is spread
*
* @name pansplay
* @param {number | Pattern} spread between 0 and 1
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc()
*
*/
[
'f',
'pansplay',
@@ -173,68 +545,163 @@ const generic_params = [
// ['f', 'pitch2', ''],
// ['f', 'pitch3', ''],
// ['f', 'portamento', ''],
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'rate', "used in SuperDirt softsynths as a control rate or 'speed'"],
[
'f',
'release',
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
],
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
// ['f', 'sagogo', ''],
// ['f', 'sclap', ''],
// ['f', 'sclaves', ''],
// ['f', 'scrash', ''],
// TODO: slide param for certain synths
['f', 'slide', ''],
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'semitone', ''],
[
'f',
'shape',
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
],
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
['f', 'velocity', ''],
['f', 'voice', ''], // TODO: synth param
/**
* Sets the level of reverb.
*
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* 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.'],
/**
* Sets the room size of the reverb, see {@link room}.
*
* @name size
* @param {number | Pattern} size between 0 and 1
* @example
* s("bd sd").room(.8).size("<0 .2 .4 .6 .8 1>").osc()
*
*/
// TODO: find out why :
// s("bd sd").room(.8).size("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
[
'f',
'size',
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
],
['f', 'slide', ''],
// ['f', 'sagogo', ''],
// ['f', 'sclap', ''],
// ['f', 'sclaves', ''],
// ['f', 'scrash', ''],
/**
* Wave shaping distortion. CAUTION: it might get loud
*
* @name shape
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd").shape("<0 .2 .4 .6 .8 1>").osc()
*
*/
[
'f',
'shape',
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
],
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
* @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>").osc()
* @example
* speed("1 1.5*2 [2 1.1]").s("sax").cut(1).osc()
*
*/
[
'f',
'speed',
'a pattern of numbers which changes the speed of sample playback, i.e. a cheap way of changing pitch. Negative values will play the sample backwards!',
],
['f', 'squiz', ''],
['f', 'stutterdepth', ''],
['f', 'stuttertime', ''],
['f', 'sustain', ''],
['f', 'timescale', ''],
['f', 'timescalewin', ''],
// ['f', 'tomdecay', ''],
/**
* Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
* @name unit
* @param {number | string | Pattern} unit see description above
* @example
* speed("1 2 .5 3").s("bd").unit("c").osc()
*
*/
[
's',
'unit',
'used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.',
],
['f', 'velocity', ''],
/**
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
*
* "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated."
*
* @name squiz
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
* @example
* squiz("2 4/2 6 [8 16]").s("bd").osc()
*
*/
['f', 'squiz', ''],
['f', 'stutterdepth', ''], // TODO: what is this? not found in tidal doc
['f', 'stuttertime', ''], // TODO: what is this? not found in tidal doc
['f', 'timescale', ''], // TODO: what is this? not found in tidal doc
['f', 'timescalewin', ''], // TODO: what is this? not found in tidal doc
// ['f', 'tomdecay', ''],
// ['f', 'vcfegint', ''],
// ['f', 'vcoegint', ''],
['f', 'voice', ''],
/**
*
* Formant filter to make things sound like vowels.
*
* @name vowel
* @param {string | Pattern} vowel You can use a e i o u. Use a rest (~) to override the effect
* @example
* vowel("a e i [o u]").slow(2)
* .n("<[0,7]!4 [2,7]!4>")
* .s('supersquare').osc()
*
*/
[
's',
'vowel',
'formant filter to make things sound like vowels, a pattern of either `a`, `e`, `i`, `o` or `u`. Use a rest (`~`) for no effect.',
],
/* // TODO: find out how it works
* Made by Calum Gunn. Divides an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discards a fraction of them. Takes a number between 1 and 100, denoted the percentage of segments to drop. The SuperCollider manual describes the Waveloss effect this way:
*
* Divide an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discard a fraction of them (i.e. replace them with silence of the same length). The technique was described by Trevor Wishart in a lecture. Parameters: the filter drops drop out of out of chunks. mode can be 1 to drop chunks in a simple deterministic fashion (e.g. always dropping the first 30 out of a set of 40 segments), or 2 to drop chunks randomly but in an appropriate proportion.)
*
* mode: ?
* waveloss: ?
*
* @name waveloss
*/
['f', 'waveloss', ''],
// TODO: midi effects?
['f', 'dur', ''],
// ['f', 'modwheel', ''],
['f', 'expression', ''],
['f', 'sustainpedal', ''],
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
*
* @name tremolodepth
* @param {number | Pattern} depth between 0 and 1
* @example
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
// TODO: tremdp alias
['f', 'tremolodepth', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['f', 'tremolorate', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
// TODO: doesn't seem to do anything
['f', 'phaserdepth', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'phaserrate', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'fshift', 'frequency shifter'],
['f', 'fshiftnote', 'frequency shifter'],
['f', 'fshiftphase', 'frequency shifter'],
['f', 'triode', 'tube distortion'],
['f', 'krush', 'shape/bass enhancer'],
['f', 'kcutoff', ''],
@@ -277,18 +744,34 @@ const generic_params = [
['f', 'uid', ''],
['f', 'val', ''],
['f', 'cps', ''],
['f', 'clip', ''],
];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _setter = (func) =>
const _setter = (func, name) =>
function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
}
return this.set(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(controls[name]);
Pattern.prototype[name] = _setter(controls[name], name);
});
// create custom param
controls.createParam = (name) => {
const func = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(func, name);
return (...pats) => _name(name, ...pats);
};
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: createParam(name) }), {});
export default controls;
+16
View File
@@ -6,6 +6,22 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction, { gcd } from './fraction.mjs';
/**
* Intended for a debugging, drawLine renders the pattern as a string, where each character represents the same time span.
* Should only be used with single characters as values, otherwise the character slots will be messed up.
* Character legend:
*
* - "|" cycle separator
* - "-" hold previous value
* - "." silence
*
* @param {Pattern} pattern the pattern to use
* @param {number} chars max number of characters (approximately)
* @returns string
* @example
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
* console.log(line);
*/
function drawLine(pat, chars = 60) {
let cycle = 0;
let pos = Fraction(0);
+60
View File
@@ -17,10 +17,70 @@ const euclid = (pulses, steps, rotation = 0) => {
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 accurately
* describe a large number of rhythms used in the most important music world traditions.
*
* @memberof Pattern
* @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.
* "c3".euclid(3,8)
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* "c3".euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* "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.
* "c3".euclid(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* "c3".euclid(3,7)
* @example // The Cuban tresillo pattern.
* "c3".euclid(3,8)
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
* "c3".euclid(4,7)
* @example // The Aksak rhythm of Turkey.
* "c3".euclid(4,9)
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
* "c3".euclid(4,11)
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
* "c3".euclid(5,6)
* @example // The Nawakhat pattern, another popular Arab rhythm.
* "c3".euclid(5,7)
* @example // The Cuban cinquillo pattern.
* "c3".euclid(5,8)
* @example // A popular Arab rhythm called Agsag-Samai.
* "c3".euclid(5,9)
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
* "c3".euclid(5,11)
* @example // The Venda clapping pattern of a South African childrens song.
* "c3".euclid(5,12)
* @example // The Bossa-Nova rhythm necklace of Brazil.
* "c3".euclid(5,16)
* @example // A typical rhythm played on the Bendir (frame drum).
* "c3".euclid(7,8)
* @example // A common West African bell pattern.
* "c3".euclid(7,12)
* @example // A Samba rhythm necklace from Brazil.
* "c3".euclid(7,16,14)
* @example // A rhythm necklace used in the Central African Republic.
* "c3".euclid(9,16)
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
* "c3".euclid(11,24,14)
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
* "c3".euclid(13,24,5)
*/
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
return this.struct(euclid(pulses, steps, rotation));
};
/**
* Similar to {@link Pattern#euclid}, but each pulse is held until the next pulse, so there will be no gaps.
*/
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
const bin_pat = euclid(pulses, steps, rotation);
const firstOne = bin_pat.indexOf(1);
+2 -2
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
// this is a shortcut to eval code from a gist
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
export default (route) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
export default (route, cache = true) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
.then((res) => res.text())
.then((code) => eval(code));
+10 -9
View File
@@ -14,7 +14,10 @@ export class Hap {
then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the
start and end of the 'part' timespan.
The context is to store a list of source code locations causing the event
The context is to store a list of source code locations causing the event.
The word 'Event' is more or less a reserved word in javascript, hence this
class is named called 'Hap'.
*/
constructor(whole, part, value, context = {}, stateful = false) {
@@ -37,18 +40,18 @@ export class Hap {
}
withSpan(func) {
// Returns a new event with the function f applies to the event timespan.
// Returns a new hap with the function f applies to the hap timespan.
const whole = this.whole ? func(this.whole) : undefined;
return new Hap(whole, func(this.part), this.value, this.context);
}
withValue(func) {
// Returns a new event with the function f applies to the event value.
// Returns a new hap with the function f applies to the hap value.
return new Hap(this.whole, this.part, func(this.value), this.context);
}
hasOnset() {
// Test whether the event contains the onset, i.e that
// Test whether the hap contains the onset, i.e that
// the beginning of the part is the same as that of the whole timespan."""
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
}
@@ -83,11 +86,9 @@ export class Hap {
}
showWhole() {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
}
showWhole() {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
}`;
}
combineContext(b) {
+17 -1
View File
@@ -14,4 +14,20 @@ export * from './signal.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
// export * from './value.mjs';
export * from './speak.mjs';
export { default as gist } from './gist.js';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
export const version = p.version; */
console.log(
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
'background-color: black;color:white;padding:4px;border-radius:15px',
);
if (globalThis._strudelLoaded) {
console.warn(
`@strudel.cycles/core was loaded more than once...
This might happen when you have multiple versions of strudel installed.
Please check with "npm ls @strudel.cycles/core".`,
);
}
globalThis._strudelLoaded = true;
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.5",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/core",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.5",
"version": "0.1.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+405 -99
View File
@@ -13,20 +13,39 @@ import { unionWithObj } from './value.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod, objectify } from './util.mjs';
import drawLine from './drawLine.mjs';
/** @class Class representing a pattern. */
export class Pattern {
// the following functions will get patternFactories as nested functions:
_Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
/**
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
*
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
*/
constructor(query) {
this.query = query;
}
/**
* query haps insude the tiven time span
*
* @param {Fraction | number} begin from time
* @param {Fraction | number} end to time
* @returns Hap[]
* @example
* const pattern = sequence('a', ['b', 'c']);
* const haps = pattern.queryArc(0, 1);
*/
queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end)));
}
/**
* Returns a new pattern, with queries split at cycle boundaries. This makes
* some calculations easier to express, as all haps are then constrained to
* happen within a cycle.
* @returns Pattern
*/
_splitQueries() {
// Splits queries at cycle boundaries. This makes some calculations
// easier to express, as all events are then constrained to happen within
// a cycle.
const pat = this;
const q = (state) => {
return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan))));
@@ -34,48 +53,98 @@ export class Pattern {
return new Pattern(q);
}
/**
* Returns a new pattern, where the given function is applied to the query
* timespan before passing it to the original pattern.
* @param {Function} func the function to apply
* @returns Pattern
*/
withQuerySpan(func) {
return new Pattern((state) => this.query(state.withSpan(func)));
}
/**
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
* begin and end time of the query timespan.
* @param {Function} func the function to apply
* @returns Pattern
*/
withQueryTime(func) {
// Returns a new pattern, with the function applied to both the begin
// and end of the the query timespan
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
}
withEventSpan(func) {
// Returns a new pattern, with the function applied to each event
// timespan.
/**
* Similar to {@link Pattern#withQuerySpan}, but the function is applied to the timespans
* of all haps returned by pattern queries (both `part` timespans, and where
* present, `whole` timespans).
* @param {Function} func
* @returns Pattern
*/
withHapSpan(func) {
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
}
withEventTime(func) {
// Returns a new pattern, with the function applied to both the begin
// and end of each event timespan.
return this.withEventSpan((span) => span.withTime(func));
/**
* As with {@link Pattern#withHapSpan}, but the function is applied to both the
* begin and end time of the hap timespans.
* @param {Function} func the function to apply
* @returns Pattern
*/
withHapTime(func) {
return this.withHapSpan((span) => span.withTime(func));
}
_withEvents(func) {
/**
* Returns a new pattern with the given function applied to the list of haps returned by every query.
* @param {Function} func
* @returns Pattern
*/
_withHaps(func) {
return new Pattern((state) => func(this.query(state)));
}
_withEvent(func) {
return this._withEvents((events) => events.map(func));
/**
* As with {@link Pattern#_withHaps}, but applies the function to every hap, rather than every list of haps.
* @param {Function} func
* @returns Pattern
*/
_withHap(func) {
return this._withHaps((haps) => haps.map(func));
}
/**
* Returns a new pattern with the context field set to every hap set to the given value.
* @param {*} context
* @returns Pattern
*/
_setContext(context) {
return this._withEvent((event) => event.setContext(context));
return this._withHap((hap) => hap.setContext(context));
}
/**
* Returns a new pattern with the given function applied to the context field of every hap.
* @param {Function} func
* @returns Pattern
*/
_withContext(func) {
return this._withEvent((event) => event.setContext(func(event.context)));
return this._withHap((hap) => hap.setContext(func(hap.context)));
}
/**
* Returns a new pattern with the context field of every hap set to an empty object.
* @returns Pattern
*/
_stripContext() {
return this._withEvent((event) => event.setContext({}));
return this._withHap((hap) => hap.setContext({}));
}
/**
* Returns a new pattern with the given location information added to the
* context of every hap.
* @param {Number} start
* @param {Number} end
* @returns Pattern
*/
withLocation(start, end) {
const location = {
start: { line: start[0], column: start[1], offset: start[2] },
@@ -113,39 +182,72 @@ export class Pattern {
});
}
/**
* Returns a new pattern, with the function applied to the value of
* each hap. It has the alias {@link Pattern#fmap}.
* @param {Function} func
* @returns Pattern
*/
withValue(func) {
// Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'.
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
}
// alias
/**
* see {@link Pattern#withValue}
*/
fmap(func) {
return this.withValue(func);
}
_filterEvents(event_test) {
return new Pattern((state) => this.query(state).filter(event_test));
/**
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern
*/
_filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test));
}
/**
* As with {@link Pattern#_filterHaps}, but the function is applied to values
* inside haps.
* @param {Function} value_test
* @returns Pattern
*/
_filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
}
/**
* Returns a new pattern, with haps containing undefined values removed from
* query results.
* @returns Pattern
*/
_removeUndefineds() {
return this._filterValues((val) => val != undefined);
}
/**
* Returns a new pattern, with all haps without onsets filtered out. A hap
* with an onset is one with a `whole` timespan that begins at the same time
* as its `part` timespan.
* @returns Pattern
*/
onsetsOnly() {
// Returns a new pattern that will only return events where the start
// Returns a new pattern that will only return haps where the start
// of the 'whole' timespan matches the start of the 'part'
// timespan, i.e. the events that include their 'onset'.
return this._filterEvents((hap) => hap.hasOnset());
// timespan, i.e. the haps that include their 'onset'.
return this._filterHaps((hap) => hap.hasOnset());
}
/**
* Returns a new pattern, with 'continuous' haps (those without 'whole'
* timespans) removed from query results.
* @returns Pattern
*/
discreteOnly() {
// removes continuous events that don't have a 'whole' timespan
return this._filterEvents((hap) => hap.whole);
// removes continuous haps that don't have a 'whole' timespan
return this._filterHaps((hap) => hap.whole);
}
_appWhole(whole_func, pat_val) {
@@ -154,27 +256,38 @@ export class Pattern {
// pattern of functions.
const pat_func = this;
const query = function (state) {
const event_funcs = pat_func.query(state);
const event_vals = pat_val.query(state);
const apply = function (event_func, event_val) {
const s = event_func.part.intersection(event_val.part);
const hap_funcs = pat_func.query(state);
const hap_vals = pat_val.query(state);
const apply = function (hap_func, hap_val) {
const s = hap_func.part.intersection(hap_val.part);
if (s == undefined) {
return undefined;
}
return new Hap(
whole_func(event_func.whole, event_val.whole),
whole_func(hap_func.whole, hap_val.whole),
s,
event_func.value(event_val.value),
event_val.combineContext(event_func),
hap_func.value(hap_val.value),
hap_val.combineContext(hap_func),
);
};
return flatten(
event_funcs.map((event_func) => removeUndefineds(event_vals.map((event_val) => apply(event_func, event_val)))),
hap_funcs.map((hap_func) => removeUndefineds(hap_vals.map((hap_val) => apply(hap_func, hap_val)))),
);
};
return new Pattern(query);
}
/**
* When this method is called on a pattern of functions, it matches its haps
* with those in the given pattern of values. A new pattern is returned, with
* each matching value applied to the corresponding function.
*
* In this `appBoth` variant, where timespans of the function and value haps
* are not the same but do intersect, the resulting hap has a timespan of the
* intersection. This applies to both the part and the whole timespan.
* @param {Pattern} pat_val
* @returns Pattern
*/
appBoth(pat_val) {
// Tidal's <*>
const whole_func = function (span_a, span_b) {
@@ -186,14 +299,23 @@ export class Pattern {
return this._appWhole(whole_func, pat_val);
}
/**
* As with {@link Pattern#appBoth}, but the `whole` timespan is not the intersection,
* but the timespan from the function of patterns that this method is called
* on. In practice, this means that the pattern structure, including onsets,
* are preserved from the pattern of functions (often referred to as the left
* hand or inner pattern).
* @param {Pattern} pat_val
* @returns Pattern
*/
appLeft(pat_val) {
const pat_func = this;
const query = function (state) {
const haps = [];
for (const hap_func of pat_func.query(state)) {
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
for (const hap_val of event_vals) {
const hap_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
for (const hap_val of hap_vals) {
const new_whole = hap_func.whole;
const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) {
@@ -209,6 +331,13 @@ export class Pattern {
return new Pattern(query);
}
/**
* As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the
* pattern of values, i.e. structure is preserved from the right hand/outer
* pattern.
* @param {Pattern} pat_val
* @returns Pattern
*/
appRight(pat_val) {
const pat_func = this;
@@ -232,6 +361,13 @@ export class Pattern {
return new Pattern(query);
}
/**
* Queries the pattern for the first cycle, returning Haps. Mainly of use when
* debugging a pattern.
* @param {Boolean} with_context - set to true, otherwise the context field
* will be stripped from the resulting haps.
* @returns [Hap]
*/
firstCycle(with_context = false) {
var self = this;
if (!with_context) {
@@ -240,18 +376,30 @@ export class Pattern {
return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))));
}
/**
* Accessor for a list of values returned by querying the first cycle.
*/
get _firstCycleValues() {
return this.firstCycle().map((hap) => hap.value);
}
/**
* More human-readable version of the {@link Pattern#_firstCycleValues} accessor.
*/
get _showFirstCycle() {
return this.firstCycle().map(
(hap) => `${hap.value}: ${hap.whole.begin.toFraction()} - ${hap.whole.end.toFraction()}`,
);
}
_sortEventsByPart() {
return this._withEvents((events) =>
events.sort((a, b) =>
/**
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
* of use when comparing two patterns for equality, in tests.
* @returns Pattern
*/
_sortHapsByPart() {
return this._withHaps((haps) =>
haps.sort((a, b) =>
a.part.begin
.sub(b.part.begin)
.or(a.part.end.sub(b.part.end))
@@ -288,68 +436,112 @@ export class Pattern {
}
_asNumber(dropfails = false, softfail = false) {
return this._withEvent((event) => {
const asNumber = Number(event.value);
return this._withHap((hap) => {
const asNumber = Number(hap.value);
if (!isNaN(asNumber)) {
return event.withValue(() => asNumber);
return hap.withValue(() => asNumber);
}
const specialValue = {
e: Math.E,
pi: Math.PI,
}[event.value];
}[hap.value];
if (typeof specialValue !== 'undefined') {
return event.withValue(() => specialValue);
return hap.withValue(() => specialValue);
}
if (isNote(event.value)) {
if (isNote(hap.value)) {
// set context type to midi to let the player know its meant as midi number and not as frequency
return new Hap(event.whole, event.part, toMidi(event.value), { ...event.context, type: 'midi' });
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
}
if (dropfail) {
if (dropfails) {
// return 'nothing'
return undefined;
}
if (softfail) {
// return original hap
return event;
return hap;
}
throw new Error('cannot parse as number: "' + event.value + '"');
return event;
throw new Error('cannot parse as number: "' + hap.value + '"');
return hap;
});
if (dropfail) {
return result._removeUndefineds();
}
return result;
}
/**
* Assumes a numerical pattern. Returns a new pattern with all values rounded
* to the nearest integer.
* @returns Pattern
*/
round() {
return this._asNumber().fmap((v) => Math.round(v));
}
/**
* Assumes a numerical pattern. Returns a new pattern with all values set to
* their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2`
* replaced with `-5`.
* @returns Pattern
*/
floor() {
return this._asNumber().fmap((v) => Math.floor(v));
}
/**
* Assumes a numerical pattern. Returns a new pattern with all values set to
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
* replaced with `-4`.
* @returns Pattern
*/
ceil() {
return this._asNumber().fmap((v) => Math.ceil(v));
}
/**
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
* 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
* @returns Pattern
*/
_toBipolar() {
return this.fmap((x) => x * 2 - 1);
}
/**
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
* 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1
* @returns Pattern
*/
_fromBipolar() {
return this.fmap((x) => (x + 1) / 2);
}
// Assumes source pattern of numbers in range 0..1
/**
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
* 1. Returns a new pattern with values scaled to the given min/max range.
* @param {Number} min
* @param {Number} max
* @returns Pattern
*/
range(min, max) {
return this.mul(max - min).add(min);
}
/**
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
* 1. Returns a new pattern with values scaled to the given min/max range,
* following an exponential curve.
* @param {Number} min
* @param {Number} max
* @returns Pattern
*/
rangex(min, max) {
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
}
// Assumes source pattern of numbers in range -1..1
/**
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
* 1. Returns a new pattern with values scaled to the given min/max range.
* @param {Number} min
* @param {Number} max
* @returns Pattern
*/
range2(min, max) {
return this._fromBipolar().range(min, max);
}
@@ -393,7 +585,7 @@ export class Pattern {
join() {
// Flattens a pattern of patterns into a pattern, where wholes are
// the intersection of matched inner and outer events.
// the intersection of matched inner and outer haps.
return this.bind(id);
}
@@ -403,7 +595,7 @@ export class Pattern {
outerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are
// taken from inner events.
// taken from inner haps.
return this.outerBind(id);
}
@@ -413,35 +605,35 @@ export class Pattern {
innerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are
// taken from inner events.
// taken from inner haps.
return this.innerBind(id);
}
// Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern events
// Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern haps
_trigJoin(cycleZero = false) {
const pat_of_pats = this;
return new Pattern((state) => {
return (
pat_of_pats
// drop continuous events from the outer pattern.
// drop continuous haps from the outer pattern.
.discreteOnly()
.query(state)
.map((outer_hap) => {
return (
outer_hap.value
// trig = align the inner pattern cycle start to outer pattern events
// Trigzero = align the inner pattern cycle zero to outer pattern events
// trig = align the inner pattern cycle start to outer pattern haps
// Trigzero = align the inner pattern cycle zero to outer pattern haps
.late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
.query(state)
.map((inner_hap) =>
new Hap(
// Supports continuous events in the inner pattern
// Supports continuous haps in the inner pattern
inner_hap.whole ? inner_hap.whole.intersection(outer_hap.whole) : undefined,
inner_hap.part.intersection(outer_hap.part),
inner_hap.value,
).setContext(outer_hap.combineContext(inner_hap)),
)
// Drop events that didn't intersect
// Drop haps that didn't intersect
.filter((hap) => hap.part)
);
})
@@ -531,7 +723,7 @@ export class Pattern {
const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
return new TimeSpan(begin, end);
};
return this.withQuerySpan(qf).withEventSpan(ef)._splitQueries();
return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
}
_compress(b, e) {
@@ -545,15 +737,43 @@ export class Pattern {
return this._compress(span.begin, span.end);
}
/**
* Speed up a pattern by the given factor.
*
* @name fast
* @memberof Pattern
* @param {number | Pattern} factor speed up factor
* @returns Pattern
* @example
* seq(e5, b4, d5, c5).fast(2)
*/
_fast(factor) {
const fastQuery = this.withQueryTime((t) => t.mul(factor));
return fastQuery.withEventTime((t) => t.div(factor));
return fastQuery.withHapTime((t) => t.div(factor));
}
/**
* Slow down a pattern over the given number of cycles.
*
* @name slow
* @memberof Pattern
* @param {number | Pattern} factor slow down factor
* @returns Pattern
* @example
* seq(e5, b4, d5, c5).slow(2)
*/
_slow(factor) {
return this._fast(Fraction(1).div(factor));
}
_inside(factor, f) {
return f(this._slow(factor))._fast(factor);
}
_outside(factor, f) {
return f(this._fast(factor))._slow(factor);
}
_ply(factor) {
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
}
@@ -582,7 +802,7 @@ export class Pattern {
_early(offset) {
// Equivalent of Tidal's <~ operator
offset = Fraction(offset);
return this.withQueryTime((t) => t.add(offset)).withEventTime((t) => t.sub(offset));
return this.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
}
_late(offset) {
@@ -596,7 +816,7 @@ export class Pattern {
s = Fraction(s);
const d = e.sub(s);
return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
.withEventSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
._splitQueries();
}
@@ -636,7 +856,7 @@ export class Pattern {
}
log() {
return this._withEvent((e) => {
return this._withHap((e) => {
return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) });
});
}
@@ -680,6 +900,16 @@ export class Pattern {
return slowcatPrime(...pats);
}
/**
* Returns a new pattern where every other cycle is played once, twice as
* fast, and offset in time by one quarter of a cycle. Creates a kind of
* breakbeat feel.
* @returns Pattern
*/
brak() {
return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
}
rev() {
const pat = this;
const query = function (state) {
@@ -788,13 +1018,6 @@ export class Pattern {
return this._chunk(n, func, true);
}
edit(...funcs) {
return stack(...funcs.map((func) => func(this)));
}
pipe(func) {
return func(this);
}
_bypass(on) {
on = Boolean(parseInt(on));
return on ? silence : this;
@@ -804,19 +1027,45 @@ export class Pattern {
return silence;
}
// sets absolute duration of events
// sets absolute duration of haps
_duration(value) {
return this.withEventSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
}
// sets event relative duration of events
// sets hap relative duration of haps
_legato(value) {
return this.withEventSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
}
_velocity(velocity) {
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
}
// move this to controls? (speed and unit are controls)
_loopAt(factor, cps = 1) {
return this.speed((1 / factor) * cps)
.unit('c')
.slow(factor);
}
onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
}
log(func = id) {
return this._withHap((hap) =>
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
console.log(func(...args));
},
}),
);
}
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
}
// TODO - adopt value.mjs fully..
@@ -837,7 +1086,7 @@ function _composeOp(a, b, func) {
}
// Make composers
(function() {
(function () {
const num = (pat) => pat._asNumber();
const numOrString = (pat) => pat._asNumber(false, true);
@@ -961,8 +1210,14 @@ Pattern.prototype.factories = {
// Nothing
export const silence = new Pattern((_) => []);
/** A discrete value that repeats once per cycle:
*
* @param {any} value - The value to repeat
* @returns {Pattern}
* @example
* pure('e4')
*/
export function pure(value) {
// A discrete value that repeats once per cycle
function query(state) {
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
}
@@ -971,7 +1226,15 @@ export function pure(value) {
export function isPattern(thing) {
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
return thing instanceof Pattern;
const is = thing instanceof Pattern || thing._Pattern;
if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel.cycles/core.
Please check by running "npm ls @strudel.cycles/core".`,
);
}
return is;
}
export function reify(thing) {
@@ -982,8 +1245,13 @@ export function reify(thing) {
return pure(thing);
}
// Basic functions for combining patterns
/** The given items are played at the same time at the same length:
*
* @param {...any} items - The items to stack
* @return {Pattern}
* @example
* stack(g3, b3, [e4, d4])
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
@@ -991,10 +1259,17 @@ export function stack(...pats) {
return new Pattern(query);
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: {@link cat}
*
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* slowcat(e5, b4, [d5, c5])
*
*/
export function slowcat(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle.
// Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
@@ -1010,14 +1285,16 @@ export function slowcat(...pats) {
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
// be the second (rather than fourth) cycle from the first pattern.
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
return pat.withEventTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
};
return new Pattern(query)._splitQueries();
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
* @param {...any} items - The items to concatenate
* @return {Pattern}
*/
export function slowcatPrime(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
pats = pats.map(reify);
const query = function (state) {
const pat_n = Math.floor(state.span.begin) % pats.length;
@@ -1027,18 +1304,33 @@ export function slowcatPrime(...pats) {
return new Pattern(query)._splitQueries();
}
/** Concatenation: as with {@link slowcat}, but squashes a cycle from each pattern into one cycle
*
* Synonyms: {@link seq}, {@link sequence}
*
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* fastcat(e5, b4, [d5, c5])
* // sequence(e5, b4, [d5, c5])
* // seq(e5, b4, [d5, c5])
*/
export function fastcat(...pats) {
// Concatenation: as with slowcat, but squashes a cycle from each
// pattern into one cycle
return slowcat(...pats)._fast(pats.length);
}
/** See {@link slowcat} */
export function cat(...pats) {
return slowcat(...pats);
}
/** Like {@link fastcat}, but where each step has a temporal weight:
* @param {...Array} items - The items to concatenate
* @return {Pattern}
* @example
* timeCat([3,e3],[1, g3])
*/
export function timeCat(...timepats) {
// Like cat, but where each step has a temporal 'weight'
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
let begin = Fraction(0);
const pats = [];
@@ -1050,11 +1342,12 @@ export function timeCat(...timepats) {
return stack(...pats);
}
/** See {@link fastcat} */
export function sequence(...pats) {
return fastcat(...pats);
}
// shorthand for sequence
/** See {@link fastcat} */
export function seq(...pats) {
return fastcat(...pats);
}
@@ -1118,6 +1411,7 @@ export function pr(args) {
}
export const add = curry((a, pat) => pat.add(a));
export const chop = curry((a, pat) => pat.chop(a));
export const chunk = curry((a, pat) => pat.chunk(a));
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
export const div = curry((a, pat) => pat.div(a));
@@ -1204,6 +1498,10 @@ Pattern.prototype.chunkBack = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._chunkBack)(...args, this);
};
Pattern.prototype.loopAt = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._loopAt)(...args, this);
};
Pattern.prototype.zoom = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._zoom)(...args, this);
@@ -1212,6 +1510,14 @@ Pattern.prototype.compress = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._compress)(...args, this);
};
Pattern.prototype.outside = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._outside)(...args, this);
};
Pattern.prototype.inside = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._inside)(...args, this);
};
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
Pattern.prototype.bootstrap = function () {
+104 -5
View File
@@ -22,17 +22,61 @@ export const signal = (func) => {
export const isaw = signal((t) => 1 - (t % 1));
export const isaw2 = isaw._toBipolar();
/**
* A sawtooth signal between 0 and 1.
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4))
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4)
*
*/
export const saw = signal((t) => t % 1);
export const saw2 = saw._toBipolar();
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
* A sine signal between 0 and 1.
*
* @return {Pattern}
* @example
* sine.segment(16).range(0,15).slow(2).scale('C minor')
*
*/
export const sine = sine2._fromBipolar();
/**
* A cosine signal between 0 and 1.
*
* @return {Pattern}
* @example
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor')
*
*/
export const cosine = sine._early(Fraction(1).div(4));
export const cosine2 = sine2._early(Fraction(1).div(4));
/**
* A square signal between 0 and 1.
*
* @return {Pattern}
* @example
* square.segment(2).range(0,7).scale('C minor')
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
export const square2 = square._toBipolar();
/**
* A triangle signal between 0 and 1.
*
* @return {Pattern}
* @example
* triangle.segment(2).range(0,7).scale('C minor')
*
*/
export const tri = fastcat(isaw, saw);
export const tri2 = fastcat(isaw2, saw2);
@@ -66,7 +110,14 @@ const timeToRandsPrime = (seed, n) => {
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* A continuous pattern of random numbers, between 0 and 1
*/
export const rand = signal(timeToRand);
/**
* A continuous pattern of random numbers, between -1 and 1
*/
export const rand2 = rand._toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
@@ -75,19 +126,67 @@ export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
export const chooseWith = (pat, xs) => {
export const __chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.range(0, xs.length)
.fmap((i) => xs[Math.floor(i)])
.outerJoin();
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
};
/**
* Choose from the list of values (or patterns of values) using the given
* pattern of numbers, which should be in the range of 0..1
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin();
};
/**
* As with {chooseWith}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const chooseInWith = (pat, xs) => {
return __chooseWith(pat, xs).innerJoin();
};
/**
* Chooses randomly from the given list of values.
* @param {...any} xs
* @returns {Pattern} - a continuous pattern.
*/
export const choose = (...xs) => chooseWith(rand, xs);
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose = function (...xs) {
return chooseWith(this, xs);
};
/**
* As with choose, but the pattern that this method is called on should be
* in the range -1 .. 1
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose2 = function (...xs) {
return chooseWith(this._fromBipolar(), xs);
};
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
export const randcat = chooseCycles;
const _wchooseWith = function (pat, ...pairs) {
const values = pairs.map((pair) => reify(pair[0]));
const weights = [];
+11 -5
View File
@@ -6,7 +6,13 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, patternify2 } from './index.mjs';
const synth = window?.speechSynthesis;
let synth;
try {
synth = window?.speechSynthesis;
} catch (err) {
console.warn('cannot use window: not in browser?');
}
let allVoices = synth?.getVoices();
// console.log('voices', allVoices);
@@ -26,11 +32,11 @@ function speak(words, lang, voice) {
}
Pattern.prototype._speak = function (lang, voice) {
return this._withEvent((event) => {
const onTrigger = (time, event) => {
speak(event.value, lang, voice);
return this._withHap((hap) => {
const onTrigger = (time, hap) => {
speak(hap.value, lang, voice);
};
return event.setContext({ ...event.context, onTrigger });
return hap.setContext({ ...hap.context, onTrigger });
});
};
+36 -19
View File
@@ -41,6 +41,7 @@ import {
tri2,
id,
ply,
rev
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -57,7 +58,7 @@ const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle());
return assert.deepStrictEqual(a._sortHapsByPart().firstCycle(), b._sortHapsByPart().firstCycle());
};
describe('TimeSpan', function () {
@@ -277,10 +278,7 @@ describe('Pattern', function () {
);
});
it('can SqueezeOut() structure', () => {
sameFirst(
sequence(1, [2, 3]).keepifSqueezeOut(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()', function () {
@@ -320,7 +318,7 @@ describe('Pattern', function () {
});
});
describe('setSqueeze()', () => {
it('Can squeeze one pattern inside the events of another', () => {
it('Can squeeze one pattern inside the haps of another', () => {
sameFirst(
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
sequence(
@@ -394,7 +392,7 @@ describe('Pattern', function () {
});
it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
// .fast(sequence(1,silence) is a quick hack to cut an event in two..
// .fast(sequence(1,silence) is a quick hack to cut a hap in two..
assert.deepStrictEqual(
pure('a').fast(sequence(1, 4)).firstCycle(),
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
@@ -439,6 +437,20 @@ describe('Pattern', function () {
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
});
});
describe('inside', () => {
it('can rev inside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev),
sequence('b', 'a', 'd', 'c')
);
});
});
describe('outside', () => {
it('can rev outside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev),
sequence('d', 'c')
);
});
});
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(
@@ -468,7 +480,7 @@ describe('Pattern', function () {
});
it('Can alternate', function () {
assert.deepStrictEqual(
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortEventsByPart().firstCycle(),
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortHapsByPart().firstCycle(),
fastcat(13, 10, 13, 10).firstCycle(),
);
});
@@ -589,6 +601,11 @@ describe('Pattern', function () {
);
});
});
describe('brak()', () => {
it('Can make something a bit breakbeaty', () => {
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
});
});
describe('timeCat()', function () {
it('Can concatenate patterns with different relative durations', function () {
assert.deepStrictEqual(
@@ -679,7 +696,7 @@ describe('Pattern', function () {
});
});
describe('_setContext()', () => {
it('Can set the event context', () => {
it('Can set the hap context', () => {
assert.deepStrictEqual(
pure('a')
._setContext([
@@ -701,7 +718,7 @@ describe('Pattern', function () {
});
});
describe('_withContext()', () => {
it('Can update the event context', () => {
it('Can update the hap context', () => {
assert.deepStrictEqual(
pure('a')
._setContext([
@@ -756,13 +773,13 @@ describe('Pattern', function () {
});
});
describe('early', () => {
it('Can shift an event earlier', () => {
it('Can shift a hap earlier', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
]);
});
it('Can shift an event earlier, into negative time', () => {
it('Can shift a hap earlier, into negative time', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
@@ -780,9 +797,9 @@ describe('Pattern', function () {
describe('jux', () => {
it('Can juxtapose', () => {
assert.deepStrictEqual(
pure({ a: 1 }).jux(fast(2))._sortEventsByPart().firstCycle(),
pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
._sortEventsByPart()
._sortHapsByPart()
.firstCycle(),
);
});
@@ -790,9 +807,9 @@ describe('Pattern', function () {
describe('juxBy', () => {
it('Can juxtapose by half', () => {
assert.deepStrictEqual(
pure({ a: 1 }).juxBy(0.5, fast(2))._sortEventsByPart().firstCycle(),
pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
._sortEventsByPart()
._sortHapsByPart()
.firstCycle(),
);
});
@@ -821,7 +838,7 @@ describe('Pattern', function () {
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
);
});
it('Doesnt drop events in the 9th cycle', () => {
it('Doesnt drop haps in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
});
@@ -848,7 +865,7 @@ describe('Pattern', function () {
});
it('Can chop(2,3)', () => {
assert.deepStrictEqual(
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortEventsByPart().firstCycle(),
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
sequence(
[
{ sound: 'a', begin: 0, end: 0.5 },
@@ -860,7 +877,7 @@ describe('Pattern', function () {
{ sound: 'a', begin: 2 / 3, end: 1 },
],
)
._sortEventsByPart()
._sortHapsByPart()
.firstCycle(),
);
});
+22 -9
View File
@@ -10,7 +10,7 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#b]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -24,38 +24,51 @@ export const toMidi = (note) => {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1 }[char], 0) || 0;
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
};
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct;
};
// 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 = (event) => {
let { value: note, context } = event;
export const getPlayableNoteValue = (hap) => {
let { value: note, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(event.value);
note = fromMidi(hap.value);
} else if (typeof note === 'string' && !isNote(note)) {
throw new Error('not a note: ' + note);
}
return note;
};
export const getFrequency = (event) => {
let { value, context } = event;
export const getFrequency = (hap) => {
let { value, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object' && value.freq) {
return value.freq;
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(event.value);
value = fromMidi(hap.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(event.value));
value = fromMidi(toMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency:' + value);
}
+1
View File
@@ -7,6 +7,7 @@ class Strudel extends HTMLElement {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.tidalcycles.org/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
iframe.setAttribute('src', src);
iframe.setAttribute('width', '600');
iframe.setAttribute('height', '400');
+1
View File
@@ -1,4 +1,5 @@
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<!-- <script src="./embed.js"></script> -->
<strudel-repl>
<!--
"a4 [a3 c3] a3 c3".color('#F9D649')
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/embed",
"version": "0.0.2",
"version": "0.1.1",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
@@ -20,6 +20,5 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
+6 -2
View File
@@ -11,11 +11,15 @@ npm i @strudel.cycles/eval --save
## Example
<!-- TODO: -extend +evalScope -->
```js
import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
extend(strudel); // add strudel to eval scope
evalScope(
import('@strudel.cycles/core'),
// import other strudel packages here
); // add strudel to eval scope
async function run(code) {
const { pattern } = await evaluate(code);
+26 -4
View File
@@ -7,17 +7,39 @@ This program is free software: you can redistribute it and/or modify it under th
import shapeshifter from './shapeshifter.mjs';
import * as strudel from '@strudel.cycles/core';
const { isPattern } = strudel;
const { isPattern, Pattern } = strudel;
export const extend = (...args) => {
// TODO: find a way to make args available to eval without adding it to global scope...
// sadly, "with" does not work in strict mode
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
Object.assign(globalThis, ...args);
};
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('@strudel.cycles/eval evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str) {
return Function('"use strict";return (' + str + ')')();
}
export const evaluate = async (code) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
let evaluated = await eval(shapeshifted);
let evaluated = await safeEval(shapeshifted);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
+1
View File
@@ -0,0 +1 @@
export * from './evaluate.mjs';
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.5",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/eval",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
+4 -4
View File
@@ -1,8 +1,9 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.5",
"version": "0.1.3",
"description": "Code evaluator for strudel",
"main": "evaluate.mjs",
"main": "index.mjs",
"type": "module",
"directories": {
"test": "test"
},
@@ -13,7 +14,6 @@
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"type": "module",
"keywords": [
"tidalcycles",
"strudel",
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/core": "^0.1.2",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
+1
View File
@@ -12,6 +12,7 @@ import * as strudel from '@strudel.cycles/core';
const { fastcat } = strudel;
extend({ mini }, strudel);
// TODO: test evalScope
describe('evaluate', () => {
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
+3
View File
@@ -0,0 +1,3 @@
import './midi.mjs';
export * from './midi.mjs';
+7 -7
View File
@@ -39,11 +39,11 @@ Pattern.prototype.midi = function (output, channel = 1) {
}')`,
);
}
return this._withEvent((event) => {
// const onTrigger = (time: number, event: any) => {
const onTrigger = (time, event) => {
let note = event.value;
const velocity = event.context?.velocity ?? 0.9;
return this._withHap((hap) => {
// const onTrigger = (time: number, hap: any) => {
const onTrigger = (time, hap) => {
let note = hap.value;
const velocity = hap.context?.velocity ?? 0.9;
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
@@ -75,10 +75,10 @@ Pattern.prototype.midi = function (output, channel = 1) {
// await enableWebMidi()
device.playNote(note, channel, {
time,
duration: event.duration.valueOf() * 1000 - 5,
duration: hap.duration.valueOf() * 1000 - 5,
velocity,
});
};
return event.setContext({ ...event.context, onTrigger });
return hap.setContext({ ...hap.context, onTrigger });
});
};
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.6",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/midi",
"version": "0.0.4",
"license": "GPL-3.0-or-later",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"tone": "^14.7.77",
"webmidi": "^2.5.2"
+3 -3
View File
@@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.6",
"version": "0.1.3",
"description": "Midi API for strudel",
"main": "midi.mjs",
"main": "index.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
@@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.0.6",
"@strudel.cycles/tone": "^0.1.3",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
+2
View File
@@ -0,0 +1,2 @@
export * from './mini.mjs';
export * from './krill-parser.js';
+164 -158
View File
@@ -1,10 +1,4 @@
/*
krill-parser.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill-parser.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Generated by Peggy 1.2.0.
// Generated by Peggy 2.0.1.
//
// https://peggyjs.org/
@@ -16,6 +10,7 @@ function peg$subclass(child, parent) {
function peg$SyntaxError(message, expected, found, location) {
var self = Error.call(this, message);
// istanbul ignore next Check is a necessary evil to support older environments
if (Object.setPrototypeOf) {
Object.setPrototypeOf(self, peg$SyntaxError.prototype);
}
@@ -51,14 +46,15 @@ peg$SyntaxError.prototype.format = function(sources) {
var loc = this.location.source + ":" + s.line + ":" + s.column;
if (src) {
var e = this.location.end;
var filler = peg$padEnd("", s.line.toString().length);
var filler = peg$padEnd("", s.line.toString().length, ' ');
var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n"
+ filler + " |\n"
+ s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1)
+ peg$padEnd("", last - s.column, "^");
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^");
} else {
str += "\n at " + loc;
}
@@ -79,7 +75,7 @@ peg$SyntaxError.buildMessage = function(expected, found) {
: classEscape(part);
});
return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]";
return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]";
},
any: function() {
@@ -185,31 +181,32 @@ function peg$parse(input, options) {
var peg$c7 = "#";
var peg$c8 = "^";
var peg$c9 = "_";
var peg$c10 = "[";
var peg$c11 = "]";
var peg$c12 = "<";
var peg$c13 = ">";
var peg$c14 = "@";
var peg$c15 = "!";
var peg$c16 = "(";
var peg$c17 = ")";
var peg$c18 = "/";
var peg$c19 = "*";
var peg$c20 = "%";
var peg$c21 = "struct";
var peg$c22 = "target";
var peg$c23 = "euclid";
var peg$c24 = "slow";
var peg$c25 = "rotL";
var peg$c26 = "rotR";
var peg$c27 = "fast";
var peg$c28 = "scale";
var peg$c29 = "//";
var peg$c30 = "cat";
var peg$c31 = "$";
var peg$c32 = "setcps";
var peg$c33 = "setbpm";
var peg$c34 = "hush";
var peg$c10 = ":";
var peg$c11 = "[";
var peg$c12 = "]";
var peg$c13 = "<";
var peg$c14 = ">";
var peg$c15 = "@";
var peg$c16 = "!";
var peg$c17 = "(";
var peg$c18 = ")";
var peg$c19 = "/";
var peg$c20 = "*";
var peg$c21 = "%";
var peg$c22 = "struct";
var peg$c23 = "target";
var peg$c24 = "euclid";
var peg$c25 = "slow";
var peg$c26 = "rotL";
var peg$c27 = "rotR";
var peg$c28 = "fast";
var peg$c29 = "scale";
var peg$c30 = "//";
var peg$c31 = "cat";
var peg$c32 = "$";
var peg$c33 = "setcps";
var peg$c34 = "setbpm";
var peg$c35 = "hush";
var peg$r0 = /^[1-9]/;
var peg$r1 = /^[eE]/;
@@ -235,32 +232,33 @@ function peg$parse(input, options) {
var peg$e14 = peg$literalExpectation("#", false);
var peg$e15 = peg$literalExpectation("^", false);
var peg$e16 = peg$literalExpectation("_", false);
var peg$e17 = peg$literalExpectation("[", false);
var peg$e18 = peg$literalExpectation("]", false);
var peg$e19 = peg$literalExpectation("<", false);
var peg$e20 = peg$literalExpectation(">", false);
var peg$e21 = peg$literalExpectation("@", false);
var peg$e22 = peg$literalExpectation("!", false);
var peg$e23 = peg$literalExpectation("(", false);
var peg$e24 = peg$literalExpectation(")", false);
var peg$e25 = peg$literalExpectation("/", false);
var peg$e26 = peg$literalExpectation("*", false);
var peg$e27 = peg$literalExpectation("%", false);
var peg$e28 = peg$literalExpectation("struct", false);
var peg$e29 = peg$literalExpectation("target", false);
var peg$e30 = peg$literalExpectation("euclid", false);
var peg$e31 = peg$literalExpectation("slow", false);
var peg$e32 = peg$literalExpectation("rotL", false);
var peg$e33 = peg$literalExpectation("rotR", false);
var peg$e34 = peg$literalExpectation("fast", false);
var peg$e35 = peg$literalExpectation("scale", false);
var peg$e36 = peg$literalExpectation("//", false);
var peg$e37 = peg$classExpectation(["\n"], true, false);
var peg$e38 = peg$literalExpectation("cat", false);
var peg$e39 = peg$literalExpectation("$", false);
var peg$e40 = peg$literalExpectation("setcps", false);
var peg$e41 = peg$literalExpectation("setbpm", false);
var peg$e42 = peg$literalExpectation("hush", false);
var peg$e17 = peg$literalExpectation(":", false);
var peg$e18 = peg$literalExpectation("[", false);
var peg$e19 = peg$literalExpectation("]", false);
var peg$e20 = peg$literalExpectation("<", false);
var peg$e21 = peg$literalExpectation(">", false);
var peg$e22 = peg$literalExpectation("@", false);
var peg$e23 = peg$literalExpectation("!", false);
var peg$e24 = peg$literalExpectation("(", false);
var peg$e25 = peg$literalExpectation(")", false);
var peg$e26 = peg$literalExpectation("/", false);
var peg$e27 = peg$literalExpectation("*", false);
var peg$e28 = peg$literalExpectation("%", false);
var peg$e29 = peg$literalExpectation("struct", false);
var peg$e30 = peg$literalExpectation("target", false);
var peg$e31 = peg$literalExpectation("euclid", false);
var peg$e32 = peg$literalExpectation("slow", false);
var peg$e33 = peg$literalExpectation("rotL", false);
var peg$e34 = peg$literalExpectation("rotR", false);
var peg$e35 = peg$literalExpectation("fast", false);
var peg$e36 = peg$literalExpectation("scale", false);
var peg$e37 = peg$literalExpectation("//", false);
var peg$e38 = peg$classExpectation(["\n"], true, false);
var peg$e39 = peg$literalExpectation("cat", false);
var peg$e40 = peg$literalExpectation("$", false);
var peg$e41 = peg$literalExpectation("setcps", false);
var peg$e42 = peg$literalExpectation("setbpm", false);
var peg$e43 = peg$literalExpectation("hush", false);
var peg$f0 = function() { return parseFloat(text()); };
var peg$f1 = function(chars) { return chars.join("") };
@@ -294,7 +292,6 @@ function peg$parse(input, options) {
var peg$f29 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f30 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f31 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -810,6 +807,15 @@ function peg$parse(input, options) {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e16); }
}
if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 58) {
s0 = peg$c10;
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
}
}
}
}
@@ -852,11 +858,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s2 = peg$c10;
s2 = peg$c11;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -864,11 +870,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s6 = peg$c11;
s6 = peg$c12;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -896,11 +902,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 60) {
s2 = peg$c12;
s2 = peg$c13;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e20); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -908,11 +914,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 62) {
s6 = peg$c13;
s6 = peg$c14;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e21); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -976,11 +982,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 64) {
s1 = peg$c14;
s1 = peg$c15;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e21); }
if (peg$silentFails === 0) { peg$fail(peg$e22); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1004,11 +1010,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 33) {
s1 = peg$c15;
s1 = peg$c16;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e22); }
if (peg$silentFails === 0) { peg$fail(peg$e23); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1032,11 +1038,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c16;
s1 = peg$c17;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e23); }
if (peg$silentFails === 0) { peg$fail(peg$e24); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1060,11 +1066,11 @@ function peg$parse(input, options) {
}
s12 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 41) {
s13 = peg$c17;
s13 = peg$c18;
peg$currPos++;
} else {
s13 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e24); }
if (peg$silentFails === 0) { peg$fail(peg$e25); }
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1098,11 +1104,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s1 = peg$c18;
s1 = peg$c19;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e25); }
if (peg$silentFails === 0) { peg$fail(peg$e26); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1126,11 +1132,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 42) {
s1 = peg$c19;
s1 = peg$c20;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); }
if (peg$silentFails === 0) { peg$fail(peg$e27); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1154,11 +1160,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 37) {
s1 = peg$c20;
s1 = peg$c21;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); }
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1332,12 +1338,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c21) {
s1 = peg$c21;
if (input.substr(peg$currPos, 6) === peg$c22) {
s1 = peg$c22;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1361,12 +1367,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c22) {
s1 = peg$c22;
if (input.substr(peg$currPos, 6) === peg$c23) {
s1 = peg$c23;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
if (peg$silentFails === 0) { peg$fail(peg$e30); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1402,12 +1408,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5, s6, s7;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c23) {
s1 = peg$c23;
if (input.substr(peg$currPos, 6) === peg$c24) {
s1 = peg$c24;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e30); }
if (peg$silentFails === 0) { peg$fail(peg$e31); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1443,12 +1449,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c24) {
s1 = peg$c24;
if (input.substr(peg$currPos, 4) === peg$c25) {
s1 = peg$c25;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e31); }
if (peg$silentFails === 0) { peg$fail(peg$e32); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1472,12 +1478,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c25) {
s1 = peg$c25;
if (input.substr(peg$currPos, 4) === peg$c26) {
s1 = peg$c26;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
if (peg$silentFails === 0) { peg$fail(peg$e33); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1501,12 +1507,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c26) {
s1 = peg$c26;
if (input.substr(peg$currPos, 4) === peg$c27) {
s1 = peg$c27;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e33); }
if (peg$silentFails === 0) { peg$fail(peg$e34); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1530,12 +1536,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c27) {
s1 = peg$c27;
if (input.substr(peg$currPos, 4) === peg$c28) {
s1 = peg$c28;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e34); }
if (peg$silentFails === 0) { peg$fail(peg$e35); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1559,12 +1565,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.substr(peg$currPos, 5) === peg$c28) {
s1 = peg$c28;
if (input.substr(peg$currPos, 5) === peg$c29) {
s1 = peg$c29;
peg$currPos += 5;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e35); }
if (peg$silentFails === 0) { peg$fail(peg$e36); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1609,12 +1615,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 2) === peg$c29) {
s1 = peg$c29;
if (input.substr(peg$currPos, 2) === peg$c30) {
s1 = peg$c30;
peg$currPos += 2;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e36); }
if (peg$silentFails === 0) { peg$fail(peg$e37); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@@ -1623,7 +1629,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e37); }
if (peg$silentFails === 0) { peg$fail(peg$e38); }
}
while (s3 !== peg$FAILED) {
s2.push(s3);
@@ -1632,7 +1638,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e37); }
if (peg$silentFails === 0) { peg$fail(peg$e38); }
}
}
s1 = [s1, s2];
@@ -1649,21 +1655,21 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
s0 = peg$currPos;
if (input.substr(peg$currPos, 3) === peg$c30) {
s1 = peg$c30;
if (input.substr(peg$currPos, 3) === peg$c31) {
s1 = peg$c31;
peg$currPos += 3;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e38); }
if (peg$silentFails === 0) { peg$fail(peg$e39); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s3 = peg$c10;
s3 = peg$c11;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
@@ -1705,11 +1711,11 @@ function peg$parse(input, options) {
}
s7 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s8 = peg$c11;
s8 = peg$c12;
peg$currPos++;
} else {
s8 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1770,11 +1776,11 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 36) {
s3 = peg$c31;
s3 = peg$c32;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e39); }
if (peg$silentFails === 0) { peg$fail(peg$e40); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
@@ -1852,12 +1858,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c32) {
s1 = peg$c32;
if (input.substr(peg$currPos, 6) === peg$c33) {
s1 = peg$c33;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e40); }
if (peg$silentFails === 0) { peg$fail(peg$e41); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1881,12 +1887,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c33) {
s1 = peg$c33;
if (input.substr(peg$currPos, 6) === peg$c34) {
s1 = peg$c34;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e41); }
if (peg$silentFails === 0) { peg$fail(peg$e42); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1910,12 +1916,12 @@ function peg$parse(input, options) {
var s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c34) {
s1 = peg$c34;
if (input.substr(peg$currPos, 4) === peg$c35) {
s1 = peg$c35;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e42); }
if (peg$silentFails === 0) { peg$fail(peg$e43); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1938,35 +1944,34 @@ function peg$parse(input, options) {
}
var PatternStub = function(source, alignment)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.source_ = source;
}
var PatternStub = function(source, alignment)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.source_ = source;
}
var OperatorStub = function(name, args, source)
{
this.type_ = name;
this.arguments_ = args;
this.source_ = source;
}
var OperatorStub = function(name, args, source)
{
this.type_ = name;
this.arguments_ = args;
this.source_ = source;
}
var ElementStub = function(source, options)
{
this.type_ = "element";
this.source_ = source;
this.options_ = options;
this.location_ = location();
}
var CommandStub = function(name, options)
{
this.type_ = "command";
this.name_ = name;
this.options_ = options;
}
var ElementStub = function(source, options)
{
this.type_ = "element";
this.source_ = source;
this.options_ = options;
this.location_ = location();
}
var CommandStub = function(name, options)
{
this.type_ = "command";
this.name_ = name;
this.options_ = options;
}
peg$result = peg$startRuleFunction();
@@ -1990,5 +1995,6 @@ function peg$parse(input, options) {
export {
peg$SyntaxError as SyntaxError,
peg$parse as parse
};
+1 -1
View File
@@ -87,7 +87,7 @@ quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step = ws chars:step_char+ ws { return chars.join("") }
// define a sub cycle e.g. [1 2, 3 [4]]
+1031
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -1,11 +1,12 @@
{
"name": "@strudel.cycles/mini",
"version": "0.0.7",
"version": "0.1.3",
"description": "Mini notation for strudel",
"main": "mini.mjs",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
"test": "mocha --colors",
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs"
},
"repository": {
"type": "git",
@@ -25,8 +26,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
"@strudel.cycles/core": "^0.1.2",
"@strudel.cycles/eval": "^0.1.3",
"@strudel.cycles/tone": "^0.1.3"
},
"devDependencies": {
"peggy": "^2.0.1"
}
}
+21 -7
View File
@@ -5,26 +5,40 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import OSC from 'osc-js';
import { Pattern } from '@strudel.cycles/core';
import { Pattern, objectify } from '@strudel.cycles/core';
const comm = new OSC();
comm.open();
const latency = 0.1;
let startedAt = -1;
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
*
* @name osc
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this._withEvent((event) => {
const onTrigger = (time, event, currentTime) => {
return this._withHap((hap) => {
const onTrigger = (time, hap, currentTime, cps) => {
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const keyvals = Object.entries(objectify(event.value)).flat();
const offset = (time - currentTime + latency) * 1000;
const ts = Math.floor(Date.now() + offset);
if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000;
}
const controls = Object.assign({}, { cps, cycle, delta }, objectify(hap.value));
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + (time + latency) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
comm.send(bundle);
};
return event.setContext({ ...event.context, onTrigger });
return hap.setContext({ ...hap.context, onTrigger });
});
};
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/osc",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"osc-js": "^2.3.2"
}
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"version": "0.1.1",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
@@ -21,7 +21,9 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": ["Alex McLean <alex@slab.org>"],
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
+25
View File
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
!dist
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+4
View File
@@ -0,0 +1,4 @@
# @strudel.cycles/react
This package contains react hooks and components for strudel.
Example coming soon
File diff suppressed because one or more lines are too long
+684
View File
@@ -0,0 +1,684 @@
import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { HighlightStyle, tags } from '@codemirror/highlight';
import { useInView } from 'react-hook-inview';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import { Tone } from '@strudel.cycles/tone';
import { TimeSpan, State } from '@strudel.cycles/core';
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
/*
Credits for color palette:
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
const materialPalenightTheme = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '15px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '22px',
},
'.cm-line': {
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
// commented out because it looks bad in mini repl one liners
//'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: 'transparent',
color: '#676e95',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#c792ea' },
{ tag: tags.operator, color: '#89ddff' },
{ tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: tags.typeName, color: '#f07178' },
{ tag: tags.atom, color: '#f78c6c' },
{ tag: tags.number, color: '#ff5370' },
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: tags.string, color: '#c3e88d' },
{ tag: tags.special(tags.string), color: '#f07178' },
{ tag: tags.comment, color: stone },
{ tag: tags.variableName, color: '#f07178' },
{ tag: tags.tagName, color: '#ff5370' },
{ tag: tags.bracket, color: '#a2a1a4' },
{ tag: tags.meta, color: '#ffcb6b' },
{ tag: tags.attributeName, color: '#c792ea' },
{ tag: tags.propertyName, color: '#c792ea' },
{ tag: tags.className, color: '#decb6b' },
{ tag: tags.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash2, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash2 = Decoration.set([]);
}
}
}
return flash2;
} catch (err) {
console.warn("flash error", err);
return flash2;
}
},
provide: (f) => EditorView.decorations.from(f)
});
const flash = (view) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200);
};
const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
}
return highlights;
} catch (err) {
return highlights;
}
},
provide: (f) => EditorView.decorations.from(f)
});
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
display: "flex",
flexDirection: "column",
flex: "1 0 auto"
},
value,
onChange,
extensions: [
javascript(),
materialPalenight,
highlightField,
flashField
]
}));
}
/*
useCycle.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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/>.
*/
/* export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>;
onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props
} */
// function useCycle(props: UseCycleProps) {
function useCycle(props) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
}, time);
}, event.part.begin.valueOf());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
};
}
/*
usePostMessage.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.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/>.
*/
function usePostMessage(listener) {
useEffect(() => {
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}, [listener]);
return useCallback((data) => window.postMessage(data, '*'), []);
}
/*
useRepl.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.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/>.
*/
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
const onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return (time, event) => onDrawProp?.(time, event, activeCode);
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime, 1 /* cps */);
}
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog, defaultSynth],
),
onQuery: useCallback(
(state) => {
try {
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
},
[pattern],
),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
ready: !!pattern && !!activeCode,
});
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
cycle.setStarted(false);
setActiveCode(undefined);
}
});
const activateCode = useCallback(
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash(code));
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
},
[activeCode, dirty, code, cycle, autolink, id, broadcast],
);
// logs events of cycle
const logCycle = (_events, cycle) => {
if (_events.length) ;
};
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
setActiveCode,
activateCode,
activeCode,
pushLog,
hash,
};
}
/*
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
let highlights = []; // actively highlighted events
let lastEnd;
function useHighlighting({ view, pattern, active }) {
useEffect(() => {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
try {
const audioTime = Tone.getTransport().seconds;
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) });
}
frame = requestAnimationFrame(updateHighlights);
}
return () => {
cancelAnimationFrame(frame);
};
} else {
highlights = [];
view.dispatch({ effects: setHighlights.of([]) });
}
}
}, [pattern, active, view]);
}
var tailwind = '';
var style = '';
const container = "_container_xpa19_1";
const header = "_header_xpa19_5";
const buttons = "_buttons_xpa19_9";
const button = "_button_xpa19_9";
const buttonDisabled = "_buttonDisabled_xpa19_17";
const error = "_error_xpa19_21";
const body = "_body_xpa19_25";
var styles = {
container: container,
header: header,
buttons: buttons,
button: button,
buttonDisabled: buttonDisabled,
error: error,
body: body
};
function Icon({ type }) {
return /* @__PURE__ */ React.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__ */ React.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__ */ React.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__ */ React.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"
})
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
tune,
defaultSynth,
autolink: false,
onEvent
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
}, /* @__PURE__ */ React.createElement("div", {
className: styles.header
}, /* @__PURE__ */ React.createElement("div", {
className: styles.buttons
}, /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
onClick: () => togglePlay()
}, /* @__PURE__ */ React.createElement(Icon, {
type: cycle.started ? "pause" : "play"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(dirty ? styles.button : styles.buttonDisabled),
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh"
}))), error && /* @__PURE__ */ React.createElement("div", {
className: styles.error
}, error.message)), /* @__PURE__ */ React.createElement("div", {
className: styles.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
value: code,
onChange: setCode,
onViewChanged: setView
})));
}
/*
useWebMidi.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function useWebMidi(props) {
const { ready, connected, disconnected } = props;
const [loading, setLoading] = useState(true);
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
useEffect(() => {
enableWebMidi()
.then(() => {
// Reacting when a new device becomes available
WebMidi.addListener('connected', (e) => {
setOutputs([...WebMidi.outputs]);
connected?.(WebMidi, e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
setOutputs([...WebMidi.outputs]);
disconnected?.(WebMidi, e);
});
ready?.(WebMidi);
setLoading(false);
})
.catch((err) => {
if (err) {
console.error(err);
//throw new Error("Web Midi could not be enabled...");
console.warn('Web Midi could not be enabled..');
return;
}
});
}, [ready, connected, disconnected, outputs]);
const outputByName = (name) => WebMidi.getOutputByName(name);
return { loading, outputs, outputByName };
}
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
+1
View File
@@ -0,0 +1 @@
*,:before,:after{--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}.cm-editor{background-color:transparent!important}._container_xpa19_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(17 17 17 / var(--tw-bg-opacity))}._header_xpa19_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_xpa19_9{display:flex}._button_xpa19_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_xpa19_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_xpa19_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_xpa19_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_xpa19_25{position:relative;overflow:auto}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <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>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+3984
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@strudel.cycles/react",
"version": "0.1.4",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"exports": {
".": {
"require": "./dist/index.cjs.js",
"import": "./dist/index.es.js"
},
"./dist/style.css": "./dist/style.css"
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "^0.1.2",
"@strudel.cycles/eval": "^0.1.3",
"@strudel.cycles/tone": "^0.1.3",
"react-codemirror6": "^1.1.0",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9"
}
}
@@ -1,11 +1,12 @@
/*
setupTests.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/setupTests.js>
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css';
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
import { evalScope } from '@strudel.cycles/eval';
const defaultSynth = getDefaultSynth();
evalScope(
Tone,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
function App() {
return (
<div>
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
</div>
);
}
export default App;
@@ -1,10 +1,44 @@
import React from 'react';
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
// import { materialPalenight } from 'codemirror6-themes';
import { materialPalenight } from './themes/material-palenight';
import { materialPalenight } from '../themes/material-palenight';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200);
};
export const setHighlights = StateEffect.define();
const highlightField = StateField.define({
@@ -26,7 +60,8 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
}),
)
@@ -60,6 +95,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
javascript(),
materialPalenight,
highlightField,
flashField,
// theme, language, ...
]}
/>
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
export function Icon({ type }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="sc-h-5 sc-w-5" viewBox="0 0 20 20" fill="currentColor">
{
{
refresh: (
<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: (
<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: (
<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"
/>
),
}[type]
}
</svg>
);
}
@@ -0,0 +1,74 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
defaultSynth,
autolink: false,
onEvent,
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={cycle.started ? 'pause' : 'play'} />
</button>
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
</button>
</div>
{error && <div className={styles.error}>{error.message}</div>}
</div>
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
}
@@ -0,0 +1,27 @@
.container {
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#111111];
}
.header {
@apply sc-flex sc-justify-between sc-bg-slate-700 sc-border-t sc-border-slate-500;
}
.buttons {
@apply sc-flex;
}
.button {
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-700 sc-border-r sc-border-slate-500 sc-text-white hover:sc-bg-slate-600;
}
.buttonDisabled {
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-600 sc-text-slate-400 sc-cursor-not-allowed;
}
.error {
@apply sc-text-right sc-p-1 sc-text-sm sc-text-red-200;
}
.body {
@apply sc-overflow-auto sc-relative;
}
+3
View File
@@ -0,0 +1,3 @@
.cm-editor {
background-color: transparent !important;
}
+2 -1
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/>.
*/
export default function cx(...classes) { // : Array<string | undefined>
export default function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { setHighlights } from './CodeMirror6';
import { setHighlights } from '../components/CodeMirror6';
import { Tone } from '@strudel.cycles/tone';
let highlights = []; // actively highlighted events
@@ -14,7 +14,10 @@ function useHighlighting({ view, pattern, active }) {
function updateHighlights() {
try {
const audioTime = Tone.getTransport().seconds;
const span = [lastEnd || audioTime, audioTime + 1 / 60];
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
@@ -36,6 +36,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
@@ -57,7 +59,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime);
onTrigger(time, event, currentTime, 1 /* cps */);
}
} catch (err) {
console.warn(err);
@@ -136,6 +138,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
+10
View File
@@ -0,0 +1,10 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror, flash } from './components/CodeMirror6';
export * from './components/MiniRepl';
export { default as useCycle } from './hooks/useCycle';
export { default as useHighlighting } from './hooks/useHighlighting';
export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useRepl } from './hooks/useRepl';
export { default as cx } from './cx';
export { useWebMidi } from './hooks/useWebMidi';
+5
View File
@@ -0,0 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))
@@ -16,7 +16,7 @@ const ivory = '#abb2bf',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.2)',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
@@ -36,11 +36,18 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
borderLeftColor: cursor,
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
@@ -60,7 +67,8 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
// commented out because it looks bad in mini repl one liners
//'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@@ -70,7 +78,7 @@ export const materialPalenightTheme = EditorView.theme(
// done
'.cm-gutters': {
background: '#2C323699',
background: 'transparent',
color: '#676e95',
border: 'none',
},
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
corePlugins: {
preflight: false,
},
prefix: 'sc-',
};
+42
View File
@@ -0,0 +1,42 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { peerDependencies, dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
],
build: {
lib: {
entry: resolve(__dirname, 'src', 'index.js'),
formats: ['es', 'cjs'],
fileName: (ext) => `index.${ext}.js`,
// for UMD name: 'GlobalName'
},
rollupOptions: {
external: [
...Object.keys(peerDependencies),
...Object.keys(dependencies),
// TODO: find out which of below names are obsolete now
'@strudel.cycles/tone',
'@strudel.cycles/eval',
'@strudel.cycles/core',
'@strudel.cycles/core/util.mjs',
'@strudel.cycles/mini',
'@strudel.cycles/tonal',
'@strudel.cycles/midi',
'@strudel.cycles/xen',
'@strudel.cycles/serial',
'@strudel.cycles/webaudio',
'@codemirror/view',
'@codemirror/highlight',
'@codemirror/state'
],
},
target: 'esnext',
},
});
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.0.6",
"version": "0.1.3",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
@@ -19,6 +19,5 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
+28 -10
View File
@@ -9,7 +9,7 @@ import { Pattern, isPattern } from '@strudel.cycles/core';
var serialWriter;
var choosing = false;
export async function getWriter(br=115200) {
export async function getWriter(br=38400) {
if (choosing) {
return;
}
@@ -34,25 +34,43 @@ export async function getWriter(br=115200) {
const latency = 0.1;
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.serial = async function (...args) {
return this._withEvent((event) => {
Pattern.prototype.serial = function (...args) {
return this._withHap((hap) => {
if (!serialWriter) {
getWriter(...args);
}
const onTrigger = (time, event, currentTime) => {
const onTrigger = (time, hap, currentTime) => {
var message = "";
if (typeof event.value === 'object') {
for (const [key, val] of Object.entries(event.value).flat()) {
message += `${key}:${val};`
if (typeof hap.value === 'object') {
if ('action' in hap.value) {
message += hap.value['action'] + '(';
var first = true;
for (const [key, val] of Object.entries(hap.value)) {
if (key === 'action') {
continue;
}
if (first) {
first = false;
}
else {
message +=',';
}
message += `${key}:${val}`
}
message += ')';
}
else {
for (const [key, val] of Object.entries(hap.value)) {
message += `${key}:${val};`
}
}
}
else {
message = event.value;
message = hap.value;
}
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(serialWriter, offset, message);
};
return event.setContext({ ...event.context, onTrigger });
return hap.setContext({ ...hap.context, onTrigger });
});
};
+13
View File
@@ -0,0 +1,13 @@
// this script converts a soundfont into a json file, it has not been not used yet
import fetch from 'node-fetch';
const name = '0000_JCLive';
const js = await fetch(`https://felixroos.github.io/webaudiofontdata/sound/${name}_sf2_file.js`).then((res) =>
res.text(),
);
// console.log(js);
let [_, data] = js.split('_sf2_file=');
data = eval(data);
console.log(JSON.stringify(data));
+114
View File
@@ -0,0 +1,114 @@
let loadCache = {};
async function loadFont(name) {
if (loadCache[name]) {
return loadCache[name];
}
const load = async () => {
// TODO: make soundfont source configurable
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
console.log('load font', name, url);
const preset = await fetch(url).then((res) => res.text());
let [_, data] = preset.split('={');
return eval('{' + data);
};
loadCache[name] = load();
return loadCache[name];
}
export async function getFontBufferSource(name, pitch, ac) {
if (typeof pitch === 'string') {
pitch = toMidi(pitch);
}
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const src = ac.createBufferSource();
src.buffer = buffer;
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
// src detune?
src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
if (!loop) {
/* const waveDuration = duration + this.afterTime;
if (waveDuration > zone.buffer.duration / playbackRate) {
waveDuration = zone.buffer.duration / playbackRate;
// TODO: do sth with waveduration
} */
} else {
src.loop = true;
src.loopStart = zone.loopStart / zone.sampleRate;
src.loopEnd = zone.loopEnd / zone.sampleRate;
//+ (zone.delay ? zone.delay : 0);
}
return src;
}
let bufferCache = {};
export async function getFontPitch(name, pitch, ac) {
const key = `${name}:::${pitch}`;
if (bufferCache[key]) {
return bufferCache[key];
}
// console.log('load buffer', key);
const load = async () => {
const preset = await loadFont(name);
if (!preset) {
throw new Error(`Could not load soundfont ${name}`);
}
const zone = findZone(preset, pitch);
if (!zone) {
throw new Error('no soundfont zone found for preset ', name, 'pitch', pitch);
}
const buffer = await getBuffer(zone, ac);
if (!buffer) {
throw new Error(`no soundfont buffer found for preset ${name}, pitch: ${pitch}`);
}
return { buffer, zone };
};
bufferCache[key] = load(); // dont await here to cache promise immediately!
return bufferCache[key];
}
function findZone(preset, pitch) {
return preset.find((zone) => {
return zone.keyRangeLow <= pitch && zone.keyRangeHigh + 1 >= pitch;
});
}
// promisified version of https://github.com/felixroos/webaudiofont/blob/c6f97249b60dcfafc20fca5bb381294a6b2f8f51/npm/dist/WebAudioFontPlayer.js#L740
async function getBuffer(zone, audioContext) {
if (zone.sample) {
console.warn('zone.sample untested!');
const decoded = atob(zone.sample);
zone.buffer = audioContext.createBuffer(1, decoded.length / 2, zone.sampleRate);
const float32Array = zone.buffer.getChannelData(0);
let b1, b2, n;
for (var i = 0; i < decoded.length / 2; i++) {
b1 = decoded.charCodeAt(i * 2);
b2 = decoded.charCodeAt(i * 2 + 1);
if (b1 < 0) {
b1 = 256 + b1;
}
if (b2 < 0) {
b2 = 256 + b2;
}
n = b2 * 256 + b1;
if (n >= 65536 / 2) {
n = n - 65536;
}
float32Array[i] = n / 65536.0;
}
} else {
if (zone.file) {
const datalen = zone.file.length;
const arraybuffer = new ArrayBuffer(datalen);
const view = new Uint8Array(arraybuffer);
const decoded = atob(zone.file);
let b;
for (let i = 0; i < decoded.length; i++) {
b = decoded.charCodeAt(i);
view[i] = b;
}
return new Promise((resolve) => audioContext.decodeAudioData(arraybuffer, resolve));
}
}
}
+6
View File
@@ -0,0 +1,6 @@
import { getFontBufferSource } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
globalThis.getFontBufferSource = getFontBufferSource;
globalThis.soundfontList = soundfontList;
globalThis.soundfontList = soundfontList;
File diff suppressed because it is too large Load Diff
+301
View File
@@ -0,0 +1,301 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/soundfonts",
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"webaudiofont": "^3.0.1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/jsonc-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA=="
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
},
"node_modules/marked": {
"version": "4.0.17",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz",
"integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/shiki": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
"dependencies": {
"jsonc-parser": "^3.0.0",
"vscode-oniguruma": "^1.6.1",
"vscode-textmate": "5.2.0"
}
},
"node_modules/typedoc": {
"version": "0.22.17",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
"dependencies": {
"glob": "^8.0.3",
"lunr": "^2.3.9",
"marked": "^4.0.16",
"minimatch": "^5.1.0",
"shiki": "^0.10.1"
},
"bin": {
"typedoc": "bin/typedoc"
},
"engines": {
"node": ">= 12.10.0"
},
"peerDependencies": {
"typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x"
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA=="
},
"node_modules/vscode-textmate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ=="
},
"node_modules/webaudiofont": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webaudiofont/-/webaudiofont-3.0.1.tgz",
"integrity": "sha512-PwqbR4OdvzbW8/TLTitSU/rw4mxMKyAGTKV42mePCgMoUGLJoQj8HDLWVqtNOpUZ0ghPNhNE6cRFEv6HFFEBQA==",
"dependencies": {
"typedoc": "^0.22.14"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
},
"dependencies": {
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"glob": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"jsonc-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA=="
},
"lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
},
"marked": {
"version": "4.0.17",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz",
"integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA=="
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"requires": {
"brace-expansion": "^2.0.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"requires": {
"wrappy": "1"
}
},
"shiki": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
"requires": {
"jsonc-parser": "^3.0.0",
"vscode-oniguruma": "^1.6.1",
"vscode-textmate": "5.2.0"
}
},
"typedoc": {
"version": "0.22.17",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
"requires": {
"glob": "^8.0.3",
"lunr": "^2.3.9",
"marked": "^4.0.16",
"minimatch": "^5.1.0",
"shiki": "^0.10.1"
}
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"peer": true
},
"vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA=="
},
"vscode-textmate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ=="
},
"webaudiofont": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webaudiofont/-/webaudiofont-3.0.1.tgz",
"integrity": "sha512-PwqbR4OdvzbW8/TLTitSU/rw4mxMKyAGTKV42mePCgMoUGLJoQj8HDLWVqtNOpUZ0ghPNhNE6cRFEv6HFFEBQA==",
"requires": {
"typedoc": "^0.22.14"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.1.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "*",
"@strudel.cycles/webaudio": "^0.1.4"
},
"devDependencies": {
"node-fetch": "^3.2.6"
}
}
+2
View File
@@ -0,0 +1,2 @@
import './tonal.mjs';
import './voicings.mjs';
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.5",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/tonal",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"version": "0.1.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
+3 -3
View File
@@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.5",
"version": "0.1.3",
"description": "Tonal functions for strudel",
"main": "tonal.mjs",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/core": "^0.1.2",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}
+79 -14
View File
@@ -8,8 +8,8 @@ import { Note, Interval, Scale } from '@tonaljs/tonal';
import { Pattern, mod } from '@strudel.cycles/core';
// transpose note inside scale by offset steps
// function scaleTranspose(scale: string, offset: number, note: string) {
export function scaleTranspose(scale, offset, note) {
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
@@ -42,18 +42,50 @@ export function scaleTranspose(scale, offset, note) {
}
// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
/**
* Change the pitch of each value by the given amount. Expects numbers or note strings as values.
* The amount can be given as a number of semitones or as a string in interval short notation.
* If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of
* the form: ST where S is the degree number and T the type of interval with
*
* - M = major
* - m = minor
* - P = perfect
* - A = augmented
* - d = diminished
*
* Examples intervals:
*
* - 1P = unison
* - 3M = major third
* - 3m = minor third
* - 4P = perfect fourth
* - 4A = augmented fourth
* - 5P = perfect fifth
* - 5d = diminished fifth
*
* @param {string | number} amount Either number of semitones or interval string.
* @returns Pattern
* @memberof Pattern
* @name transpose
* @example
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)
* @example
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(0)
*/
Pattern.prototype._transpose = function (intervalOrSemitones) {
return this._withEvent((event) => {
return this._withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
if (typeof event.value === 'number') {
if (typeof hap.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return event.withValue(() => event.value + semitones);
return hap.withValue(() => hap.value + semitones);
}
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return event.withValue(() => Note.simplify(Note.transpose(event.value, interval)));
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
});
};
@@ -63,27 +95,60 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
/**
* Transposes notes inside the scale by the number of steps.
* Expected to be called on a Pattern which already has a {@link Pattern#scale}
*
* @memberof Pattern
* @name scaleTranspose
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @example
* "-8 [2,4,6]"
* .scale('C4 bebop major')
* .scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")
*/
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this._withEvent((event) => {
if (!event.context.scale) {
return this._withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof event.value !== 'string') {
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value));
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}.
*
* The scale name has the form "TO? N" wher
*
* - T = Tonic
* - O = Octave (optional, defaults to 3)
* - N = Name of scale, available names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2"
* .scale(seq('C2 major', 'C2 minor').slow(2))
*/
Pattern.prototype._scale = function (scale /* : string */) {
return this._withEvent((event) => {
let note = event.value;
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 = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
}
return event.withValue(() => note).setContext({ ...event.context, scale });
return hap.withValue(() => note).setContext({ ...hap.context, scale });
});
};
+12
View File
@@ -31,6 +31,18 @@ Pattern.prototype.fmapNested = function (func) {
);
};
/**
* Turns chord symbols into voicings, using the smoothest voice leading possible.
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
* @name voicings
* @memberof Pattern
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
* @returns Pattern
* @example
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
*/
Pattern.prototype.voicings = function (range) {
let lastVoicing;
if (!range?.length) {
+13 -10
View File
@@ -20,7 +20,7 @@ export const getDrawContext = (id = 'test-canvas') => {
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -29,25 +29,28 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
events = [];
const animate = (time) => {
const t = Tone.getTransport().seconds;
if (cycleSpan) {
const currentCycle = Math.floor(t / cycleSpan);
if (from !== undefined && to !== undefined) {
const currentCycle = Math.floor(t);
if (cycle !== currentCycle) {
cycle = currentCycle;
const begin = currentCycle * cycleSpan;
const end = (currentCycle + lookaheadCycles) * cycleSpan;
events = this._asNumber(true) // true = silent error
.query(new State(new TimeSpan(begin, end)))
.filter((event) => event.part.begin.equals(event.whole.begin));
const begin = currentCycle + from;
const end = currentCycle + to;
setTimeout(() => {
events = this.query(new State(new TimeSpan(begin, end)))
.filter(Boolean)
.filter((event) => event.part.begin.equals(event.whole.begin));
onQuery?.(events);
}, 0);
}
}
callback(ctx, events, t, cycleSpan, time);
callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
return this;
};
export const cleanup = () => {
export const cleanupDraw = () => {
const ctx = getDrawContext();
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) {
+5
View File
@@ -0,0 +1,5 @@
import './pianoroll.mjs';
export * from './tone.mjs';
export * from './draw.mjs';
export * from './ui.mjs';

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