Compare commits

..

83 Commits

Author SHA1 Message Date
Felix Roos 1829d8cadf Publish
- @strudel/codemirror@1.0.0
 - @strudel/core@1.0.0
 - @strudel/csound@1.0.0
 - @strudel/embed@1.0.0
 - @strudel/hydra@1.0.0
 - @strudel/midi@1.0.0
 - @strudel/mini@1.0.0
 - @strudel/osc@1.0.0
 - @strudel/repl@1.0.0
 - @strudel/serial@1.0.0
 - @strudel/soundfonts@1.0.0
 - superdough@1.0.0
 - @strudel/tonal@1.0.0
 - @strudel/transpiler@1.0.0
 - @strudel/web@1.0.0
 - @strudel/webaudio@1.0.0
 - @strudel/xen@1.0.0
2024-01-22 22:46:35 +01:00
Felix Roos 02efc090ef hotfix: examples were still 1 cps based 2024-01-22 20:46:10 +01:00
Felix Roos b184348f55 hotfix: youtube embed styles were lost on prod 2024-01-22 20:38:34 +01:00
Felix Roos 06743cb072 Merge pull request #929 from tidalcycles/2-years
2 years blog post
2024-01-22 20:20:52 +01:00
Felix Roos 6fa66fb1d0 finish year 2 post 2024-01-22 20:19:01 +01:00
Felix Roos 23c0ab9648 offset blog post anchor links 2024-01-22 20:18:51 +01:00
Alex McLean 6422047cff make 0.5hz cps the default (#931)
* 0.5 default cps

* 1 -> 0.5 cps defaults

* start moving examples to 2Hz

* more 2Hz doc edits

* small tweaks

* format

* adapt cycles page

* adapt pitch page

* tonal page

* accumulation

* synth page

* adapt conditional-modifiers

* audio effects page

* adapt signals doc

* fix: errors for signals

* adapt signals page

* start time modifiers

* adapt time modifiers

* adapt factories

* hydra + pattern intro

* adapt mini notation page

* start recipes

* adapt recipes page

* use code_v1 table

* delete old dbdump + add new csv based tool

* fix: tests

* fix: cpm

* shuffle featured patterns

* fix: snapshot

---------

Co-authored-by: Felix Roos <flix91@gmail.com>
2024-01-22 19:02:34 +00:00
Jade (Rose) Rowland 46c2c72637 Merge pull request #934 from daslyfe/patterntab_fix
Fix pattern tab not showing patterns without created date
2024-01-21 18:52:01 -05:00
Jade (Rose) Rowland c088590244 remove console statement 2024-01-21 18:51:06 -05:00
Jade (Rose) Rowland 192ba69682 fixed if statement 2024-01-21 18:49:06 -05:00
Jade (Rose) Rowland 2cc428e968 Merge pull request #897 from daslyfe/pattern_selection
Add useful pattern selection behavior for performing.
2024-01-21 14:46:27 -05:00
Jade (Rose) Rowland 15c451a6d9 default pattern filtername 2024-01-21 14:35:17 -05:00
Jade (Rose) Rowland b46e8c07be reset/autoplay when window PARENT does not exist or does not contain oodles 2024-01-21 12:44:14 -05:00
Jade (Rose) Rowland 961e931c31 autoplay and reset cps on default 2024-01-21 12:12:33 -05:00
Jade (Rose) Rowland b1d49552ae title order 2024-01-21 11:27:48 -05:00
Jade (Rose) Rowland d31d154841 fix oven imports 2024-01-21 11:24:58 -05:00
Jade (Rose) Rowland d03892bbcf Merge branch 'tidalcycles:main' into pattern_selection 2024-01-21 11:19:06 -05:00
Felix Roos 0cf0c1c585 Merge pull request #933 from tidalcycles/refactor-cps-functions
Refactor cps functions
2024-01-21 01:30:27 +01:00
Felix Roos 0695b4bee9 clean + comment 2024-01-21 01:29:31 +01:00
Felix Roos 0cfec3a7bd show repls in community bakery 2024-01-21 01:21:21 +01:00
Felix Roos 6224cd40d4 mini repl max height option 2024-01-21 01:21:13 +01:00
Felix Roos 18ae82174d fix: "can't redefine non-configurable property" 2024-01-21 01:13:32 +01:00
Felix Roos aa1b9d11dc inject scheduler into Pattern methods right before eval 2024-01-21 00:59:31 +01:00
Felix Roos 0a82471112 implement loopAt with pat state 2024-01-21 00:45:40 +01:00
Felix Roos 8052e10f0c implement fit with pat state 2024-01-21 00:39:26 +01:00
Jade (Rose) Rowland 1146f6ba52 fix annoying div 2024-01-20 18:14:42 -05:00
Alex McLean 738cea0025 Make splice cps-aware (#932)
* make splice cps-aware

* format

* copypaste fix
2024-01-20 22:47:31 +00:00
Jade (Rose) Rowland 6c28398d20 fix scroll 2024-01-20 14:25:42 -05:00
Jade (Rose) Rowland 4fe75143d0 cleanup 2024-01-20 14:10:46 -05:00
Jade (Rose) Rowland 3cd59cdea5 cleaning up 2024-01-20 14:02:48 -05:00
Jade (Rose) Rowland 9dca7eb878 add pattern filter behaves like sounds tab 2024-01-20 13:59:32 -05:00
Jade (Rose) Rowland b45fd5ce3a fix invalid date 2024-01-20 13:00:33 -05:00
Felix Roos 229b1470f7 fix: localhost links 2024-01-20 10:59:34 +01:00
Jade (Rose) Rowland 0a349e1707 avoid json errors in firefox 2024-01-19 23:04:41 -05:00
Felix Roos d176e9ccf0 add stats 2024-01-19 21:51:52 +01:00
Felix Roos 710c3acb5d more writing 2024-01-19 21:21:33 +01:00
Felix Roos a7f5d0e0c1 Merge pull request #923 from tidalcycles/feed
community bakery
2024-01-19 18:50:57 +01:00
Felix Roos 9928526dda Merge pull request #924 from geikha/main
add pickF and pickmodF
2024-01-19 18:49:53 +01:00
Renzo Torr- 2f8111be0e update the .snap file 2024-01-19 14:47:03 -03:00
Renzo Torr- 68113937ff code format 2024-01-19 14:06:24 -03:00
Felix Roos c598fa026b write more 2024-01-19 15:49:16 +01:00
Alex McLean 491c99c348 Mini-notation additions towards tidal compatibility (#926)
- `.` feet now work, e.g. `"a b c . d e . f"` is the same as `"[a b c] [d e] [f]"`
- `_` elongation now works, e.g. `"a _ _ b"` is the same as `"a@3 b"` (`"a @ @ b"` also works, like in tidal)
- standalone `!` now works, e.g. `"a ! ! b"` is the same as `"a!3 b"`

Reworks #47 and #49
Ref #30
2024-01-19 14:10:47 +00:00
Felix Roos d49aa8242a begin 2 year post 2024-01-19 13:50:28 +01:00
Renzo Torr- 9ffbc06cf9 add pickF and pickmodF
allows to pick functions via a pattern of numbers, to apply to another pattern
2024-01-19 02:45:54 -03:00
Jade (Rose) Rowland af52ac30ba prettier 2024-01-19 00:28:55 -05:00
Jade (Rose) Rowland 7aea1b5fc8 fixed imports 2024-01-19 00:27:09 -05:00
Jade (Rose) Rowland 500ddf3946 Merge branch 'main' into pattern_selection 2024-01-19 00:23:06 -05:00
Jade (Rose) Rowland 3431eaf309 cleaning up 2024-01-19 00:20:10 -05:00
Jade (Rose) Rowland 9a13c58583 cleaning up 2024-01-19 00:14:08 -05:00
Jade (Rose) Rowland 77a48e8351 persistent pattern data 2024-01-18 22:30:53 -05:00
Jade (Rose) Rowland 6cb156d876 cleaning up 2024-01-18 20:01:54 -05:00
Felix Roos d8677c6261 hotfix: rss links 2024-01-18 23:40:22 +01:00
Felix Roos 6ef9ca18e9 Merge pull request #919 from tidalcycles/blog-improvements
Blog improvements
2024-01-18 23:34:10 +01:00
Felix Roos 6667738dc9 more rename 2024-01-18 21:34:20 +01:00
Felix Roos 4f7415b0ec rename 2024-01-18 21:31:21 +01:00
Felix Roos c6f3a8b7c7 more text 2024-01-18 18:30:57 +01:00
Felix Roos 4229d673c9 basic community oven 2024-01-18 18:21:56 +01:00
Alex McLean 98b7859605 pick, pickmod, inhabit, inhabitmod (#921)
* the args for `pick` are now reversed as standard (old behaviour still supported to avoid breaking change)
* `pick` is also now a pattern method
* `pick` now also accepts a lookup table for pick-by-name as an alternative to pick-by-index from a list
* `inhabit` added with same behaviour as `pick`, except cycles from source patterns are squeezed into events of inhabited patterns
* Also some general doc tidying, sorry for the noise..
* There is also `pickmod` and `inhabitmod`, for wrapping indexes around rather than clamping them
2024-01-18 17:08:29 +00:00
Alex McLean a8db707440 Revert "pick now accepts lookup tables, with alternate cycle squeezing behaviour as new inhabit function" (#920) 2024-01-18 17:04:26 +00:00
Alex McLean 873a412179 pick now accepts lookup tables, with alternate cycle squeezing behaviour as new inhabit function (#918)
- the args for `pick` are now reversed (breaking change!)
- `pick` is also now a method on the inhabited pattern
- `pick` now also accepts a lookup table
- `inhabit` added with same behaviour as `pick`, except cycles from source patterns are squeezed into events of inhabited patterns
- Also some general doc tidying, sorry for the noise..
- There is now also `pickmod` and `inhabitmod`, for wrapping indexes around rather than clamping them
2024-01-18 16:45:39 +00:00
Felix Roos 0778b4809e format 2024-01-18 13:47:34 +01:00
Felix Roos f85fb35c66 minirepl multi tune support + add other getting-started examples (more to come) 2024-01-18 13:46:35 +01:00
Felix Roos 5ada0daf40 add rss link to top + fix typo 2024-01-18 13:01:06 +01:00
Felix Roos 2ed5f5aa65 blog style fixes 2024-01-18 12:50:35 +01:00
Felix Roos ae5e6e3ab8 add rss.xml 2024-01-18 12:49:22 +01:00
Felix Roos 9ba7b9b4d4 Merge pull request #917 from tidalcycles/deprecate-old-package-names
rename @strudel.cycles/* packages to @strudel/*
2024-01-18 10:30:08 +01:00
Felix Roos a45f1fba45 update versions in example html files 2024-01-18 10:25:24 +01:00
Felix Roos e6b47d2cf2 fix link 2024-01-18 10:04:53 +01:00
Jade (Rose) Rowland 7adfe6f2df fixing edge cases... 2024-01-17 09:45:00 -05:00
Jade (Rose) Rowland 45df7bfea3 making things consistent 2024-01-16 00:45:00 -05:00
Jade (Rose) Rowland 106772e480 Merge branch 'main' into pattern_selection 2024-01-15 18:37:18 -05:00
Felix Roos e7e60d3ed8 Merge branch 'main' into pattern_selection 2024-01-14 00:18:57 +01:00
Jade (Rose) Rowland 78f6fdf169 reset for example patterns 2024-01-08 10:49:26 -05:00
Jade (Rose) Rowland e01c6dc18d prettier 2024-01-08 00:25:58 -05:00
Jade (Rose) Rowland 51dd408733 all working 2024-01-08 00:15:07 -05:00
Jade (Rose) Rowland 1ff7548b52 refactoring 2024-01-07 22:48:07 -05:00
Jade (Rose) Rowland f7fe44063a handle deletion cases 2024-01-07 00:58:03 -05:00
Jade (Rose) Rowland 0355c358b8 fixed create and duplicate behavior 2024-01-06 23:49:37 -05:00
Jade (Rose) Rowland 4e0a60db00 example code logic 2024-01-06 22:10:14 -05:00
Jade (Rose) Rowland 3604d3940c mutate viewing pattern 2024-01-06 17:53:51 -05:00
Jade (Rose) Rowland 02b9bd82cf highlight viewing pattern 2024-01-06 17:33:24 -05:00
Jade (Rose) Rowland 1bc86c359e state 2024-01-03 01:04:39 -05:00
Jade (Rose) Rowland 1e2c26123b working 2024-01-03 00:35:47 -05:00
95 changed files with 5595 additions and 3122 deletions
+17 -11
View File
@@ -16,34 +16,40 @@
</div>
<div id="output"></div>
<script type="module">
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
import {
getAudioContext,
webaudioOutput,
initAudioOnFirstClick,
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
registerSynthSounds,
} from 'https://cdn.skypack.dev/@strudel/webaudio@0.11.0';
initAudioOnFirstClick();
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = getTune();
evalScope(
const loadModules = evalScope(
controls,
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
import('https://cdn.skypack.dev/@strudel/core@0.11.0'),
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'),
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'),
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'),
);
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
transpiler,
});
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
document.getElementById('start').addEventListener('click', async () => {
await loadModules;
await initAudio;
evaluate(input.value);
});
function getTune() {
return `await samples('github:tidalcycles/Dirt-Samples/master')
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<script src="https://unpkg.com/@strudel/embed@0.11.0"></script>
<!-- <script src="./embed.js"></script> -->
<strudel-repl>
<!--
+3 -1
View File
@@ -57,6 +57,9 @@ const visibleMiniLocations = StateField.define({
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
if (!hap.context?.locations || !hap.whole) {
continue;
}
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
@@ -64,7 +67,6 @@ const visibleMiniLocations = StateField.define({
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "0.11.0",
"version": "1.0.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+80 -76
View File
@@ -40,7 +40,7 @@ const generic_params = [
* @name n
* @param {number | Pattern} value sample index starting from 0
* @example
* s("bd sd,hh*3").n("<0 1>")
* s("bd sd [~ bd] sd,hh*6").n("<0 1>")
*/
// also see https://github.com/tidalcycles/strudel/pull/63
['n'],
@@ -82,7 +82,7 @@ const generic_params = [
* @name gain
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
*
*/
['gain'],
@@ -91,13 +91,13 @@ const generic_params = [
*
* @name postgain
* @example
* s("bd sd,hh*4")
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
*
*/
['postgain'],
/**
* Like {@link gain}, but linear.
* Like `gain`, but linear.
*
* @name amp
* @param {number | Pattern} amount gain.
@@ -114,7 +114,7 @@ const generic_params = [
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
* note("c3 e3").attack("<0 .1 .5>")
* note("c3 e3 f3 g3").attack("<0 .1 .5>")
*
*/
['attack', 'att'],
@@ -128,7 +128,7 @@ const generic_params = [
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
@@ -143,7 +143,7 @@ const generic_params = [
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b")
* note("c e g b g e")
* .fm("<0 1 2 8 32>")
* .scope()
*
@@ -156,7 +156,7 @@ const generic_params = [
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
@@ -171,7 +171,7 @@ const generic_params = [
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
@@ -184,7 +184,7 @@ const generic_params = [
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
@@ -198,7 +198,7 @@ const generic_params = [
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
@@ -216,7 +216,7 @@ const generic_params = [
* @name bank
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
* s("bd sd [~ bd] sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['bank'],
@@ -231,7 +231,7 @@ const generic_params = [
* @name decay
* @param {number | Pattern} time decay time in seconds
* @example
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
*
*/
['decay', 'dec'],
@@ -242,7 +242,7 @@ const generic_params = [
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
* note("c3 e3 f3 g3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['sustain', 'sus'],
@@ -267,7 +267,7 @@ const generic_params = [
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
* s("bd sd [~ bd] sd,hh*6").bpf("<1000 2000 4000 8000>")
*
*/
[['bandf', 'bandq', 'bpenv'], 'bpf', 'bp'],
@@ -279,7 +279,7 @@ const generic_params = [
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
* s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>")
*
*/
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
@@ -293,7 +293,7 @@ const generic_params = [
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
* @example
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
* s("rave").begin("<0 .25 .5 .75>")
* s("rave").begin("<0 .25 .5 .75>").fast(2)
*
*/
['begin'],
@@ -304,7 +304,7 @@ const generic_params = [
* @name end
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
* @example
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
* s("bd*2,oh*4").end("<.1 .2 .5 1>").fast(2)
*
*/
['end'],
@@ -376,7 +376,7 @@ const generic_params = [
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
*
*/
['coarse'],
@@ -463,7 +463,7 @@ const generic_params = [
* @name cut
* @param {number | Pattern} group cut group number
* @example
* s("rd*4").cut(1)
* s("[oh hh]*4").cut(1)
*
*/
['cut'],
@@ -476,9 +476,9 @@ const generic_params = [
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
* s("bd sd [~ bd] sd,hh*6").lpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
* s("bd*16").lpf("1000:0 1000:10 1000:20 1000:30")
*
*/
[['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp'],
@@ -489,7 +489,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
@@ -502,7 +502,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
@@ -515,7 +515,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
@@ -528,7 +528,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
@@ -541,7 +541,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
@@ -554,7 +554,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
@@ -567,7 +567,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
@@ -581,7 +581,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
@@ -595,7 +595,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
@@ -609,7 +609,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
@@ -623,7 +623,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
@@ -637,7 +637,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
@@ -651,7 +651,7 @@ const generic_params = [
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
@@ -666,7 +666,7 @@ const generic_params = [
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
@@ -681,7 +681,7 @@ const generic_params = [
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
@@ -695,11 +695,11 @@ const generic_params = [
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
* .ftype("12db 24db")
*/
['ftype'],
['fanchor'],
@@ -712,9 +712,9 @@ const generic_params = [
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
* s("bd sd [~ bd] sd,hh*8").hpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd sd,hh*4").hpf("<2000 2000:25>")
* s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>")
*
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
@@ -726,11 +726,11 @@ const generic_params = [
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* note("a e")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* note("a e")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
@@ -750,11 +750,11 @@ const generic_params = [
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* note("a e").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* note("a e")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
@@ -766,7 +766,7 @@ const generic_params = [
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
* s("bd sd [~ bd] sd,hh*8").hpf(2000).hpq("<0 10 20 30>")
*
*/
['hresonance', 'hpq'],
@@ -777,7 +777,7 @@ const generic_params = [
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
* s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>")
*
*/
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
@@ -804,7 +804,7 @@ const generic_params = [
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .25 .5 1>")
* s("bd bd").delay("<0 .25 .5 1>")
* @example
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
*
@@ -818,7 +818,7 @@ const generic_params = [
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
['delayfeedback', 'delayfb', 'dfb'],
@@ -829,7 +829,7 @@ const generic_params = [
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
*
*/
['delaytime', 'delayt', 'dt'],
@@ -856,7 +856,7 @@ const generic_params = [
*/
['detune', 'det'],
/**
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
* @name dry
* @param {number | Pattern} dry 0 = wet, 1 = dry
@@ -868,7 +868,7 @@ const generic_params = [
['dry'],
// 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.
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
* @param {number | Pattern} time between 0 and 1
@@ -899,7 +899,7 @@ const generic_params = [
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb>").pattack("<0 .1 .25 .5>")
* note("c eb g bb").pattack("0 .1 .25 .5").slow(2)
*
*/
['pattack', 'patt'],
@@ -947,7 +947,7 @@ const generic_params = [
* @name pcurve
* @param {number | Pattern} type 0 = linear, 1 = exponential
* @example
* note("g1*2")
* note("g1*4")
* .s("sine").pdec(.5)
* .penv(32)
* .pcurve("<0 1>")
@@ -963,7 +963,7 @@ const generic_params = [
* @name panchor
* @param {number | Pattern} anchor anchor offset
* @example
* note("c").penv(12).panchor("<0 .5 1 .5>")
* note("c c4").penv(12).panchor("<0 .5 1 .5>")
*
*/
['panchor'],
@@ -1062,8 +1062,8 @@ const generic_params = [
* @param {number | Pattern} number
* @example
* stack(
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* s("hh*6").delay(.5).delaytime(.25).orbit(1),
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
['orbit'],
@@ -1076,6 +1076,8 @@ const generic_params = [
* @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>")
* @example
* s("bd rim sd rim bd ~ cp rim").pan(sine.slow(2))
*
*/
['pan'],
@@ -1133,9 +1135,9 @@ const generic_params = [
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
* s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
* @example
* s("bd sd").room("<0.9:1 0.9:4>")
* s("bd sd [~ bd] sd").room("<0.9:1 0.9:4>")
*
*/
[['room', 'size']],
@@ -1147,9 +1149,9 @@ const generic_params = [
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000)
* @example
* s("bd sd").room(0.5).rlp(5000)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000)
*/
['roomlp', 'rlp'],
/**
@@ -1160,9 +1162,9 @@ const generic_params = [
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000).rdim(8000)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000).rdim(8000)
* @example
* s("bd sd").room(0.5).rlp(5000).rdim(400)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000).rdim(400)
*
*/
['roomdim', 'rdim'],
@@ -1174,9 +1176,9 @@ const generic_params = [
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
* s("bd sd").room(0.5).rlp(10000).rfade(0.5)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000).rfade(0.5)
* @example
* s("bd sd").room(0.5).rlp(5000).rfade(4)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000).rfade(4)
*
*/
['roomfade', 'rfade'],
@@ -1186,25 +1188,25 @@ const generic_params = [
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
* s("bd sd [~ bd] sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
*/
[['ir', 'i'], 'iresponse'],
/**
* Sets the room size of the reverb, see {@link room}.
* Sets the room size of the reverb, see `room`.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @example
* s("bd sd").room(.8).rsize(1)
* s("bd sd [~ bd] sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
* s("bd sd [~ bd] sd").room(.8).rsize(4)
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// s("bd sd [~ bd] sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
['roomsize', 'size', 'sz', 'rsize'],
// ['sagogo'],
@@ -1217,7 +1219,7 @@ const generic_params = [
* @name shape
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
* s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>")
*
*/
['shape'],
@@ -1227,7 +1229,7 @@ const generic_params = [
*
* @name compressor
* @example
* s("bd sd,hh*4")
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02")
*
*/
@@ -1242,14 +1244,14 @@ const generic_params = [
* @name speed
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
* @example
* s("bd").speed("<1 2 4 1 -2 -4>")
* s("bd*6").speed("1 2 4 1 -2 -4")
* @example
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
*
*/
['speed'],
/**
* 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`.
* 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`.
*
* @name unit
* @param {number | string | Pattern} unit see description above
@@ -1287,8 +1289,10 @@ const generic_params = [
* @name vowel
* @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ.
* @example
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
* note("[c2 <eb2 <g2 g1>>]*2").s('sawtooth')
* .vowel("<a e i <o u>>")
* @example
* s("bd sd mt ht bd [~ cp] ht lt").vowel("[a|e|i|o|u]")
*
*/
['vowel'],
@@ -1463,7 +1467,7 @@ controls.createParams = (...names) =>
* @param {number | Pattern} gain sustain level (0 to 1)
* @param {number | Pattern} time release time in seconds
* @example
* note("<c3 bb2 f3 eb3>").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
* note("[c3 bb2 f3 eb3]*2").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
*/
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
+3 -3
View File
@@ -10,7 +10,7 @@ import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
@@ -41,7 +41,7 @@ export class Cyclist {
this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end);
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline;
@@ -96,7 +96,7 @@ export class Cyclist {
this.start();
}
}
setCps(cps = 1) {
setCps(cps = 0.5) {
if (this.cps === cps) {
return;
}
+1 -1
View File
@@ -134,7 +134,7 @@ export class Drawer {
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
.filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
+1 -1
View File
@@ -148,7 +148,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
* note("c3").euclidLegato(3,8)
*/
const _euclidLegato = function (pulses, steps, rotation, pat) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "0.11.0",
"version": "1.0.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+120 -99
View File
@@ -26,7 +26,7 @@ export class 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}.
* @param {function} query - The function that maps a `State` to an array of `Hap`.
* @noAutocomplete
*/
constructor(query) {
@@ -39,7 +39,7 @@ export class Pattern {
/**
* Returns a new pattern, with the function applied to the value of
* each hap. It has the alias {@link Pattern#fmap}.
* each hap. It has the alias `fmap`.
* @synonyms fmap
* @param {Function} func to to apply to the value
* @returns Pattern
@@ -51,7 +51,7 @@ export class Pattern {
}
/**
* see {@link Pattern#withValue}
* see `withValue`
* @noAutocomplete
*/
fmap(func) {
@@ -115,7 +115,7 @@ export class Pattern {
}
/**
* As with {@link Pattern#appBoth}, but the `whole` timespan is not the intersection,
* As with `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
@@ -148,7 +148,7 @@ export class Pattern {
}
/**
* As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the
* As with `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
@@ -340,9 +340,9 @@ export class Pattern {
* silence
* @noAutocomplete
*/
queryArc(begin, end) {
queryArc(begin, end, controls = {}) {
try {
return this.query(new State(new TimeSpan(begin, end)));
return this.query(new State(new TimeSpan(begin, end), controls));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
return [];
@@ -387,7 +387,7 @@ export class Pattern {
}
/**
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
* As with `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
@@ -398,7 +398,7 @@ export class Pattern {
}
/**
* Similar to {@link Pattern#withQuerySpan}, but the function is applied to the timespans
* Similar to `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
@@ -410,7 +410,7 @@ export class Pattern {
}
/**
* As with {@link Pattern#withHapSpan}, but the function is applied to both the
* As with `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
@@ -427,11 +427,11 @@ export class Pattern {
* @noAutocomplete
*/
withHaps(func) {
return new Pattern((state) => func(this.query(state)));
return new Pattern((state) => func(this.query(state), state));
}
/**
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
* As with `withHaps`, but applies the function to every hap, rather than every list of haps.
* @param {Function} func
* @returns Pattern
* @noAutocomplete
@@ -499,7 +499,7 @@ export class Pattern {
}
/**
* As with {@link Pattern#filterHaps}, but the function is applied to values
* As with `filterHaps`, but the function is applied to values
* inside haps.
* @param {Function} value_test
* @returns Pattern
@@ -621,7 +621,7 @@ export class Pattern {
}
/**
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
* More human-readable version of the `firstCycleValues` accessor.
* @noAutocomplete
*/
get showFirstCycle() {
@@ -691,13 +691,13 @@ export class Pattern {
// Methods without corresponding toplevel functions
/**
* Layers the result of the given function(s). Like {@link Pattern.superimpose}, but without the original pattern:
* Layers the result of the given function(s). Like `superimpose`, but without the original pattern:
* @name layer
* @memberof Pattern
* @synonyms apply
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* .layer(x=>x.add("0,2"))
* .scale('C minor').note()
*/
@@ -711,7 +711,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* .superimpose(x=>x.add(2))
* .scale('C minor').note()
*/
@@ -727,8 +727,8 @@ export class Pattern {
* @name stack
* @memberof Pattern
* @example
* s("hh*2").stack(
* note("c2(3,8)")
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
stack(...pats) {
@@ -745,8 +745,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*2").seq(
* note("c2(3,8)")
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
seq(...pats) {
@@ -759,8 +759,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*2").cat(
* note("c2(3,8)")
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
cat(...pats) {
@@ -858,7 +858,7 @@ Pattern.prototype.arpWith = function (func) {
* Selects indices in in stacked notes.
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* .arp("0 [0,2] 1 [0,2]")
* */
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
@@ -929,14 +929,14 @@ function _composeOp(a, b, func) {
* @memberof Pattern
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* "c3 e3 g3".add("<0 5 7 0>").note()
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // "48 52 55".add("<0 5 7 0>").note()
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
@@ -945,7 +945,7 @@ function _composeOp(a, b, func) {
* @name sub
* @memberof Pattern
* @example
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
@@ -955,7 +955,7 @@ function _composeOp(a, b, func) {
* @name mul
* @memberof Pattern
* @example
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
@@ -1049,9 +1049,9 @@ function _composeOp(a, b, func) {
* Applies the given structure to the pattern:
*
* @example
* note("c3,eb3,g3")
* note("c,eb,g")
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
* .slow(2)
*/
Pattern.prototype.struct = function (...args) {
return this.keepif.out(...args);
@@ -1063,7 +1063,7 @@ function _composeOp(a, b, func) {
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>")
*/
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
@@ -1075,7 +1075,7 @@ function _composeOp(a, b, func) {
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
* s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
*/
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
@@ -1088,7 +1088,7 @@ function _composeOp(a, b, func) {
* While reset will only reset the current cycle, restart will start from cycle 0.
*
* @example
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
* s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
*/
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
@@ -1178,7 +1178,8 @@ export function reify(thing) {
* @return {Pattern}
* @synonyms polyrhythm, pr
* @example
* stack("g3", "b3", ["e4", "d4"]).note() // "g3,b3,[e4,d4]".note()
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4,d4]".note()
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
@@ -1189,7 +1190,7 @@ export function stack(...pats) {
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: {@link cat}
* synonyms: `cat`
*
* @return {Pattern}
* @example
@@ -1237,17 +1238,19 @@ export function slowcatPrime(...pats) {
* @synonyms slowcat
* @return {Pattern}
* @example
* cat("e5", "b4", ["d5", "c5"]).note() // "<e5 b4 [d5 c5]>".note()
* cat("e5", "b4", ["d5", "c5"]).note()
* // "<e5 b4 [d5 c5]>".note()
*
*/
export function cat(...pats) {
return slowcat(...pats);
}
/** Like {@link Pattern.seq}, but each step has a length, relative to the whole.
/** Like `seq`, but each step has a length, relative to the whole.
* @return {Pattern}
* @example
* timeCat([3,"e3"],[1, "g3"]).note() // "e3@3 g3".note()
* timeCat([3,"e3"],[1, "g3"]).note()
* // "e3@3 g3".note()
*/
export function timeCat(...timepats) {
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@@ -1267,7 +1270,10 @@ export function timeCat(...timepats) {
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
* arrange(
* [4, "<c a f e>(3,8)"],
* [2, "<g a>(5,8)"]
* ).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
@@ -1279,7 +1285,7 @@ export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
/** See {@link fastcat} */
/** See `fastcat` */
export function sequence(...pats) {
return fastcat(...pats);
}
@@ -1287,7 +1293,8 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms fastcat, sequence
* @example
* seq("e5", "b4", ["d5", "c5"]).note() // "e5 b4 [d5 c5]".note()
* seq("e5", "b4", ["d5", "c5"]).note()
* // "e5 b4 [d5 c5]".note()
*
*/
export function seq(...pats) {
@@ -1313,9 +1320,9 @@ function _sequenceCount(x) {
* @param {number} steps how many items are placed in one cycle
* @param {any[]} sequences one or more arrays of Patterns / values
* @example
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
* // note("{c d e f g f e d}%2").stack(s("bd"))
* polymeterSteps(4, ["c", "d", "e"])
* .note().stack(s("bd"))
* // note("{c d e}%4").stack(s("bd"))
*/
export function polymeterSteps(steps, ...args) {
const seqs = args.map((a) => _sequenceCount(a));
@@ -1463,7 +1470,7 @@ export function register(name, func, patternify = true) {
* @memberof Pattern
* @returns Pattern
* @example
* "0.5 1.5 2.5".round().scale('C major').note()
* n("0.5 1.5 2.5".round()).scale("C:major")
*/
export const round = register('round', function (pat) {
return pat.asNumber().fmap((v) => Math.round(v));
@@ -1477,7 +1484,7 @@ export const round = register('round', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".floor().note()
* note("42 42.1 42.5 43".floor())
*/
export const floor = register('floor', function (pat) {
return pat.asNumber().fmap((v) => Math.floor(v));
@@ -1491,7 +1498,7 @@ export const floor = register('floor', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".ceil().note()
* note("42 42.1 42.5 43".ceil())
*/
export const ceil = register('ceil', function (pat) {
return pat.asNumber().fmap((v) => Math.ceil(v));
@@ -1524,7 +1531,8 @@ export const fromBipolar = register('fromBipolar', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine.range(500,4000))
*/
export const range = register('range', function (min, max, pat) {
return pat.mul(max - min).add(min);
@@ -1538,7 +1546,8 @@ export const range = register('range', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.rangex(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine.rangex(500,4000))
*/
export const rangex = register('rangex', function (min, max, pat) {
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
@@ -1551,7 +1560,8 @@ export const rangex = register('rangex', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine2.range2(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine2.range2(500,4000))
*/
export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
@@ -1564,7 +1574,8 @@ export const range2 = register('range2', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
* ratio("1, 5:4, 3:2").mul(110)
* .freq().s("piano")
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
@@ -1636,7 +1647,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
});
/**
* Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
* Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
* @example
* s("bd hh sd hh").focus(1/4, 3/4)
*/
@@ -1667,7 +1678,7 @@ export const ply = register('ply', function (factor, pat) {
* @param {number | Pattern} factor speed up factor
* @returns Pattern
* @example
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
@@ -1696,7 +1707,7 @@ export const hurry = register('hurry', function (r, pat) {
* @param {number | Pattern} factor slow down factor
* @returns Pattern
* @example
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
* s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
@@ -1753,7 +1764,7 @@ export const lastOf = register('lastOf', function (n, func, pat) {
*/
/**
* An alias for {@link firstOf}
* An alias for `firstOf`
* @name every
* @memberof Pattern
* @param {number} n how many cycles
@@ -1785,9 +1796,9 @@ export const apply = register('apply', function (func, pat) {
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
// TODO - global clock
// this is redefined in repl.mjs, using the current cps as divisor
export const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60);
return pat._fast(cpm / 60 / 1);
});
/**
@@ -1860,7 +1871,7 @@ export const linger = register('linger', function (t, pat) {
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(0,12).segment(24)).add(40)
* note(saw.range(40,52).segment(24))
*/
export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate));
@@ -1886,7 +1897,7 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
* @param {function} func
* @returns Pattern
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
*/
export const when = register('when', function (on, func, pat) {
return on ? func(pat) : pat;
@@ -1923,7 +1934,7 @@ export const brak = register('brak', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* note("c3 d3 e3 g3").rev()
* note("c d e g").rev()
*/
export const rev = register('rev', function (pat) {
const query = function (state) {
@@ -1993,7 +2004,7 @@ export const palindrome = register('palindrome', function (pat) {
* @name juxBy
* @synonyms juxby
* @example
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
* s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
*/
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
by /= 2;
@@ -2012,7 +2023,11 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @example
* s("lt ht mt ht hh").jux(rev)
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(press)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))
*/
export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
@@ -2028,7 +2043,7 @@ export const jux = register('jux', function (func, pat) {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().clip(.2)
* .scale("C:minor").note()
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2121,7 +2136,8 @@ const { repeatCycles } = register('repeatCycles', _repeatCycles);
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
* "0 1 2 3".chunk(4, x=>x.add(7))
* .scale("A:minor").note()
*/
const _chunk = function (n, func, pat, back = false, fast = false) {
const binary = Array(n - 1).fill(false);
@@ -2146,7 +2162,8 @@ const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChu
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
* "0 1 2 3".chunkBack(4, x=>x.add(7))
* .scale("A:minor").note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
@@ -2160,10 +2177,11 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
* @memberof Pattern
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7".fastChunk(4, x => x.color('red')).slow(4).scale("C2:major").note()
.s("folkharp")
* "<0 8> 1 2 3 4 5 6 7"
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
@@ -2178,6 +2196,8 @@ export const bypass = register('bypass', function (on, pat) {
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
* note("<c d e f>").ribbon(1, 2).fast(2)
* @example
* // Looping a portion of randomness
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
*/
@@ -2251,7 +2271,7 @@ export const legato = register('legato', function (value, pat) {
* s("rhodes")
* .chop(4)
* .rev() // reverse order of chops
* .loopAt(4) // fit sample into 4 cycles
* .loopAt(2) // fit sample into 2 cycles
*
*/
export const chop = register('chop', function (n, pat) {
@@ -2269,7 +2289,7 @@ export const chop = register('chop', function (n, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(6)
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)
*/
export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
@@ -2285,10 +2305,10 @@ export const striate = register('striate', function (n, pat) {
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAt(4)
* s("rhodes").loopAt(2)
*/
// TODO - global cps clock
const _loopAt = function (factor, pat, cps = 1) {
const _loopAt = function (factor, pat, cps = 0.5) {
return pat
.speed((1 / factor) * cps)
.unit('c')
@@ -2303,10 +2323,10 @@ const _loopAt = function (factor, pat, cps = 1) {
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@@ -2334,50 +2354,51 @@ export const slice = register(
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
*/
export const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
return sliced.withHap(function (hap) {
return hap.withValue((v) => ({
...{
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
}));
return new Pattern((state) => {
// TODO - default cps to 0.5
const cps = state.controls._cps || 1;
const haps = sliced.query(state);
return haps.map((hap) =>
hap.withValue((v) => ({
...{
speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
})),
);
});
},
false, // turns off auto-patternification
);
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state));
});
// the fit function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to loopAt.
* Similar to `loopAt`.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/4").fit()
* s("rhodes/2").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
pat.withHaps((haps, state) =>
haps.map((hap) =>
hap.withValue((v) => ({
...v,
speed: (state.controls._cps || 1) / hap.whole.duration,
unit: 'c',
})),
),
),
);
+55 -63
View File
@@ -61,12 +61,67 @@ export function repl({
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const all = function (transform) {
allTransform = transform;
return silence;
};
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
console.warn('injectPatternMethods: error:', err);
}
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
evalScope({
all,
hush,
cpm,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
};
const evaluate = async (code, autostart = true, shouldHush = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
updateState({ code, pending: true });
injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler);
@@ -94,74 +149,11 @@ export function repl({
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
};
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
const all = function (transform) {
allTransform = transform;
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
// already defined..
}
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
all,
hush,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
}
+131 -36
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, _mod, clamp } from './util.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -27,9 +27,11 @@ export const isaw2 = isaw.toBipolar();
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
* n(saw.range(0,8).segment(8))
* .scale('C major')
*
*/
export const saw = signal((t) => t % 1);
@@ -42,7 +44,8 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
*
* @return {Pattern}
* @example
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
* n(sine.segment(16).range(0,15))
* .scale("C:minor")
*
*/
export const sine = sine2.fromBipolar();
@@ -52,7 +55,8 @@ export const sine = sine2.fromBipolar();
*
* @return {Pattern}
* @example
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
* n(stack(sine,cosine).segment(16).range(0,15))
* .scale("C:minor")
*
*/
export const cosine = sine._early(Fraction(1).div(4));
@@ -63,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
* @return {Pattern}
* @example
* square.segment(2).range(0,7).scale('C minor').note()
* n(square.segment(4).range(0,7)).scale("C:minor")
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
@@ -74,7 +78,7 @@ export const square2 = square.toBipolar();
*
* @return {Pattern}
* @example
* tri.segment(8).range(0,7).scale('C minor').note()
* n(tri.segment(8).range(0,7)).scale("C:minor")
*
*/
export const tri = fastcat(isaw, saw);
@@ -118,8 +122,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* A discrete pattern of numbers from 0 to n-1
* @example
* run(4).scale('C4 major').note()
* // "0 1 2 3".scale('C4 major').note()
* n(run(4)).scale("C4:pentatonic")
* // n("0 1 2 3").scale("C4:pentatonic")
*/
export const run = (n) => saw.range(0, n).floor().segment(n);
@@ -129,7 +133,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
* @name rand
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
*
*/
export const rand = signal(timeToRand);
@@ -151,36 +155,127 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
* irand(8).struct("x(3,8)").scale('C minor').note()
* n(irand(8)).struct("x x*2 x x*3").scale("C:minor")
*
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
/**
* pick from the list of values (or patterns of values) via the index using the given
* pattern of integers
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"]))
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return pat
.fmap((i) => {
const key = clamp(Math.round(i), 0, xs.length - 1);
return xs[key];
})
.innerJoin();
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/**
* pick from the list of values (or patterns of values) via the index using the given
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const inhabit = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const inhabitmod = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
@@ -269,9 +364,9 @@ Pattern.prototype.choose2 = function (...xs) {
* Picks one of the elements at random each cycle.
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(4)
* chooseCycles("bd", "hh", "sd").s().fast(8)
* @example
* "bd | hh | sd".s().fast(4)
* s("bd | hh | sd").fast(8)
*/
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
@@ -314,7 +409,7 @@ export const perlinWith = (pat) => {
* @name perlin
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
*
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
@@ -356,7 +451,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
/**
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
* 0 = 100% chance of removal
* 1 = 0% chance of removal
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
@@ -380,7 +475,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
*
* Randomly applies the given function by the given probability.
* Similar to {@link Pattern#someCyclesBy}
* Similar to `someCyclesBy`
*
* @name sometimesBy
* @memberof Pattern
@@ -388,7 +483,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
* s("hh*8").sometimesBy(.4, x=>x.speed("0.5"))
*/
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
@@ -406,7 +501,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh*4").sometimes(x=>x.speed("0.5"))
* s("hh*8").sometimes(x=>x.speed("0.5"))
*/
export const sometimes = register('sometimes', function (func, pat) {
return pat._sometimesBy(0.5, func);
@@ -415,7 +510,7 @@ export const sometimes = register('sometimes', function (func, pat) {
/**
*
* Randomly applies the given function by the given probability on a cycle by cycle basis.
* Similar to {@link Pattern#sometimesBy}
* Similar to `sometimesBy`
*
* @name someCyclesBy
* @memberof Pattern
@@ -423,7 +518,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
*/
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
@@ -445,7 +540,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @memberof Pattern
* @returns Pattern
* @example
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
*/
export const someCycles = register('someCycles', function (func, pat) {
return pat._someCyclesBy(0.5, func);
+60
View File
@@ -46,6 +46,7 @@ import {
rev,
time,
run,
pick,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -1057,4 +1058,63 @@ describe('Pattern', () => {
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
});
});
describe('inhabit', () => {
it('Can pattern named patterns', () => {
expect(
sameFirst(
sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
),
);
});
it('Can pattern indexed patterns', () => {
expect(
sameFirst(
sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
),
);
});
});
describe('pick', () => {
it('Can pattern named patterns', () => {
expect(
sameFirst(
sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
sequence(1, 20, 3, stack(4, 40)),
),
);
});
it('Can pattern indexed patterns', () => {
expect(
sameFirst(
sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
sequence(1, 20, 3, stack(4, 40)),
),
);
});
it('Clamps indexes', () => {
expect(
sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
);
});
it('Is backwards compatible', () => {
expect(
sameFirst(
pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
),
);
});
});
describe('pickmod', () => {
it('Wraps indexes', () => {
expect(
sameFirst(
sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
sequence(1, 20, 3, 40),
),
);
});
});
});
+7
View File
@@ -316,3 +316,10 @@ export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || ''));
}
export function objectMap(obj, fn) {
if (Array.isArray(obj)) {
return obj.map(fn);
}
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "0.11.0",
"version": "1.0.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "0.11.0",
"version": "1.0.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "0.11.0",
"version": "1.0.0",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "0.11.0",
"version": "1.0.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
+193 -96
View File
@@ -288,48 +288,50 @@ function peg$parse(input, options) {
var peg$f0 = function() { return parseFloat(text()); };
var peg$f1 = function() { return parseInt(text()); };
var peg$f2 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f3 = function(s) { return s };
var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f5 = function(a) { return a };
var peg$f6 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
var peg$f7 = function(a) { return x => x.options_['weight'] = a };
var peg$f8 = function(a) { return x => x.options_['reps'] = a };
var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
var peg$f15 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
var peg$f2 = function(chars) { const s = chars.join(""); return (s === ".") || (s === "_") };
var peg$f3 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f4 = function(s) { return s };
var peg$f5 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f6 = function(a) { return a };
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 };
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f13 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f15 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
var peg$f16 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
};
var peg$f16 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f17 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f18 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f19 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f20 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f21 = function(sc) { return sc; };
var peg$f22 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f23 = function(s) { return { name: "target", args : { name:s}}};
var peg$f24 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f27 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f28 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f29 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f30 = function(s, v) { return v};
var peg$f31 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f32 = function(sg) {return sg};
var peg$f33 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f34 = function(sc) { return sc };
var peg$f35 = function(c) { return c };
var peg$f36 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f37 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f38 = function() { return new CommandStub("hush")};
var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; };
var peg$f21 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f23 = function(sc) { return sc; };
var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f25 = function(s) { return { name: "target", args : { name:s}}};
var peg$f26 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f27 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f28 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f29 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f30 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f31 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f32 = function(s, v) { return v};
var peg$f33 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f34 = function(sg) {return sg};
var peg$f35 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f36 = function(sc) { return sc };
var peg$f37 = function(c) { return c };
var peg$f38 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f39 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f40 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -821,6 +823,30 @@ function peg$parse(input, options) {
return s0;
}
function peg$parsedot() {
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 46) {
s2 = peg$c0;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e1); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
s1 = [s1, s2, s3];
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsequote() {
var s0;
@@ -913,7 +939,7 @@ function peg$parse(input, options) {
}
function peg$parsestep() {
var s0, s1, s2, s3;
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = peg$parsews();
@@ -929,8 +955,20 @@ function peg$parse(input, options) {
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f2(s2);
peg$savedPos = peg$currPos;
s4 = peg$f2(s2);
if (s4) {
s4 = peg$FAILED;
} else {
s4 = undefined;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f3(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -966,7 +1004,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f3(s4);
s0 = peg$f4(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1014,7 +1052,7 @@ function peg$parse(input, options) {
}
s8 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f4(s4, s7);
s0 = peg$f5(s4, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1046,7 +1084,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f5(s2);
s0 = peg$f6(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1086,7 +1124,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f6(s4);
s0 = peg$f7(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1150,25 +1188,33 @@ function peg$parse(input, options) {
}
function peg$parseop_weight() {
var s0, s1, s2;
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 64) {
s1 = peg$c18;
s2 = peg$c18;
peg$currPos++;
} else {
s1 = peg$FAILED;
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f7(s2);
if (s2 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 95) {
s2 = peg$c10;
peg$currPos++;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
}
if (s2 !== peg$FAILED) {
s3 = peg$parsenumber();
if (s3 === peg$FAILED) {
s3 = null;
}
peg$savedPos = s0;
s0 = peg$f8(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1178,25 +1224,24 @@ function peg$parse(input, options) {
}
function peg$parseop_replicate() {
var s0, s1, s2;
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 33) {
s1 = peg$c19;
s2 = peg$c19;
peg$currPos++;
} else {
s1 = peg$FAILED;
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f8(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
if (s2 !== peg$FAILED) {
s3 = peg$parsenumber();
if (s3 === peg$FAILED) {
s3 = null;
}
peg$savedPos = s0;
s0 = peg$f9(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1246,7 +1291,7 @@ function peg$parse(input, options) {
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f9(s3, s7, s11);
s0 = peg$f10(s3, s7, s11);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1286,7 +1331,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f10(s2);
s0 = peg$f11(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1314,7 +1359,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f11(s2);
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1344,7 +1389,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f12(s2);
s0 = peg$f13(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1368,7 +1413,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f13(s2);
s0 = peg$f14(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1396,7 +1441,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f14(s2);
s0 = peg$f15(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1422,7 +1467,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f15(s1, s2);
s0 = peg$f16(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1447,7 +1492,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f16(s1);
s1 = peg$f17(s1);
}
s0 = s1;
@@ -1496,7 +1541,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f17(s1);
s1 = peg$f18(s1);
}
s0 = s1;
@@ -1545,7 +1590,56 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f18(s1);
s1 = peg$f19(s1);
}
s0 = s1;
return s0;
}
function peg$parsedot_tail() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$currPos;
s3 = peg$parsedot();
if (s3 !== peg$FAILED) {
s4 = peg$parsesequence();
if (s4 !== peg$FAILED) {
s2 = s4;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$currPos;
s3 = peg$parsedot();
if (s3 !== peg$FAILED) {
s4 = peg$parsesequence();
if (s4 !== peg$FAILED) {
s2 = s4;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f20(s1);
}
s0 = s1;
@@ -1561,12 +1655,15 @@ function peg$parse(input, options) {
s2 = peg$parsestack_tail();
if (s2 === peg$FAILED) {
s2 = peg$parsechoose_tail();
if (s2 === peg$FAILED) {
s2 = peg$parsedot_tail();
}
}
if (s2 === peg$FAILED) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f19(s1, s2);
s0 = peg$f21(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1586,7 +1683,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f20(s1, s2);
s0 = peg$f22(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1609,7 +1706,7 @@ function peg$parse(input, options) {
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f21(s4);
s0 = peg$f23(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1671,7 +1768,7 @@ function peg$parse(input, options) {
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f24(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1704,7 +1801,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s4);
s0 = peg$f25(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1749,7 +1846,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f24(s3, s5, s7);
s0 = peg$f26(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1782,7 +1879,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f25(s3);
s0 = peg$f27(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1811,7 +1908,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f26(s3);
s0 = peg$f28(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1840,7 +1937,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f27(s3);
s0 = peg$f29(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1869,7 +1966,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f28(s3);
s0 = peg$f30(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1911,7 +2008,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f29(s4);
s0 = peg$f31(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2003,7 +2100,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f32(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -2020,7 +2117,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f32(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -2040,7 +2137,7 @@ function peg$parse(input, options) {
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f31(s5, s6);
s0 = peg$f33(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2086,7 +2183,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f32(s1);
s0 = peg$f34(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2108,7 +2205,7 @@ function peg$parse(input, options) {
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s1, s5);
s0 = peg$f35(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2133,7 +2230,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f34(s1);
s1 = peg$f36(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2166,7 +2263,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f35(s2);
s0 = peg$f37(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2191,7 +2288,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f36(s3);
s0 = peg$f38(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2220,7 +2317,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f37(s3);
s0 = peg$f39(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2246,7 +2343,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f38();
s1 = peg$f40();
}
s0 = s1;
+13 -7
View File
@@ -98,6 +98,7 @@ DIGIT = [0-9]
ws "whitespace" = [ \n\r\t\u00A0]*
comma = ws "," ws
pipe = ws "|" ws
dot = ws "." ws
quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
@@ -105,7 +106,8 @@ quote = '"' / "'"
// single step definition (e.g bd)
step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" =
unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
step = ws chars:step_char+ ws !{ const s = chars.join(""); return (s === ".") || (s === "_") } { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
@@ -129,11 +131,11 @@ slice = step / sub_cycle / polymeter / slow_sequence
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
op_weight = ws ("@" / "_") a:number?
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
op_replicate = "!"a:number
{ return x => x.options_['reps'] = a }
op_replicate = ws "!" a:number?
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
@@ -175,9 +177,13 @@ stack_tail = tail:(comma @sequence)+
choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
// a foot separates subsequences, as an alternative to wrapping them in []
dot_tail = tail:(dot @sequence)+
{ return { alignment: 'feet', list: tail, seed: seed++ }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
@@ -287,4 +293,4 @@ Lt = [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FC
Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A]
// Number, Letter
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
+3
View File
@@ -107,6 +107,9 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
if (alignment === 'rand') {
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
}
if (alignment === 'feet') {
return strudel.fastcat(...children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "0.11.0",
"version": "1.0.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+14
View File
@@ -73,6 +73,10 @@ describe('mini', () => {
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
});
it('supports replication via repeated !', () => {
expect(minS('a ! ! b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!! d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
});
it('supports euclidean rhythms', () => {
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
});
@@ -190,6 +194,16 @@ describe('mini', () => {
it('supports patterned ranges', () => {
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
});
it('supports the . operator', () => {
expect(minS('a . b c')).toEqual(minS('a [b c]'));
expect(minS('a . b c . [d e f . g h]')).toEqual(minS('a [b c] [[d e f] [g h]]'));
});
it('supports the _ operator', () => {
expect(minS('a _ b _ _')).toEqual(minS('a@2 b@3'));
});
it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
});
});
describe('getLeafLocation', () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "0.11.0",
"version": "1.0.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "0.11.0",
"version": "1.0.0",
"description": "Strudel REPL as a Web Component",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "0.11.0",
"version": "1.0.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "0.11.0",
"version": "1.0.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.10.0",
"version": "1.0.0",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "0.11.0",
"version": "1.0.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+2 -2
View File
@@ -157,9 +157,9 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* n(rand.range(0,12).segment(8).round())
* n(rand.range(0,12).segment(8))
* .scale("C:ritusen")
* .s("folkharp")
* .s("piano")
*/
export const scale = register('scale', function (scale, pat) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "0.11.0",
"version": "1.0.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "0.11.0",
"version": "1.0.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "0.11.0",
"version": "1.0.0",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "0.11.0",
"version": "1.0.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {
+57 -4
View File
@@ -476,20 +476,29 @@ importers:
specifier: ^1.1.0
version: 1.1.0(@vitest/ui@1.1.0)
tools/dbpatch:
dependencies:
csv:
specifier: ^6.3.6
version: 6.3.6
website:
dependencies:
'@algolia/client-search':
specifier: ^4.22.0
version: 4.22.0
'@astro-community/astro-embed-youtube':
specifier: ^0.4.3
version: 0.4.3(astro@4.0.8)
specifier: ^0.4.4
version: 0.4.4(astro@4.0.8)
'@astrojs/mdx':
specifier: ^2.0.3
version: 2.0.3(astro@4.0.8)
'@astrojs/react':
specifier: ^3.0.9
version: 3.0.9(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.11)
'@astrojs/rss':
specifier: ^4.0.2
version: 4.0.2
'@astrojs/tailwind':
specifier: ^5.1.0
version: 5.1.0(astro@4.0.8)(tailwindcss@3.4.0)
@@ -803,8 +812,8 @@ packages:
leven: 3.1.0
dev: true
/@astro-community/astro-embed-youtube@0.4.3(astro@4.0.8):
resolution: {integrity: sha512-zXtPmR9yxrTo6cuLhH8v+r62bsXbsLJgsU2FiZalPr4bXJxAUQEIlG46S/qK0AEXi9uNShKqy4+zBaJ98xTVEg==}
/@astro-community/astro-embed-youtube@0.4.4(astro@4.0.8):
resolution: {integrity: sha512-fYlycLrJFNnibZ9VHPSJO766kO2IgqYQU4mBd4iaDMaicL0gGX9cVZ80QdnpzGrI6w0XOJOY7prx86eWEVBy8w==}
peerDependencies:
astro: ^2.0.0 || ^3.0.0-beta || ^4.0.0-beta
dependencies:
@@ -890,6 +899,13 @@ packages:
- vite
dev: false
/@astrojs/rss@4.0.2:
resolution: {integrity: sha512-Hb9GKAyvsn5EUjZtB6SniesBScMQe7SQinEHLY5EFa74QEvgcWaXTmA0Mb0P3vqDSN3d/NTYbGivprrSAawfnA==}
dependencies:
fast-xml-parser: 4.3.3
kleur: 4.1.5
dev: false
/@astrojs/tailwind@5.1.0(astro@4.0.8)(tailwindcss@3.4.0):
resolution: {integrity: sha512-BJoCDKuWhU9FT2qYg+fr6Nfb3qP4ShtyjXGHKA/4mHN94z7BGcmauQK23iy+YH5qWvTnhqkd6mQPQ1yTZTe9Ig==}
peerDependencies:
@@ -6213,6 +6229,28 @@ packages:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
dev: false
/csv-generate@4.3.1:
resolution: {integrity: sha512-7YeeJq+44/I/O5N2sr2qBMcHZXhpfe38eh7DOFxyMtYO+Pir7kIfgFkW5MPksqKqqR6+/wX7UGoZm1Ot11151w==}
dev: false
/csv-parse@5.5.3:
resolution: {integrity: sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A==}
dev: false
/csv-stringify@6.4.5:
resolution: {integrity: sha512-SPu1Vnh8U5EnzpNOi1NDBL5jU5Rx7DVHr15DNg9LXDTAbQlAVAmEbVt16wZvEW9Fu9Qt4Ji8kmeCJ2B1+4rFTQ==}
dev: false
/csv@6.3.6:
resolution: {integrity: sha512-jsEsX2HhGp7xiwrJu5srQavKsh+HUJcCi78Ar3m4jlmFKRoTkkMy7ZZPP+LnQChmaztW+uj44oyfMb59daAs/Q==}
engines: {node: '>= 0.1.90'}
dependencies:
csv-generate: 4.3.1
csv-parse: 5.5.3
csv-stringify: 6.4.5
stream-transform: 3.3.0
dev: false
/dargs@7.0.0:
resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
engines: {node: '>=8'}
@@ -7182,6 +7220,13 @@ packages:
/fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
/fast-xml-parser@4.3.3:
resolution: {integrity: sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==}
hasBin: true
dependencies:
strnum: 1.0.5
dev: false
/fastq@1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies:
@@ -12457,6 +12502,10 @@ packages:
transitivePeerDependencies:
- supports-color
/stream-transform@3.3.0:
resolution: {integrity: sha512-pG1NeDdmErNYKtvTpFayrEueAmL0xVU5wd22V7InGnatl4Ocq3HY7dcXIKj629kXvYQvglCC7CeDIGAlx1RNGA==}
dev: false
/stream-via@1.0.4:
resolution: {integrity: sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==}
engines: {node: '>=0.10.0'}
@@ -12637,6 +12686,10 @@ packages:
acorn: 8.11.3
dev: true
/strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
dev: false
/strong-log-transformer@2.1.0:
resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==}
engines: {node: '>=4'}
+1
View File
@@ -2,4 +2,5 @@ packages:
# all packages in direct subdirs of packages/
- "packages/*"
- "examples/*"
- "tools/dbpatch"
- "website/"
File diff suppressed because it is too large Load Diff
+9 -405
View File
@@ -339,402 +339,6 @@ exports[`renders tunes > tune: blippyRhodes 1`] = `
]
`;
exports[`renders tunes > tune: bridgeIsOver 1`] = `
[
"[ -155/52 ⇜ (0/1 → 5/52) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ -75/26 ⇜ (0/1 → 5/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -145/52 ⇜ (0/1 → 15/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -35/13 ⇜ (0/1 → 5/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ -35/13 ⇜ (0/1 → 5/13) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ -135/52 ⇜ (0/1 → 5/13) ⇝ 25/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ -5/2 ⇜ (0/1 → 5/13) ⇝ 15/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -125/52 ⇜ (0/1 → 5/13) ⇝ 35/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -30/13 ⇜ (0/1 → 5/13) ⇝ 10/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ -115/52 ⇜ (0/1 → 5/13) ⇝ 45/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -55/26 ⇜ (0/1 → 5/13) ⇝ 25/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -105/52 ⇜ (0/1 → 5/13) ⇝ 55/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 0/1 → 5/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ -135/52 ⇜ (0/1 → 25/52) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -5/2 ⇜ (0/1 → 15/26) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -125/52 ⇜ (0/1 → 35/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ -30/13 ⇜ (0/1 → 10/13) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ -115/52 ⇜ (0/1 → 10/13) ⇝ 45/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ -55/26 ⇜ (0/1 → 10/13) ⇝ 25/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -105/52 ⇜ (0/1 → 10/13) ⇝ 55/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -25/13 ⇜ (0/1 → 10/13) ⇝ 15/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ -95/52 ⇜ (0/1 → 10/13) ⇝ 5/4 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -45/26 ⇜ (0/1 → 10/13) ⇝ 35/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -85/52 ⇜ (0/1 → 10/13) ⇝ 75/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (0/1 → 1/1) ⇝ 40/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ (0/1 → 1/1) ⇝ 80/13 | s:mad ]",
"[ (5/52 → 1/1) ⇝ 165/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (5/26 → 1/1) ⇝ 85/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (15/52 → 1/1) ⇝ 175/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -135/52 ⇜ (5/13 → 25/52) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ -5/2 ⇜ (5/13 → 15/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -125/52 ⇜ (5/13 → 35/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -30/13 ⇜ (5/13 → 10/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 5/13 → 10/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ -115/52 ⇜ (5/13 → 45/52) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -55/26 ⇜ (5/13 → 25/26) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -105/52 ⇜ (5/13 → 1/1) ⇝ 55/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (5/13 → 1/1) ⇝ 45/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ (5/13 → 1/1) ⇝ 45/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ (25/52 → 1/1) ⇝ 185/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (25/52 → 1/1) ⇝ 185/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (15/26 → 1/1) ⇝ 95/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (15/26 → 1/1) ⇝ 95/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (35/52 → 1/1) ⇝ 15/4 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (35/52 → 1/1) ⇝ 15/4 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ -115/52 ⇜ (10/13 → 45/52) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ -55/26 ⇜ (10/13 → 25/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 → 155/156 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ -105/52 ⇜ (10/13 → 1/1) ⇝ 55/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -25/13 ⇜ (10/13 → 1/1) ⇝ 15/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ -95/52 ⇜ (10/13 → 1/1) ⇝ 5/4 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -45/26 ⇜ (10/13 → 1/1) ⇝ 35/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -85/52 ⇜ (10/13 → 1/1) ⇝ 75/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (10/13 → 1/1) ⇝ 50/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ (10/13 → 1/1) ⇝ 50/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ (45/52 → 1/1) ⇝ 205/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (45/52 → 1/1) ⇝ 205/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (25/26 → 1/1) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (25/26 → 1/1) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (155/156 → 1/1) ⇝ 15/13 | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ -105/52 ⇜ (1/1 → 55/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ -105/52 ⇜ (1/1 → 55/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ -25/13 ⇜ (1/1 → 15/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 155/156 ⇜ (1/1 → 15/13) | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ -95/52 ⇜ (1/1 → 5/4) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ -45/26 ⇜ (1/1 → 35/26) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ -85/52 ⇜ (1/1 → 75/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 0/1 ⇜ (1/1 → 2/1) ⇝ 40/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 0/1 ⇜ (1/1 → 2/1) ⇝ 80/13 | s:mad ]",
"[ 5/52 ⇜ (1/1 → 2/1) ⇝ 165/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 5/26 ⇜ (1/1 → 2/1) ⇝ 85/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 15/52 ⇜ (1/1 → 2/1) ⇝ 175/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 5/13 ⇜ (1/1 → 2/1) ⇝ 45/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/13 ⇜ (1/1 → 2/1) ⇝ 45/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 25/52 ⇜ (1/1 → 2/1) ⇝ 185/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 25/52 ⇜ (1/1 → 2/1) ⇝ 185/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 15/26 ⇜ (1/1 → 2/1) ⇝ 95/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 15/26 ⇜ (1/1 → 2/1) ⇝ 95/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 35/52 ⇜ (1/1 → 2/1) ⇝ 15/4 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 35/52 ⇜ (1/1 → 2/1) ⇝ 15/4 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 ⇜ (1/1 → 2/1) ⇝ 50/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 10/13 ⇜ (1/1 → 2/1) ⇝ 50/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 45/52 ⇜ (1/1 → 2/1) ⇝ 205/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/52 ⇜ (1/1 → 2/1) ⇝ 205/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 25/26 ⇜ (1/1 → 2/1) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 25/26 ⇜ (1/1 → 2/1) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (55/52 → 2/1) ⇝ 215/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (55/52 → 2/1) ⇝ 215/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 → 20/13 | note:ab2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4537037037037037 ]",
"[ (15/13 → 2/1) ⇝ 55/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ (5/4 → 2/1) ⇝ 225/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (35/26 → 2/1) ⇝ 115/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (75/52 → 2/1) ⇝ 235/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 20/13 → 25/13 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ (25/13 → 2/1) ⇝ 30/13 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 25/13 ⇜ (2/1 → 30/13) | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 0/1 ⇜ (2/1 → 3/1) ⇝ 40/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 0/1 ⇜ (2/1 → 3/1) ⇝ 80/13 | s:mad ]",
"[ 5/52 ⇜ (2/1 → 3/1) ⇝ 165/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 5/26 ⇜ (2/1 → 3/1) ⇝ 85/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 15/52 ⇜ (2/1 → 3/1) ⇝ 175/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 5/13 ⇜ (2/1 → 3/1) ⇝ 45/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/13 ⇜ (2/1 → 3/1) ⇝ 45/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 25/52 ⇜ (2/1 → 3/1) ⇝ 185/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 25/52 ⇜ (2/1 → 3/1) ⇝ 185/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 15/26 ⇜ (2/1 → 3/1) ⇝ 95/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 15/26 ⇜ (2/1 → 3/1) ⇝ 95/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 35/52 ⇜ (2/1 → 3/1) ⇝ 15/4 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 35/52 ⇜ (2/1 → 3/1) ⇝ 15/4 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 ⇜ (2/1 → 3/1) ⇝ 50/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 10/13 ⇜ (2/1 → 3/1) ⇝ 50/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 45/52 ⇜ (2/1 → 3/1) ⇝ 205/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/52 ⇜ (2/1 → 3/1) ⇝ 205/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 25/26 ⇜ (2/1 → 3/1) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 25/26 ⇜ (2/1 → 3/1) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 55/52 ⇜ (2/1 → 3/1) ⇝ 215/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 55/52 ⇜ (2/1 → 3/1) ⇝ 215/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 ⇜ (2/1 → 3/1) ⇝ 55/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/4 ⇜ (2/1 → 3/1) ⇝ 225/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 35/26 ⇜ (2/1 → 3/1) ⇝ 115/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 75/52 ⇜ (2/1 → 3/1) ⇝ 235/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 30/13 → 395/156 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 395/156 → 35/13 | note:ab2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4537037037037037 ]",
"[ (35/13 → 3/1) ⇝ 40/13 | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ 0/1 ⇜ (3/1 → 40/13) | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 0/1 ⇜ (3/1 → 40/13) ⇝ 80/13 | s:mad ]",
"[ 5/52 ⇜ (3/1 → 40/13) ⇝ 165/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 5/26 ⇜ (3/1 → 40/13) ⇝ 85/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 15/52 ⇜ (3/1 → 40/13) ⇝ 175/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 5/13 ⇜ (3/1 → 40/13) ⇝ 45/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/13 ⇜ (3/1 → 40/13) ⇝ 45/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 25/52 ⇜ (3/1 → 40/13) ⇝ 185/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 25/52 ⇜ (3/1 → 40/13) ⇝ 185/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 15/26 ⇜ (3/1 → 40/13) ⇝ 95/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 15/26 ⇜ (3/1 → 40/13) ⇝ 95/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 35/52 ⇜ (3/1 → 40/13) ⇝ 15/4 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 35/52 ⇜ (3/1 → 40/13) ⇝ 15/4 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 ⇜ (3/1 → 40/13) ⇝ 50/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 10/13 ⇜ (3/1 → 40/13) ⇝ 50/13 | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 45/52 ⇜ (3/1 → 40/13) ⇝ 205/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/52 ⇜ (3/1 → 40/13) ⇝ 205/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 25/26 ⇜ (3/1 → 40/13) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 25/26 ⇜ (3/1 → 40/13) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 55/52 ⇜ (3/1 → 40/13) ⇝ 215/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 55/52 ⇜ (3/1 → 40/13) ⇝ 215/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 ⇜ (3/1 → 40/13) ⇝ 55/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/4 ⇜ (3/1 → 40/13) ⇝ 225/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 35/26 ⇜ (3/1 → 40/13) ⇝ 115/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 75/52 ⇜ (3/1 → 40/13) ⇝ 235/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 35/13 ⇜ (3/1 → 40/13) | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ 5/52 ⇜ (40/13 → 165/52) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 5/26 ⇜ (40/13 → 85/26) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 15/52 ⇜ (40/13 → 175/52) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 5/13 ⇜ (40/13 → 45/13) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/13 ⇜ (40/13 → 45/13) | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 25/52 ⇜ (40/13 → 45/13) ⇝ 185/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 15/26 ⇜ (40/13 → 45/13) ⇝ 95/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 35/52 ⇜ (40/13 → 45/13) ⇝ 15/4 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 ⇜ (40/13 → 45/13) ⇝ 50/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/52 ⇜ (40/13 → 45/13) ⇝ 205/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 25/26 ⇜ (40/13 → 45/13) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 55/52 ⇜ (40/13 → 45/13) ⇝ 215/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 40/13 → 45/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 25/52 ⇜ (40/13 → 185/52) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 15/26 ⇜ (40/13 → 95/26) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 35/52 ⇜ (40/13 → 15/4) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 10/13 ⇜ (40/13 → 50/13) | clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]",
"[ 45/52 ⇜ (40/13 → 50/13) ⇝ 205/52 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 25/26 ⇜ (40/13 → 50/13) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 55/52 ⇜ (40/13 → 50/13) ⇝ 215/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 ⇜ (40/13 → 50/13) ⇝ 55/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/4 ⇜ (40/13 → 50/13) ⇝ 225/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 35/26 ⇜ (40/13 → 50/13) ⇝ 115/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 75/52 ⇜ (40/13 → 50/13) ⇝ 235/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 0/1 ⇜ (40/13 → 4/1) ⇝ 80/13 | s:mad ]",
"[ (40/13 → 4/1) ⇝ 80/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (165/52 → 4/1) ⇝ 25/4 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (85/26 → 4/1) ⇝ 165/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ (175/52 → 4/1) ⇝ 335/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 25/52 ⇜ (45/13 → 185/52) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 15/26 ⇜ (45/13 → 95/26) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 35/52 ⇜ (45/13 → 15/4) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 10/13 ⇜ (45/13 → 50/13) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/13 → 50/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 45/52 ⇜ (45/13 → 205/52) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 25/26 ⇜ (45/13 → 4/1) ⇝ 105/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 55/52 ⇜ (45/13 → 4/1) ⇝ 215/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (45/13 → 4/1) ⇝ 85/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (45/13 → 4/1) ⇝ 85/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (185/52 → 4/1) ⇝ 345/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (185/52 → 4/1) ⇝ 345/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (95/26 → 4/1) ⇝ 175/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (95/26 → 4/1) ⇝ 175/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ (15/4 → 4/1) ⇝ 355/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (15/4 → 4/1) ⇝ 355/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/52 ⇜ (50/13 → 205/52) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 25/26 ⇜ (50/13 → 4/1) ⇝ 105/26 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 55/52 ⇜ (50/13 → 4/1) ⇝ 215/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 ⇜ (50/13 → 4/1) ⇝ 55/13 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/4 ⇜ (50/13 → 4/1) ⇝ 225/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 35/26 ⇜ (50/13 → 4/1) ⇝ 115/26 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 75/52 ⇜ (50/13 → 4/1) ⇝ 235/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (50/13 → 4/1) ⇝ 635/156 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ (50/13 → 4/1) ⇝ 90/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (50/13 → 4/1) ⇝ 90/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ (205/52 → 4/1) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (205/52 → 4/1) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 25/26 ⇜ (4/1 → 105/26) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 25/26 ⇜ (4/1 → 105/26) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 50/13 ⇜ (4/1 → 635/156) | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 55/52 ⇜ (4/1 → 215/52) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 55/52 ⇜ (4/1 → 215/52) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/13 ⇜ (4/1 → 55/13) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 5/4 ⇜ (4/1 → 225/52) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 35/26 ⇜ (4/1 → 115/26) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 75/52 ⇜ (4/1 → 235/52) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 0/1 ⇜ (4/1 → 5/1) ⇝ 80/13 | s:mad ]",
"[ 40/13 ⇜ (4/1 → 5/1) ⇝ 80/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 165/52 ⇜ (4/1 → 5/1) ⇝ 25/4 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 85/26 ⇜ (4/1 → 5/1) ⇝ 165/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 175/52 ⇜ (4/1 → 5/1) ⇝ 335/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/13 ⇜ (4/1 → 5/1) ⇝ 85/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/13 ⇜ (4/1 → 5/1) ⇝ 85/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 185/52 ⇜ (4/1 → 5/1) ⇝ 345/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 185/52 ⇜ (4/1 → 5/1) ⇝ 345/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 95/26 ⇜ (4/1 → 5/1) ⇝ 175/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 95/26 ⇜ (4/1 → 5/1) ⇝ 175/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/4 ⇜ (4/1 → 5/1) ⇝ 355/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 15/4 ⇜ (4/1 → 5/1) ⇝ 355/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 50/13 ⇜ (4/1 → 5/1) ⇝ 90/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 50/13 ⇜ (4/1 → 5/1) ⇝ 90/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 205/52 ⇜ (4/1 → 5/1) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 205/52 ⇜ (4/1 → 5/1) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (105/26 → 5/1) ⇝ 185/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (105/26 → 5/1) ⇝ 185/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 635/156 → 55/13 | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ (215/52 → 5/1) ⇝ 375/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (215/52 → 5/1) ⇝ 375/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 → 60/13 | note:ab2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4537037037037037 ]",
"[ (55/13 → 5/1) ⇝ 95/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ (225/52 → 5/1) ⇝ 385/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (115/26 → 5/1) ⇝ 15/2 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (235/52 → 5/1) ⇝ 395/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 60/13 → 5/1 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 5/1 → 70/13 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 0/1 ⇜ (5/1 → 6/1) ⇝ 80/13 | s:mad ]",
"[ 40/13 ⇜ (5/1 → 6/1) ⇝ 80/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 165/52 ⇜ (5/1 → 6/1) ⇝ 25/4 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 85/26 ⇜ (5/1 → 6/1) ⇝ 165/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 175/52 ⇜ (5/1 → 6/1) ⇝ 335/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/13 ⇜ (5/1 → 6/1) ⇝ 85/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/13 ⇜ (5/1 → 6/1) ⇝ 85/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 185/52 ⇜ (5/1 → 6/1) ⇝ 345/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 185/52 ⇜ (5/1 → 6/1) ⇝ 345/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 95/26 ⇜ (5/1 → 6/1) ⇝ 175/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 95/26 ⇜ (5/1 → 6/1) ⇝ 175/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/4 ⇜ (5/1 → 6/1) ⇝ 355/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 15/4 ⇜ (5/1 → 6/1) ⇝ 355/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 50/13 ⇜ (5/1 → 6/1) ⇝ 90/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 50/13 ⇜ (5/1 → 6/1) ⇝ 90/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 205/52 ⇜ (5/1 → 6/1) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 205/52 ⇜ (5/1 → 6/1) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 105/26 ⇜ (5/1 → 6/1) ⇝ 185/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 105/26 ⇜ (5/1 → 6/1) ⇝ 185/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 215/52 ⇜ (5/1 → 6/1) ⇝ 375/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 215/52 ⇜ (5/1 → 6/1) ⇝ 375/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 ⇜ (5/1 → 6/1) ⇝ 95/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 225/52 ⇜ (5/1 → 6/1) ⇝ 385/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 115/26 ⇜ (5/1 → 6/1) ⇝ 15/2 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 235/52 ⇜ (5/1 → 6/1) ⇝ 395/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (70/13 → 6/1) ⇝ 80/13 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 0/1 ⇜ (6/1 → 80/13) | s:mad ]",
"[ 40/13 ⇜ (6/1 → 80/13) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 165/52 ⇜ (6/1 → 80/13) ⇝ 25/4 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 85/26 ⇜ (6/1 → 80/13) ⇝ 165/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 175/52 ⇜ (6/1 → 80/13) ⇝ 335/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/13 ⇜ (6/1 → 80/13) ⇝ 85/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/13 ⇜ (6/1 → 80/13) ⇝ 85/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 185/52 ⇜ (6/1 → 80/13) ⇝ 345/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 185/52 ⇜ (6/1 → 80/13) ⇝ 345/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 95/26 ⇜ (6/1 → 80/13) ⇝ 175/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 95/26 ⇜ (6/1 → 80/13) ⇝ 175/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/4 ⇜ (6/1 → 80/13) ⇝ 355/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 15/4 ⇜ (6/1 → 80/13) ⇝ 355/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 50/13 ⇜ (6/1 → 80/13) ⇝ 90/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 50/13 ⇜ (6/1 → 80/13) ⇝ 90/13 | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 205/52 ⇜ (6/1 → 80/13) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 205/52 ⇜ (6/1 → 80/13) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 105/26 ⇜ (6/1 → 80/13) ⇝ 185/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 105/26 ⇜ (6/1 → 80/13) ⇝ 185/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 215/52 ⇜ (6/1 → 80/13) ⇝ 375/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 215/52 ⇜ (6/1 → 80/13) ⇝ 375/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 ⇜ (6/1 → 80/13) ⇝ 95/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 225/52 ⇜ (6/1 → 80/13) ⇝ 385/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 115/26 ⇜ (6/1 → 80/13) ⇝ 15/2 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 235/52 ⇜ (6/1 → 80/13) ⇝ 395/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 70/13 ⇜ (6/1 → 80/13) | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
"[ 165/52 ⇜ (80/13 → 25/4) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 85/26 ⇜ (80/13 → 165/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 175/52 ⇜ (80/13 → 335/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 45/13 ⇜ (80/13 → 85/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 45/13 ⇜ (80/13 → 85/13) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 185/52 ⇜ (80/13 → 85/13) ⇝ 345/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 95/26 ⇜ (80/13 → 85/13) ⇝ 175/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/4 ⇜ (80/13 → 85/13) ⇝ 355/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 50/13 ⇜ (80/13 → 85/13) ⇝ 90/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 205/52 ⇜ (80/13 → 85/13) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 105/26 ⇜ (80/13 → 85/13) ⇝ 185/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 215/52 ⇜ (80/13 → 85/13) ⇝ 375/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 80/13 → 85/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 185/52 ⇜ (80/13 → 345/52) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 95/26 ⇜ (80/13 → 175/26) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 15/4 ⇜ (80/13 → 355/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 50/13 ⇜ (80/13 → 90/13) | clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]",
"[ 205/52 ⇜ (80/13 → 90/13) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 105/26 ⇜ (80/13 → 90/13) ⇝ 185/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 215/52 ⇜ (80/13 → 90/13) ⇝ 375/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 ⇜ (80/13 → 90/13) ⇝ 95/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 225/52 ⇜ (80/13 → 90/13) ⇝ 385/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 115/26 ⇜ (80/13 → 90/13) ⇝ 15/2 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 235/52 ⇜ (80/13 → 90/13) ⇝ 395/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (80/13 → 7/1) ⇝ 120/13 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (80/13 → 7/1) ⇝ 160/13 | s:mad ]",
"[ (25/4 → 7/1) ⇝ 485/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ (165/26 → 7/1) ⇝ 245/26 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ (335/52 → 7/1) ⇝ 495/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 185/52 ⇜ (85/13 → 345/52) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 95/26 ⇜ (85/13 → 175/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 15/4 ⇜ (85/13 → 355/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 50/13 ⇜ (85/13 → 90/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 85/13 → 90/13 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 205/52 ⇜ (85/13 → 7/1) ⇝ 365/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 105/26 ⇜ (85/13 → 7/1) ⇝ 185/26 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 215/52 ⇜ (85/13 → 7/1) ⇝ 375/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (85/13 → 7/1) ⇝ 125/13 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (85/13 → 7/1) ⇝ 125/13 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (345/52 → 7/1) ⇝ 505/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (345/52 → 7/1) ⇝ 505/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ (175/26 → 7/1) ⇝ 255/26 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (175/26 → 7/1) ⇝ 255/26 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ (355/52 → 7/1) ⇝ 515/52 | clip:1 note:F#5 s:piano release:0.1 pan:0.6111111111111112 ]",
"[ (355/52 → 7/1) ⇝ 515/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 205/52 ⇜ (90/13 → 7/1) ⇝ 365/52 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 105/26 ⇜ (90/13 → 7/1) ⇝ 185/26 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 215/52 ⇜ (90/13 → 7/1) ⇝ 375/52 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 ⇜ (90/13 → 7/1) ⇝ 95/13 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 225/52 ⇜ (90/13 → 7/1) ⇝ 385/52 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 115/26 ⇜ (90/13 → 7/1) ⇝ 15/2 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 235/52 ⇜ (90/13 → 7/1) ⇝ 395/52 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (90/13 → 7/1) ⇝ 1115/156 | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ (90/13 → 7/1) ⇝ 10/1 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (90/13 → 7/1) ⇝ 10/1 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 205/52 ⇜ (7/1 → 365/52) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 205/52 ⇜ (7/1 → 365/52) | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 105/26 ⇜ (7/1 → 185/26) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 105/26 ⇜ (7/1 → 185/26) | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 90/13 ⇜ (7/1 → 1115/156) | note:c3 gain:0.8 clip:1 s:piano release:0.1 pan:0.4722222222222222 ]",
"[ 215/52 ⇜ (7/1 → 375/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 215/52 ⇜ (7/1 → 375/52) | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 55/13 ⇜ (7/1 → 95/13) | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 225/52 ⇜ (7/1 → 385/52) | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 115/26 ⇜ (7/1 → 15/2) | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 235/52 ⇜ (7/1 → 395/52) | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 80/13 ⇜ (7/1 → 8/1) ⇝ 120/13 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 80/13 ⇜ (7/1 → 8/1) ⇝ 160/13 | s:mad ]",
"[ 25/4 ⇜ (7/1 → 8/1) ⇝ 485/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 165/26 ⇜ (7/1 → 8/1) ⇝ 245/26 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 335/52 ⇜ (7/1 → 8/1) ⇝ 495/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 85/13 ⇜ (7/1 → 8/1) ⇝ 125/13 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 85/13 ⇜ (7/1 → 8/1) ⇝ 125/13 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ 345/52 ⇜ (7/1 → 8/1) ⇝ 505/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ 345/52 ⇜ (7/1 → 8/1) ⇝ 505/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ 175/26 ⇜ (7/1 → 8/1) ⇝ 255/26 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ 175/26 ⇜ (7/1 → 8/1) ⇝ 255/26 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 355/52 ⇜ (7/1 → 8/1) ⇝ 515/52 | clip:1 note:F#5 s:piano release:0.1 pan:0.6111111111111112 ]",
"[ 355/52 ⇜ (7/1 → 8/1) ⇝ 515/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 90/13 ⇜ (7/1 → 8/1) ⇝ 10/1 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ 90/13 ⇜ (7/1 → 8/1) ⇝ 10/1 | clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]",
"[ (365/52 → 8/1) ⇝ 525/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (365/52 → 8/1) ⇝ 525/52 | clip:1 note:F#4 s:piano release:0.1 pan:0.5555555555555556 ]",
"[ (185/26 → 8/1) ⇝ 265/26 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (185/26 → 8/1) ⇝ 265/26 | clip:1 note:G#4 s:piano release:0.1 pan:0.5648148148148149 ]",
"[ 1115/156 → 95/13 | note:bb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.46296296296296297 ]",
"[ (375/52 → 8/1) ⇝ 535/52 | clip:1 note:F#5 s:piano release:0.1 pan:0.6111111111111112 ]",
"[ (375/52 → 8/1) ⇝ 535/52 | clip:1 note:A#4 s:piano release:0.1 pan:0.5740740740740741 ]",
"[ 95/13 → 100/13 | note:ab2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4537037037037037 ]",
"[ (95/13 → 8/1) ⇝ 135/13 | clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]",
"[ (385/52 → 8/1) ⇝ 545/52 | clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]",
"[ (15/2 → 8/1) ⇝ 275/26 | clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]",
"[ (395/52 → 8/1) ⇝ 555/52 | clip:1 note:F#5 s:piano release:0.1 pan:0.6111111111111112 ]",
"[ (100/13 → 8/1) ⇝ 105/13 | note:gb2 gain:0.8 clip:1 s:piano release:0.1 pan:0.4444444444444444 ]",
]
`;
exports[`renders tunes > tune: caverave 1`] = `
[
"[ 0/1 → 1/2 | s:bd gain:0.8 ]",
@@ -1507,14 +1111,14 @@ exports[`renders tunes > tune: caverave 1`] = `
exports[`renders tunes > tune: chop 1`] = `
[
"[ 0/1 → 1/4 | s:p speed:0.03125 unit:c begin:0 end:0.0078125 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 0/1 → 1/4 | s:p speed:0.03125 unit:c begin:0.0234375 end:0.03125 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/4 → 1/2 | s:p speed:0.03125 unit:c begin:0.0078125 end:0.015625 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/4 → 1/2 | s:p speed:0.03125 unit:c begin:0.015625 end:0.0234375 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/2 → 3/4 | s:p speed:0.03125 unit:c begin:0.015625 end:0.0234375 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/2 → 3/4 | s:p speed:0.03125 unit:c begin:0.0078125 end:0.015625 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 3/4 → 1/1 | s:p speed:0.03125 unit:c begin:0.0234375 end:0.03125 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 3/4 → 1/1 | s:p speed:0.03125 unit:c begin:0 end:0.0078125 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 0/1 → 1/4 | s:p speed:0.015625 unit:c begin:0 end:0.0078125 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 0/1 → 1/4 | s:p speed:0.015625 unit:c begin:0.0234375 end:0.03125 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/4 → 1/2 | s:p speed:0.015625 unit:c begin:0.0078125 end:0.015625 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/4 → 1/2 | s:p speed:0.015625 unit:c begin:0.015625 end:0.0234375 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/2 → 3/4 | s:p speed:0.015625 unit:c begin:0.015625 end:0.0234375 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 1/2 → 3/4 | s:p speed:0.015625 unit:c begin:0.0078125 end:0.015625 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 3/4 → 1/1 | s:p speed:0.015625 unit:c begin:0.0234375 end:0.03125 pan:0 shape:0.4 decay:0.1 sustain:0.6 ]",
"[ 3/4 → 1/1 | s:p speed:0.015625 unit:c begin:0 end:0.0078125 pan:1 shape:0.4 decay:0.1 sustain:0.6 ]",
]
`;
@@ -1545,7 +1149,7 @@ exports[`renders tunes > tune: dinofunk 1`] = `
"[ 0/1 → 1/4 | note:Ab4 s:sawtooth cutoff:1239.2541394619345 gain:0.8 decay:0.05125097280354112 sustain:0 delay:0.2561353071307281 room:1 ]",
"[ 0/1 → 1/4 | note:68.1 s:sawtooth cutoff:1239.2541394619345 gain:0.8 decay:0.05125097280354112 sustain:0 delay:0.2561353071307281 room:1 ]",
"[ 0/1 → 1/2 | s:bd ]",
"[ (0/1 → 1/1) ⇝ 8/1 | s:bass speed:0.125 unit:c clip:1 ]",
"[ (0/1 → 1/1) ⇝ 8/1 | s:bass speed:0.0625 unit:c clip:1 ]",
"[ (0/1 → 1/1) ⇝ 8/1 | note:b4 s:dino delay:0.8 room:0.5 ]",
"[ 1/4 → 1/2 | s:hh ]",
"[ 1/4 → 1/2 | note:Gb3 ]",
-10
View File
@@ -1,10 +0,0 @@
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
const { data } = await supabase.from('code');
console.log(JSON.stringify(data));
-1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -175,6 +175,7 @@ evalScope(
loadCSound,
loadCsound,
loadcsound,
setcps: id,
Clock: {}, // whatever
// Tone,
},
-17
View File
@@ -1,17 +0,0 @@
import { queryCode } from '../runtime.mjs';
import { describe, it } from 'vitest';
import data from './dbdump.json';
describe('renders shared tunes', async () => {
data.forEach(({ id, code, hash }) => {
const url = `https://strudel.cc/?${hash}`;
it(`shared tune ${id} ${url}`, async ({ expect }) => {
if (code.includes('import(')) {
console.log('skip', url);
return;
}
const haps = await queryCode(code, 1);
expect(haps).toMatchSnapshot();
});
});
});
+10
View File
@@ -0,0 +1,10 @@
# dbpatch
this is a little script to update all patterns in the db. Go to supabase and export as csv as `code_rows.csv` to this folder.
Then run
```sh
node dbpatch.mjs > code_rows_patched.csv
```
It will output a csv file with the changes implemented in dbpatch.mjs
+31
View File
@@ -0,0 +1,31 @@
import { parse } from 'csv-parse/sync';
import { readFileSync } from 'fs';
import { stringify } from 'csv-stringify/sync';
const hasCpsCall = (code) =>
['setcps', 'setCps', 'setCpm', 'setcpm'].reduce((acc, m) => acc || code.includes(`${m}`), false);
function withCps(code, cps) {
if (hasCpsCall(code)) {
return code;
}
const lines = code.split('\n');
const firstNonLineComment = lines.findIndex((l) => !l.startsWith('//'));
const cpsCall = `setcps(${cps})`;
lines.splice(firstNonLineComment, 0, cpsCall);
return lines.join('\n');
}
const dumpNew = readFileSync('./code_rows.csv', { encoding: 'utf-8' });
const records = parse(dumpNew, {
columns: true,
skip_empty_lines: true,
});
const edited = records.map((entry) => ({
...entry,
code: withCps(entry.code, 1),
}));
console.log(stringify(edited));
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"csv": "^6.3.6"
}
}
+7
View File
@@ -262,6 +262,13 @@ declare module 'astro:content' {
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"year-2.mdx": {
id: "year-2.mdx";
slug: "year-2";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
};
};
+6 -5
View File
@@ -13,9 +13,10 @@
},
"dependencies": {
"@algolia/client-search": "^4.22.0",
"@astro-community/astro-embed-youtube": "^0.4.3",
"@astro-community/astro-embed-youtube": "^0.4.4",
"@astrojs/mdx": "^2.0.3",
"@astrojs/react": "^3.0.9",
"@astrojs/rss": "^4.0.2",
"@astrojs/tailwind": "^5.1.0",
"@docsearch/css": "^3.5.2",
"@docsearch/react": "^3.5.2",
@@ -23,21 +24,21 @@
"@heroicons/react": "^2.1.1",
"@nanostores/persistent": "^0.9.1",
"@nanostores/react": "^0.7.1",
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/csound": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/repl": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/xen": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/repl": "workspace:*",
"@supabase/supabase-js": "^2.39.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+4 -3
View File
@@ -1,7 +1,7 @@
---
import type { CollectionEntry } from 'astro:content';
type Props = CollectionEntry<'blog'>['data'];
type Props = { post: CollectionEntry<'blog'> };
const { post } = Astro.props;
const { Content } = await post.render();
@@ -9,11 +9,12 @@ import { format } from 'date-fns';
---
<article
class="prose max-w-none prose-headings:font-sans prose-headings:font-black prose-headings:text-slate-900 dark:prose-headings:text-gray-200 dark:text-gray-400 dark:prose-strong:text-gray-400 dark:prose-code:text-slate-400 dark:prose-a:text-gray-300 prose-a:text-slate-900 prose-blockquote:text-slate-800 dark:prose-blockquote:text-slate-400"
class="prose max-w-none prose-headings:font-sans prose-headings:font-black prose-headings:text-slate-900 dark:prose-headings:text-gray-200 dark:text-gray-400 dark:prose-strong:text-gray-400 dark:prose-code:text-slate-400 dark:prose-a:text-gray-300 prose-a:text-slate-900 prose-blockquote:text-slate-800 dark:prose-blockquote:text-slate-400 border-b-4 border-lineHighlight pt-4"
>
<div class="pb-2">
<div class="md:flex justify-between">
<h1 class="mb-4" id={post.slug}>{post.data.title}</h1>
<h1 class="mb-4">{post.data.title}</h1>
<span id={post.slug} class="-mt-16"></span>
</div>
<p class="italic p-0 m-0">
<time datetime={post.data.date.toISOString()}>
+2 -2
View File
@@ -10,7 +10,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<link rel="alternate" type="application/rss+xml" title={`RSS Feed for strudel.cc`} href="/rss.xml" />
<link rel="icon" type="image/svg+xml" href={`${baseNoTrailing}/favicon.ico`} />
<meta
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<base href={BASE_URL} />
<!-- Scrollable a11y code helper -->
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script {`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: -->
+1 -9
View File
@@ -1,5 +1,5 @@
---
import { SITE, OPEN_GRAPH, Frontmatter } from '../config';
import { SITE, OPEN_GRAPH, type Frontmatter } from '../config';
export interface Props {
frontmatter: Frontmatter;
@@ -25,11 +25,3 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
<meta property="og:image:alt" content={imageAlt} />
<meta name="description" property="og:description" content={frontmatter.description ?? SITE.description} />
<meta property="og:site_name" content={SITE.title} />
<!-- Twitter Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={OPEN_GRAPH.twitter} />
<meta name="twitter:title" content={formattedContentTitle} />
<meta name="twitter:description" content={frontmatter.description ?? SITE.description} />
<meta name="twitter:image" content={canonicalImageSrc} />
<meta name="twitter:image:alt" content={imageAlt} />
+47
View File
@@ -0,0 +1,47 @@
import { useState, useEffect } from 'react';
import { loadFeaturedPatterns, loadPublicPatterns } from '@src/user_pattern_utils.mjs';
import { MiniRepl } from '@src/docs/MiniRepl';
import { PatternLabel } from '@src/repl/panel/PatternsTab';
function PatternList({ patterns }) {
return (
<div className="space-y-4">
{/* <MiniRepl tunes={patterns.map((pat) => pat.code.trim())} /> */}
{patterns.map((pat) => (
<div key={pat.id}>
<div className="flex justify-between not-prose pb-2">
<h2 className="text-lg">
<a href={`/?${pat.hash}`} target="_blank" className="underline">
<PatternLabel pattern={pat} />
</a>
</h2>
</div>
<MiniRepl tune={pat.code.trim()} maxHeight={300} />
</div>
))}
</div>
);
}
export function Oven() {
const [featuredPatterns, setFeaturedPatterns] = useState([]);
const [publicPatterns, setPublicPatterns] = useState([]);
useEffect(() => {
loadPublicPatterns().then(({ data: pats }) => {
console.log('pats', pats);
setPublicPatterns(pats);
});
loadFeaturedPatterns().then(({ data: pats }) => {
console.log('pats', pats);
setFeaturedPatterns(pats);
});
}, []);
return (
<div>
<h2 id="featured">Featured Patterns</h2>
<PatternList patterns={featuredPatterns} />
<h2 id="latest">Last Creations</h2>
<PatternList patterns={publicPatterns} />
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
export function Youtube(props) {
return <LiteYouTubeEmbed {...props} />;
}
+1
View File
@@ -58,6 +58,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'What is Strudel?', link: 'workshop/getting-started' },
{ text: 'Showcase', link: 'intro/showcase' },
{ text: 'Blog', link: 'blog' },
{ text: 'Community Bakery', link: 'bakery' },
],
Workshop: [
// { text: 'Getting Started', link: 'workshop/getting-started' },
@@ -1,5 +1,5 @@
---
title: 'Release Notes v0.7.0 Zimtschnecke'
title: 'Release Notes v0.7.0 Zuckerguss'
description: ''
date: '2023-03-23'
tags: ['meta']
+269
View File
@@ -0,0 +1,269 @@
---
title: '2 Years of Strudel'
description: ''
date: '2024-01-22'
tags: ['meta']
author: froos
---
Strudel is now 2 years old! This was the first commit to the strudel repo:
```log
commit 38b5a0d5cdf28685b2b5e18d460772b70246207b
Author: Alex McLean <alex@slab.org>
Date: Sat Jan 22 20:24:36 2022 +0000
Initial commit
```
I have already written these 2 posts about the first year of strudel:
- [Introducing Strudel](https://loophole-letters.vercel.app/strudel)
- [1 Year of Strudel](https://loophole-letters.vercel.app/strudel1year)
To continue that tradition, let me recap what happened around Strudel in 2023, at least what I have noticed:
## ICLC 2023
I presented strudel at the [International Conference on Live Coding (ICLC)](https://iclc.toplap.org/2023/), which took place in April in Utrecht.
Here is the (pretty technical) video of my presentation:
import { Youtube } from '@src/components/Youtube';
<Youtube client:only="react" id="ZzDSW08IAdU" params="start=2805" />
For the conference, I wrote a paper with Alex McLean, called [Strudel: Live Coding Patterns on the Web](https://zenodo.org/records/7842142).
For me personally, this was my first major contact with the live coding scene and it was a very memorable experience!
## Workshops and Courses
A lot of workshops have been using strudel in the last year, from many parts of the world:
[Iceland](http://www.raflost.is/live-coding-for-beginners/), [UK](https://gutlevel.co.uk/whatson/patternclub8),
[Japan](https://yoppa.org/kanazawa23), [France](https://www.stereolux.org/agenda/workshop-de-decouverte-de-la-musique-algorithmique-avec-strudel-avec-camille-amet) and the [US](https://www.nycresistor.com/2023/09/25/making-music-with-code-9-30-2023/) (probably more I've missed).
I've also given workshops in [Switzerland](https://basel.codes/2023/index.html) and [Germany](https://www.cityofmediaarts.de/de/events/strudel-live-coding-workshop/), where we even had real strudel, thanks to [TOPLAP Karlsruhe](https://toplap-ka.de/)!
![strudel workshop space](/img/workshop-space.png)
Additionally, there is now a [Creative Coding](https://www.chalmers.se/en/collaborate-with-us/activities-for-schools/creative-coding/) course at Chalmers in Sweden, consisting of "a 10-week series of workshops [...] for younger girls to have fun and experiment with code and music".
I was very delighted to see this video of the girls having fun with Strudel:
<Youtube client:only="react" id="4zgHeNpG4wU" />
The course was also encompassed by a study on female empowerement through Creative Coding, read more in [Jasmina Maric's paper "Singing Code"](https://www.academia.edu/106106710/Singing_Code).
Another bigger installment was the [(Algo|Afro) Futures programme](https://algo-afro-futures.lurk.org/) led by Antonio Roberts, which is "a mentoring programme for early career Black artists who want to explore creative coding and live coding".
For the final concert they recorded this amazing live stream:
<Youtube client:only="react" id="zUoZvkZ3J7Q" params="start=842" />
I am so happy to see that the project I've co-created has had such an impact already, making live coding workshops more easy and accessible.
In my opinion, the reasons why people seem to choose strudel for teaching are:
- it does not require an installation, so it even runs on your phone / tablet or a chromebook
- in-code and behind-code visualizations help learners to understand what the system is doing
- the interactive documentation
- the simple javascript syntax, which is very similar to [hydra](https://hydra.ojack.xyz)
- tidal's mini notation for playful expression of rhythms (based on the [Bol Processor](https://bolprocessor.org/)'s polymetric expressions)
- its range of functions for transforming patterns for those getting deeper into exploring strange rhythms
## Performances
![strudel performance](/img/strudel-live-coding-mars-college.jpg)
People are starting to use Strudel for performances, many of which can be watched in the new [Showcase](/intro/showcase/)!
The above photo comes from the [Mars College Department of Future Music](https://mars.college/gallery/live_coding/)
## New Features
Let's talk about some of the major features that landed since last year.
### Audio Engine Improved
The audio engine has gotten a lot of attention! Once integrated into Strudel itself, the engine has been pulled out and named [superdough](https://www.npmjs.com/package/superdough),
the name being based on [SuperDirt](https://github.com/musikinformatik/SuperDirt), which is the audio engine of Tidal.
Superdough is now also used by [topos](https://topos.live/), a creation of [Raphaël Forment](https://github.com/Bubobubobubobubo/), who helped me implement a bunch of new features.
Lately, [Jade Rowland](https://github.com/daslyfe) has also developed many synthesis features into superdough! Here is a non-exhaustive list of new features:
- [filter envelopes](https://strudel.cc/learn/effects/#filter-envelope) + [24db filter option](https://strudel.cc/learn/effects/#ftype)
- [pitch envelopes](https://strudel.cc/learn/effects/#pitch-envelope)
- [fm synthesis](https://strudel.cc/learn/synths/#fm-synthesis)
- [wavetable synthesis](https://strudel.cc/recipes/recipes/#wavetable-synthesis)
- [phaser](https://strudel.cc/learn/effects/#phaser)
- [vibrato](https://strudel.cc/learn/synths/#vibrato)
- [noise](https://strudel.cc/learn/synths/#noise)
- [an integration of the zzfx synth](https://strudel.cc/learn/synths/#zzfx)
- [compressor](https://strudel.cc/learn/effects/#compressor)
- multichannel audio / audio device selection
import { MiniRepl } from '@src/docs/MiniRepl';
Here's an example that shows off filter envelopes:
<MiniRepl
client:visible
tune={`// "acidic tooth" @by eddyflux
setcps(1)
stack(
note("[<g1 f1>/8](<3 5>,8)")
.clip(perlin.range(.15,1.5))
.release(.1)
.s("sawtooth")
.lpf(sine.range(400,800).slow(16))
.lpq(cosine.range(6,14).slow(3))
.lpenv(sine.mul(4).slow(4))
.lpd(.2).lpa(.02)
.ftype('24db')
.rarely(add(note(12)))
.room(.2).shape(.3).postgain(.5)
.superimpose(x=>x.add(note(12)).delay(.5).bpf(1000))
.gain("[.2 1@3]*2") // fake sidechain
,
stack(
s("bd*2").mask("<0@4 1@16>"),
s("hh*8").gain(saw.mul(saw.fast(2))).clip(sine)
.mask("<0@8 1@16>")
).bank('RolandTR909')
)
`}
/>
### Import Sounds
In the "sounds" tab, you can now press "import sounds" to select sounds from your local disk, allowing you
to use your own samples without the need to upload them! Keep in mind that if you share a pattern that uses local samples that the samples will fail to load on other people's machines..
### Community Bakery / Patterns Tab
You can now share your patterns publicly to the [Community Bakery](/bakery/), via the "Share" button!
The Community Patterns are also listed in the new "patterns", where you can also save your own patterns locally!
### Quality of Life Improvements
You can now change the theme and font of the editor:
![strudel themes](/img/strudel-themes.png)
You can now enable autocompletion in the settings:
![autocomplete](/img/autocomplete.png)
If you enable tooltips, you will get the same info by hovering the mouse over functions while holding the ctrl key!
Another handy thing is that Strudel now has offline support! Even samples will work once you have played them once. [More info](/learn/pwa/)
### New Visualizations
There is now a new `.spiral()` visualization that renders events in a spiral, elevating the cyclical nature of time (+ swirl also means Strudel in german).
![strudel alien ui](/img/strudel-alien-live-coding.png)
Ah yes and you see one of the less readable fonts in this screenshot too :)
The `.scope()` function renders you an oscilloscope:
![scope](/img/strudel-scope.png)
The pianoroll also got some more options, including the ability to set labels:
![scope](/img/strudel-monkeys.png)
### Sliders
The `slider` is the first of a family of functions that render UI widgets inline:
<MiniRepl
client:visible
tune={`note("<c eb g bb>*8")
.s("sawtooth").lpenv(2).lpq(8)
.lpf(slider(500, 100, 2000, 1))`}
/>
Sliders are especially useful for gradual changes like fading things in and out by hand,
which is not very practical with just code. The fact that the slider is inside the code and also changes the code itself,
makes it feel connected to the rest of the system + doesn't distract or obstruct from the code.
Other widgets will come for sure.
### More Voicings
One of my personal goals that got me involved with live coding in the first place is to be able to generate good sounding chord voicings on the fly.
Strudel now supports most chord symbols:
<MiniRepl
client:visible
tune={`chord("<C^9 C7b9 Fm9 Db^7>/2")
.dict('ireal').voicing()
.s("sawtooth")
.lpf(400).lpa(.5).lpenv(4)
.phaser(4).room(.5)`}
/>
So far, this is only [documented briefly](https://strudel.cc/learn/tonal/#voicing), a more in-depth guide will follow!
### Multiplayer / Collaborative Coding
The collaborative live coding editor [flok](https://flok.cc) had strudel support early on, thanks to [Damián Silvani](https://github.com/munshkr).
Later, I've added [support for multiple strudel instances](https://github.com/munshkr/flok/pull/181), which was merged to the official version [just a few days ago](https://social.toplap.org/@munshkr/111748783047032711).
This photo shows a flok jam I did with my workshop participants in basel:
![collaborative coding](/img/strudel-collaborative-coding.png)
The cool thing about flok is that you can jam with people on the other side of the planet!
Here is a recording of the first session we organized over the discord server:
<Youtube client:only="react" id="p0J7XrT9JEs" params="start=2634" />
### Better MIDI integration
The midi integration has gotten a few new features:
- [clock out](https://github.com/tidalcycles/strudel/pull/710) to sync your midi devices / DAW to the strudel clock (better doc coming soon)
- [cc output](https://strudel.cc/learn/input-output/#ccn--ccv) to send cc values to your gear
- [cc input](https://github.com/tidalcycles/strudel/pull/699) to control strudel via MIDI (better doc coming soon)
Here is a little demo of me fiddling with a midi controller, changing a piano pattern:
<Youtube client:only="react" id="e2-Sv_jjDQk" />
### Experimental Features
- You can now run [hydra inside strudel](https://strudel.cc/learn/hydra/) + you can even run strudel in [hydra](https://hydra.ojack.xyz), thanks to [Olivia Jack](https://ojack.xyz/) and [Ámbar Tenorio Fornés](https://atenor.io/)! Read more [here](https://alpaca.pubpub.org/pub/b7hwrjfk/release/2?readingCollection=1def0192)
- There is now a [VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel) thanks to [roipoussiere](https://github.com/roipoussiere)! It allows you run patterns from a `.strudel` file inside VSCode.
- Strudel can now be downloaded as a desktop app, thanks to [vasilymilovidov](https://github.com/vasilymilovidov) who has wrapped the REPL with [tauri](https://tauri.app/). You can download it [on the releases page](https://github.com/tidalcycles/strudel/releases) (scroll down to Assets). The performance is not optimal on MacOS and Linux, which is why it is still considered experimental
## Stats
Let's end the recap with some naked numbers:
### Releases
- 0.0.2 Schwindlig
- 0.1.0 Stürmisch
- 0.0.3 Maelstrom - state of last strudel post
- 0.0.4 Gischt
- 0.3.0 Donauwelle
- 0.4.0 Brandung
- 0.5.0 Wirbel
- 0.6.0 Zimtschnecke - state after 1 year
- 0.7.0 Zuckerguss
- 0.8.0 Himbeermuffin
- 0.9.0 Bananenbrot
- 0.10.0 Kuchenboden
- 0.11.0 Kuchengabel
- 1.0.0 Geburtstagskuchen - now!
### Repo Stats
- 3399 commits (+1521)
- 427 stars (+243)
- 90 forks (+45)
- 163 open issues (+110), 186 closed (+100)
- 20 open PRs (+18), 515 closed (+292)
- 32 contributors (+17)
- 12 releases (+4)
## Thank you 💗
A big thanks to everyone contributing to this project in one way or another,
be it by writing code, fixing typos, asking questions or making sounds!
I am very grateful for how all of this came together and I'm looking forward to year 3!
If you have some spare cash to support the tidal project, we'd be happy if you can [insert some coins here](https://opencollective.com/tidalcycles).
+22
View File
@@ -1,4 +1,19 @@
export function Icon({ type }) {
if (type === 'skip') {
// !Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.
return (
<svg
fillRule="evenodd"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
height="16"
width="10"
viewBox="0 0 320 512"
>
<path d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z" />
</svg>
);
}
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
{
@@ -31,6 +46,13 @@ export function Icon({ type }) {
clipRule="evenodd"
/>
),
skip: (
<path
fillRule="evenodd"
d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
+40 -6
View File
@@ -1,8 +1,8 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon';
import { silence, getPunchcardPainter, noteToMidi } from '@strudel/core';
import { silence, getPunchcardPainter, noteToMidi, _mod } from '@strudel/core';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { StrudelMirror } from '@strudel/codemirror';
// import { prebake } from '@strudel/repl';
import { prebake } from '../repl/prebake.mjs';
@@ -10,14 +10,16 @@ import { loadModules } from '../repl/util.mjs';
import Claviature from '@components/Claviature';
import useClient from '@src/useClient.mjs';
let prebaked, modulesLoading;
let prebaked, modulesLoading, audioLoading;
if (typeof window !== 'undefined') {
prebaked = prebake();
modulesLoading = loadModules();
audioLoading = initAudioOnFirstClick();
}
export function MiniRepl({
tune: code,
tune,
tunes,
hideHeader = false,
canvasHeight = 100,
onTrigger,
@@ -25,7 +27,9 @@ export function MiniRepl({
punchcardLabels = true,
claviature,
claviatureLabels,
maxHeight,
}) {
const code = tunes ? tunes[0] : tune;
const id = useMemo(() => s4(), []);
const canvasId = useMemo(() => `canvas-${id}`, [id]);
const shouldDraw = !!punchcard || !!claviature;
@@ -75,7 +79,7 @@ export function MiniRepl({
}
return pat;
},
prebake: async () => Promise.all([modulesLoading, prebaked]),
prebake: async () => Promise.all([modulesLoading, prebaked, audioLoading]),
onUpdateState: (state) => {
setReplState({ ...state });
},
@@ -91,6 +95,14 @@ export function MiniRepl({
const containerRef = useRef();
const client = useClient();
const [tuneIndex, setTuneIndex] = useState(0);
const changeTune = (index) => {
index = _mod(index, tunes.length);
setTuneIndex(index);
editorRef.current?.setCode(tunes[index]);
editorRef.current?.evaluate();
};
if (!client) {
return <pre>{code}</pre>;
}
@@ -119,9 +131,31 @@ export function MiniRepl({
<Icon type="refresh" />
</button>
</div>
{tunes && (
<div className="flex">
<button
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
onClick={() => changeTune(tuneIndex - 1)}
>
<div className="rotate-180">
<Icon type="skip" />
</div>
</button>
<button
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
onClick={() => changeTune(tuneIndex + 1)}
>
<Icon type="skip" />
</button>
</div>
)}
</div>
)}
<div className="overflow-auto relative p-1">
<div className="overflow-auto relative p-1" style={maxHeight ? { maxHeight: `${maxHeight}px` } : {}}>
<div
ref={(el) => {
if (!editorRef.current) {
+81
View File
@@ -0,0 +1,81 @@
export const examples = [
`// "coastline" @by eddyflux
await samples('github:eddyflux/crate')
setcps(.75)
let chords = chord("<Bbm9 Fm9>/4").dict('ireal')
stack(
stack( // DRUMS
s("bd").struct("<[x*<1 2> [~@3 x]] x>"),
s("~ [rim, sd:<2 3>]").room("<0 .2>"),
n("[0 <1 3>]*<2!3 4>").s("hh"),
s("rd:<1!3 2>*2").mask("<0 0 1 1>/16").gain(.5)
).bank('crate')
.mask("<[0 1] 1 1 1>/16".early(.5))
, // CHORDS
chords.offset(-1).voicing().s("gm_epiano1:1")
.phaser(4).room(.5)
, // MELODY
n("<0!3 1*2>").set(chords).mode("root:g2")
.voicing().s("gm_acoustic_bass"),
chords.n("[0 <4 3 <2 5>>*2](<3 5>,8)")
.set(x).anchor("D5").voicing()
.segment(4).clip(rand.range(.4,.8))
.room(.75).shape(.3).delay(.25)
.fm(sine.range(3,8).slow(8))
.lpf(sine.range(500,1000).slow(8)).lpq(5)
.rarely(ply("2")).chunk(4, fast(2))
.gain(perlin.range(.6, .9))
.mask("<0 1 1 0>/16")
)
.late("[0 .01]*4").late("[0 .01]*2").size(4)`,
`// "broken cut 1" @by froos
await samples('github:tidalcycles/Dirt-Samples/master')
samples({
'slap': 'https://cdn.freesound.org/previews/495/495416_10350281-lq.mp3',
'whirl': 'https://cdn.freesound.org/previews/495/495313_10350281-lq.mp3',
'attack': 'https://cdn.freesound.org/previews/494/494947_10350281-lq.mp3'
})
setcps(1.25)
note("[c2 ~](3,8)*2,eb,g,bb,d").s("sawtooth")
.noise(0.3)
.lpf(perlin.range(800,2000).mul(0.6))
.lpenv(perlin.range(1,5)).lpa(.25).lpd(.1).lps(0)
.add.mix(note("<0!3 [1 <4!3 12>]>")).late(.5)
.vib("4:.2")
.room(1).roomsize(4).slow(4)
.stack(
s("bd").late("<0.01 .251>"),
s("breaks165:1/2").fit()
.chop(4).sometimesBy(.4, ply("2"))
.sometimesBy(.1, ply("4")).release(.01)
.gain(1.5).sometimes(mul(speed("1.05"))).cut(1)
,
s("<whirl attack>?").delay(".8:.1:.8").room(2).slow(8).cut(2),
).reset("<x@30 [x*[8 [8 [16 32]]]]@2>".late(2))`,
`// "acidic tooth" @by eddyflux
setcps(1)
stack(
note("[<g1 f1>/8](<3 5>,8)")
.clip(perlin.range(.15,1.5))
.release(.1)
.s("sawtooth")
.lpf(sine.range(400,800).slow(16))
.lpq(cosine.range(6,14).slow(3))
.lpenv(sine.mul(4).slow(4))
.lpd(.2).lpa(.02)
.ftype('24db')
.rarely(add(note(12)))
.room(.2).shape(.3).postgain(.5)
.superimpose(x=>x.add(note(12)).delay(.5).bpf(1000))
.gain("[.2 1@3]*2") // fake sidechain
,
stack(
s("bd*2").mask("<0@4 1@16>"),
s("hh*8").gain(saw.mul(saw.fast(2))).clip(sine)
.mask("<0@8 1@16>")
).bank('RolandTR909')
)`,
];
+60
View File
@@ -0,0 +1,60 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import Header from '../components/Header/Header.astro';
import LeftSidebar from '../components/LeftSidebar/LeftSidebar.astro';
import PageContent from '../components/PageContent/PageContent.astro';
import { getCollection } from 'astro:content';
import { compareDesc } from 'date-fns';
import { Oven as CommunityOven } from '../components/Oven/Oven.jsx';
import RightSidebar from '../components/RightSidebar/RightSidebar.astro';
const currentPage = Astro.url.pathname;
const posts = (await getCollection('blog')).sort((a, b) => compareDesc(a.data.date, b.data.date));
---
<html dir={'ltr'} lang={'en'} class="initial dark">
<head>
<HeadCommon />
<title>🌀 Strudel Community Bakery</title>
</head>
<body class="h-app-height text-gray-50 bg-background">
<div class="w-full h-full space-y-4 flex flex-col">
<header class="max-w-full fixed top-0 w-full z-[100]">
<Header currentPage={currentPage} />
</header>
<main class="relative pt-16">
<div class="h-full top-0 overflow-auto min-w-[300px] flex xl:justify-center pr-4 pl-4 md:pl-[300px] xl:pl-0">
<aside title="Site Navigation" class="w-[300px] px-6 left-0 hidden md:block fixed h-full">
<LeftSidebar currentPage={currentPage} />
</aside>
<PageContent>
<h1>Community Bakery</h1>
<p>
This page contains all the strudel patterns baked by the community. Add your own by clicking the "Share"
button in the REPL. Have fun, and please share some of what you create with the community.
</p>
<CommunityOven client:only />
</PageContent>
<aside class="fixed right-0 h-full overflow-auto pr-4 pl-0 pb-16 hidden xl:block" title="Table of Contents">
<RightSidebar
headings={[
{
depth: 1,
slug: 'featured',
text: 'Featured',
},
{
depth: 1,
slug: 'latest',
text: 'Last Creations',
},
]}
/>
</aside>
</div>
</main>
</div>
</body>
</html>
+13 -2
View File
@@ -8,7 +8,7 @@ import PageContent from '../components/PageContent/PageContent.astro';
import RightSidebar from '../components/RightSidebar/RightSidebar.astro';
import { getCollection } from 'astro:content';
import { compareDesc } from 'date-fns';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
const currentPage = Astro.url.pathname;
const posts = (await getCollection('blog')).sort((a, b) => compareDesc(a.data.date, b.data.date));
@@ -32,7 +32,18 @@ const posts = (await getCollection('blog')).sort((a, b) => compareDesc(a.data.da
<LeftSidebar currentPage={currentPage} />
</aside>
<PageContent>
{posts.map((post) => <BlogPost post={post} />)}
<div class="border-b-4 border-lineHighlight py-4">
<h1>Strudel Blog</h1>
<p>
Welcome to the Strudel Blog, where we will keep you updated with the latest changes and things happening
in the strudelsphere. You can subscribe to this blog using <a target="_blank" href="/rss.xml"
>this rss link</a
>
</p>
</div>
<div class="space-y-8">
{posts.map((post) => <BlogPost post={post} />)}
</div>
</PageContent>
<aside class="fixed right-0 h-full overflow-auto pr-4 pl-0 pb-16 hidden xl:block" title="Table of Contents">
<RightSidebar
+1 -1
View File
@@ -33,7 +33,7 @@ While the Mini Notation is a powerful way to write rhythms concisely, it also ha
client:idle
tune={`stack(
note("c2 eb2(3,8)").s('sawtooth').cutoff(800),
s("bd,~ sd,hh*4")
s("bd(5,8), hh*8")
)`}
/>
@@ -28,6 +28,10 @@ import { JsDoc } from '../../docs/JsDoc';
<JsDoc client:idle name="Pattern.chunkBack" h={0} />
### fastChunk
<JsDoc client:idle name="Pattern.fastChunk" h={0} />
## arp
<JsDoc client:idle name="Pattern#arp" h={0} />
+2 -2
View File
@@ -150,7 +150,7 @@ Pitch envelopes can breathe life into static sounds:
.s("gm_electric_guitar_jazz")
.penv("<.5 0 7 -2>*2").vib("4:.1")
.phaser(2).delay(.25).room(.3)
.size(4).fast(.75)`}
.size(4).fast(1.5)`}
/>
You also create some lovely chiptune-style sounds:
@@ -163,7 +163,7 @@ You also create some lovely chiptune-style sounds:
.voicing().add(note("<0 1>/8"))
.dec(.1).room(.2)
.segment("<4 [2 8]>")
.penv("<0 <2 -2>>").patt(.02)`}
.penv("<0 <2 -2>>").patt(.02).fast(2)`}
/>
Let's break down all pitch envelope controls:
+3 -2
View File
@@ -36,7 +36,8 @@ osc(1, -0.9, 300)
.scale(2)
.out()
note("[a,c,e,<a4 ab4 g4 gb4>,b4]/4").s("sawtooth").vib(2)
note("[a,c,e,<a4 ab4 g4 gb4>,b4]/2")
.s("sawtooth").vib(2)
.lpf(600).lpa(2).lpenv(6)
`}
/>
@@ -90,7 +91,7 @@ src(s0).kaleid(H("<4 5 6>"))
.out()
//
stack(
s("bd*2,[hh:0:<.5 1>]*4,~ rim").bank("RolandTR909").speed(.9),
s("bd*4,[hh:0:<.5 1>]*8,~ rim").bank("RolandTR909").speed(.9),
note("[<g1!3 <bb1 <f1 d1>>>]*3").s("sawtooth")
.room(.75).sometimes(add(note(12))).clip(.3)
.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
+46 -43
View File
@@ -22,27 +22,25 @@ Before diving deeper into the details, here is a flavour of how the Mini-Notatio
<MiniRepl
client:idle
tune={`note(\`[
[
[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]
],[
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]
]
]/16\`)`}
tune={`note(\`<
[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]
,
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]
>\`)`}
/>
## Mini Notation Format
@@ -74,9 +72,21 @@ Taking the two examples above, we have four and eight events respectively, and s
This is perhaps counter-intuitive if you are used to adding notes in a sequencer or piano roll and the overall length increasing.
But, it will begin to make sense as we go through more elements of mini-notation.
## Multiplication
A sequence can be sped up by multiplying it by a number using the asterisk symbol (`*`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} punchcard />
The multiplication by two here means that the sequence will play twice per cycle.
Multiplications can also be decimal (`*2.75`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} punchcard />
## Division
We can slow the sequence down by enclosing it in brackets and dividing it by a number (`/2`):
Contrary to multiplication, division can slow the sequence down by enclosing it in brackets and dividing it by a number (`/2`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2")`} punchcard />
@@ -102,18 +112,11 @@ The advantage of the angle brackets, is that we can add more events without need
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5 b4>")`} punchcard />
This is more similar to traditional music sequencers and piano rolls, where adding a note increases the perceived overall duration.
We can also play a certain number of notes per cycle by using angle brackets with multiplication:
## Multiplication
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 a4 c5>*8")`} punchcard />
Contrary to division, a sequence can be sped up by multiplying it by a number using the asterisk symbol (`*`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} punchcard />
The multiplication by two here means that the sequence will play twice per cycle.
As with divisions, multiplications can be decimal (`*2.75`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} punchcard />
Now we are playing 8 notes per cycle!
## Subdividing time with bracket nesting
@@ -149,13 +152,13 @@ The following are the same:
But to play multiple chords in a sequence, we have to wrap them in brackets:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} punchcard />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>*2")`} punchcard />
## Elongation
With the "@" symbol, we can specify temporal "weight" of a sequence child:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
@@ -163,19 +166,19 @@ Here, the first chord has a weight of 2, making it twice the length of the other
Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
## Mini-notation review
To recap what we've learned so far, compare the following patterns:
<MiniRepl client:idle tune={`note("<g3 b3 e4 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]/2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]*2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<g3 b3 e4 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]/2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]*2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
## Euclidian rhythms
@@ -196,7 +199,7 @@ But using the Euclidian rhythm notation, we only need to express "3 beats over 8
This makes it easy to write patterns with interesting rhythmic structures and variations that still sound familiar:
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} punchcard canvasHeight={50} />
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(2)`} punchcard canvasHeight={50} />
Note that since the example above does not use the third `offset` parameter, it can be written simply as `"(3,8)"`.
+24 -20
View File
@@ -18,7 +18,7 @@ Strudel allows loading samples in the form of audio files of various formats (wa
By default, strudel comes with a built-in "sample map", providing a solid base to play with.
<MiniRepl client:idle tune={`s("bd sd,hh*8, misc/2")`} />
<MiniRepl client:idle tune={`s("bd sd [~ bd] sd,hh*16, misc")`} />
Here, we are using the `s` function to play back different default samples (`bd`, `sd`, `hh` and `misc`) to get a drum beat.
@@ -59,15 +59,15 @@ If we open the `sounds` tab and then `drum machines`, we can see that the drum s
We _could_ use them like this:
<MiniRepl client:idle tune={`s("RolandTR808_bd RolandTR808_sd,RolandTR808_hh*8")`} />
<MiniRepl client:idle tune={`s("RolandTR808_bd RolandTR808_sd,RolandTR808_hh*16")`} />
... but thats obviously a bit much to write. Using the `bank` function, we can shorten this to:
<MiniRepl client:idle tune={`s("bd sd,hh*8").bank("RolandTR808")`} />
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808")`} />
You could even pattern the bank to switch between different drum machines:
<MiniRepl client:idle tune={`s("bd sd,hh*8").bank("<RolandTR808 RolandTR909>")`} />
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808 RolandTR909")`} />
Behind the scenes, `bank` will just prepend the drum machine name to the sample name with `_` to get the full name.
This of course only works because the name after `_` (`bd`, `sd` etc..) is standardized.
@@ -79,17 +79,21 @@ If we open the `sounds` tab again, followed by tab `drum machines`, there is als
For example `RolandTR909_hh(4)` means there are 4 samples of a TR909 hihat available.
By default, `s` will play the first sample, but we can select the other ones using `n`, starting from 0:
<MiniRepl client:idle tune={`s("hh*4").bank("RolandTR909").n("<0 1 2 3>")`} />
<MiniRepl client:idle tune={`s("hh*8").bank("RolandTR909").n("0 1 2 3")`} />
Numbers that are too high will just wrap around to the beginning
<MiniRepl client:idle tune={`s("hh*4").bank("RolandTR909").n("<0 1 2 3 4 5 6 7>")`} />
<MiniRepl client:idle tune={`s("hh*8").bank("RolandTR909").n("0 1 2 3 4 5 6 7")`} />
Here, 0-3 will play the same sounds as 4-7, because `RolandTR909_hh` only has 4 sounds.
Selecting sounds also works inside the mini notation, using "`:`" like this:
<MiniRepl client:idle tune={`s("bd:1 bd:2,hh:0 hh:1 hh:2 hh:3").bank("RolandTR909")`} />
<MiniRepl
client:idle
tune={`s("bd*4,hh:0 hh:1 hh:2 hh:3 hh:4 hh:5 hh:6 hh:7")
.bank("RolandTR909")`}
/>
# Loading Custom Samples
@@ -103,7 +107,7 @@ In this example we create a map using sounds from the default sample map:
sd: 'sd/rytm-01-classic.wav',
hh: 'hh27/000_hh27closedhh.wav',
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
s("bd sd,hh*8")`}
s("bd sd bd sd,hh*16")`}
/>
When you load your own samples, you can choose the names that you will then refer to in your pattern string inside the `s` function.
@@ -116,7 +120,7 @@ Compare with this example which uses the same samples, but with different names.
snaredrum: 'sd/rytm-01-classic.wav',
hihat: 'hh27/000_hh27closedhh.wav',
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
s("bassdrum snaredrum, hihat*8")`}
s("bassdrum snaredrum bassdrum snaredrum, hihat*16")`}
/>
Here we have changed the "map" to include longer sample names.
@@ -140,7 +144,7 @@ Because GitHub is a popular place for uploading open source samples, it has its
sd: 'sd/rytm-01-classic.wav',
hh: 'hh27/000_hh27closedhh.wav',
}, 'github:tidalcycles/Dirt-Samples/master/');
s("bd sd,hh*8")`}
s("bd sd bd sd,hh*16")`}
/>
The format is `github:user/repo/branch/`.
@@ -157,7 +161,7 @@ We can see there are some guitar samples inside the `/samples` folder, so let's
g3: 'samples/guitar/guitar_3.wav',
g4: 'samples/guitar/guitar_4.wav'
}, 'github:jarmitage/jarmitage.github.io/master/');
s("[g0 g1 g2 g3 g4]/5")`}
s("<g0 g1 g2 g3 g4>/2")`}
/>
## Multiple Samples per Sound
@@ -171,7 +175,7 @@ It is also possible, to declare multiple files for one sound, using the array no
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
s("<bd:0 bd:1>,~ <sd:0 sd:1>,[hh:0 hh:1]*2")`}
s("bd:0 bd:1,~ <sd:0 sd:1> ~ sd:0,[hh:0 hh:1]*4")`}
/>
The `:0` `:1` etc. are the indices of the array.
@@ -184,7 +188,7 @@ The sample number can also be set using `n`:
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
s("bd,~ sd,hh*4").n("<0 1>")`}
s("bd bd,~ sd ~ sd,hh*8").n("<0 1>")`}
/>
In that case, we might load our guitar sample map a different way:
@@ -200,7 +204,7 @@ In that case, we might load our guitar sample map a different way:
'samples/guitar/guitar_4.wav'
]
}, 'github:jarmitage/jarmitage.github.io/master/');
s("[guitar:0 guitar:1 guitar:2 guitar:3 guitar:4]/5")`}
s("<guitar:0 guitar:1 guitar:2 guitar:3 guitar:4>*2")`}
/>
And as above, we can choose the sample number using `n` for even more flexibility:
@@ -216,7 +220,7 @@ And as above, we can choose the sample number using `n` for even more flexibilit
'samples/guitar/guitar_4.wav'
]
}, 'github:jarmitage/jarmitage.github.io/master/');
n("<0 1 2 3 4>").s("guitar")`}
n("<0 1 2 3 4>*2").s("guitar")`}
/>
## Pitched Sounds
@@ -271,7 +275,7 @@ We can also declare different samples for different regions of the keyboard:
}}, 'github:tidalcycles/Dirt-Samples/master/');
note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
.s('moog').clip(1)
.gain(.5)`}
.gain(.5).cpm(60)`}
/>
The sampler will always pick the closest matching sample for the current note!
@@ -285,9 +289,9 @@ With it, you can enter any sample name(s) to query from [freesound.org](https://
client:idle
tune={`await samples('shabda:bass:4,hihat:4,rimshot:2')
stack(
n("0 1 2 3").s('bass').slow(2),
n("0 1 2 3 0 1 2 3").s('bass'),
n("0 1*2 2 3*2").s('hihat'),
n("~ 0 ~ 1").s('rimshot')
n("~ 0 ~ 1 ~ 0 0 1").s('rimshot')
).clip(1)`}
/>
@@ -299,8 +303,8 @@ Note that the language code and the gender parameters are optional and default t
tune={`await samples('shabda/speech:the_drum,forever')
await samples('shabda/speech/fr-FR/m:magnifique')
stack(
s("the_drum").chop(16).speed(rand.range(0.85,1.1)),
s("forever magnifique").slow(8).late(0.25)
s("the_drum*2").chop(16).speed(rand.range(0.85,1.1)),
s("forever magnifique").slow(4).late(0.125)
)`}
/>
+8 -8
View File
@@ -16,7 +16,7 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("<sawtooth square triangle sine>")
.scope()`}
/>
@@ -28,7 +28,7 @@ If you don't set a `sound` but a `note` the default value for `sound` is `triang
You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different
flavours of noise, here written from hard to soft.
<MiniRepl client:idle tune={`sound("<white pink brown>/2").scope()`} />
<MiniRepl client:idle tune={`sound("<white pink brown>").scope()`} />
Here's a more musical example of how to use noise for hihats:
@@ -44,7 +44,7 @@ Some amount of pink noise can also be added to any oscillator by using the `nois
You can also use the `crackle` type to play some subtle noise crackles. You can control noise amount by using the `density` parameter:
<MiniRepl client:idle tune={`s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4)).scope()`} />
<MiniRepl client:idle tune={`s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(2)).scope()`} />
### Additive Synthesis
@@ -52,7 +52,7 @@ To tame the harsh sound of the basic waveforms, we can set the `n` control to li
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth")
.n("<32 16 8 4>")
.scope()`}
@@ -63,7 +63,7 @@ You can also set `n` directly in mini notation with `sound`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth:<32 16 8 4>")
.scope()`}
/>
@@ -125,7 +125,7 @@ note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9)
.s('wt_flute').velocity(0.25).often(n => n.ply(2))
.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0)
.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(2)`}
.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(4)`}
/>
## ZZFX
@@ -137,8 +137,8 @@ It has 20 parameters in total, here is a snippet that uses all:
<MiniRepl
client:idle
tune={`note("<c2 eb2 f2 g2>") // also supports freq
.s("<z_sawtooth z_tan z_noise z_sine z_square>")
tune={`note("c2 eb2 f2 g2") // also supports freq
.s("{z_sawtooth z_tan z_noise z_sine z_square}%4")
.zrand(0) // randomization
// zzfx envelope
.attack(0.001)
+29 -10
View File
@@ -14,6 +14,19 @@ These functions use [tonaljs](https://github.com/tonaljs/tonal) to provide helpe
<JsDoc client:idle name="voicing" h={0} />
Here's an example of how you can play chords and a bassline:
<MiniRepl
client:idle
tune={`chord("<C^7 A7b13 Dm7 G7>*2")
.dict('ireal').layer(
x=>x.struct("[~ x]*2").voicing()
,
x=>n("0*4").set(x).mode("root:g2").voicing()
.s('sawtooth').cutoff("800:4:2")
)`}
/>
### scale(name)
<JsDoc client:idle name="scale" h={0} />
@@ -22,13 +35,13 @@ These functions use [tonaljs](https://github.com/tonaljs/tonal) to provide helpe
Transposes all notes to the given number of semitones:
<MiniRepl client:only="react" tune={`"c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()`} />
<MiniRepl client:only="react" tune={`"[c2 c3]*4".transpose("<0 -2 5 3>").note()`} />
This method gets really exciting when we use it with a pattern as above.
Instead of numbers, scientific interval notation can be used as well:
<MiniRepl client:only="react" tune={`"c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()`} />
<MiniRepl client:only="react" tune={`"[c2 c3]*4".transpose("<1P -2M 4P 3m>").note()`} />
### scaleTranspose(steps)
@@ -36,9 +49,9 @@ Transposes notes inside the scale by the number of steps:
<MiniRepl
client:idle
tune={`"-8 [2,4,6]"
tune={`"[-8 [2,4,6]]*2"
.scale('C4 bebop major')
.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")
.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>*2")
.note()`}
/>
@@ -46,22 +59,28 @@ Transposes notes inside the scale by the number of steps:
Turns chord symbols into voicings, using the smoothest voice leading possible:
<MiniRepl client:only="react" tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()`} />
<MiniRepl
client:only="react"
tune={`stack(
"<C^7 A7 Dm7 G7>".voicings('lefthand'),
"<C3 A2 D3 G2>"
).note()`}
/>
Note: This function might be removed, as `voicing` (without s) is a newer implementation.
### rootNotes(octave = 2)
Turns chord symbols into root notes of chords in given octave.
<MiniRepl client:only="react" tune={`"<C^7 A7b13 Dm7 G7>".rootNotes(3).note()`} />
<MiniRepl client:only="react" tune={`"<C^7 A7b13 Dm7 G7>*2".rootNotes(3).note()`} />
Together with layer, struct and voicings, this can be used to create a basic backing track:
<MiniRepl
client:idle
tune={`"<C^7 A7b13 Dm7 G7>".layer(
x => x.voicings('lefthand').struct("~ x").note(),
tune={`"<C^7 A7b13 Dm7 G7>*2".layer(
x => x.voicings('lefthand').struct("[~ x]*2").note(),
x => x.rootNotes(2).note().s('sawtooth').cutoff(800)
)`}
/>
So far, we've stayed within the browser. [MIDI and OSC](/learn/input-output/) are ways to break out of it.
+39 -29
View File
@@ -61,7 +61,7 @@ A sample can be looped and chopped like this:
<MiniRepl
client:visible
tune={`await samples('github:yaxu/clean-breaks/main')
s("amen/8").fit().chop(16)`}
s("amen/4").fit().chop(32)`}
punchcard
/>
@@ -72,9 +72,9 @@ Let's add randmized doubling + reversing:
<MiniRepl
client:visible
tune={`await samples('github:yaxu/clean-breaks/main')
s("amen/8").fit().chop(16).cut(1)
.sometimesBy(.5, ply(2))
.sometimesBy(.25, mul(speed(-1)))`}
s("amen/4").fit().chop(16).cut(1)
.sometimesBy(.5, ply("2"))
.sometimesBy(.25, mul(speed("-1")))`}
punchcard
/>
@@ -83,9 +83,9 @@ If we want to specify the order of samples, we can replace `chop` with `slice`:
<MiniRepl
client:visible
tune={`await samples('github:yaxu/clean-breaks/main')
s("amen/8").fit()
.slice(8, "<0 1 2 3 4*2 5 6 [6 7]>")
.cut(1).rarely(ply(2))`}
s("amen/4").fit()
.slice(8, "<0 1 2 3 4*2 5 6 [6 7]>*2")
.cut(1).rarely(ply("2"))`}
punchcard
/>
@@ -95,8 +95,8 @@ If we use `splice` instead of `slice`, the speed adjusts to the duration of the
client:visible
tune={`await samples('github:yaxu/clean-breaks/main')
s("amen")
.splice(8, "<0 1 2 3 4*2 5 6 [6 7]>")
.cut(1).rarely(ply(2))`}
.splice(8, "<0 1 2 3 4*2 5 6 [6 7]>*2")
.cut(1).rarely(ply("2"))`}
punchcard
/>
@@ -104,26 +104,38 @@ Note that we don't need `fit`, because `splice` will do that by itself.
## Filter Envelopes
A minimal filter envelope looks like this:
Using `lpenv`, we can make the filter move:
<MiniRepl
client:visible
tune={`note("g1 bb1 <c2 eb2> d2")
.s("sawtooth")
.lpf(400).lpa(.2).lpenv(4)
.lpf(400).lpenv(4)
.scope()`}
/>
We can flip the envelope by setting `lpenv` negative + add some resonance `lpq`:
The type of envelope depends on the methods you're setting. Let's set `lpa`:
<MiniRepl
client:visible
tune={`note("g1 bb1 <c2 eb2> d2")
.s("sawtooth").lpq(8)
.lpf(400).lpa(.2).lpenv(-4)
.lpf(400).lpa(.2).lpenv(4)
.scope()`}
/>
Now the filter is attacking, rather than decaying as before (decay is the default). We can also do both
<MiniRepl
client:visible
tune={`note("g1 bb1 <c2 eb2> d2")
.s("sawtooth").lpq(8)
.lpf(400).lpa(.1).lpd(.1).lpenv(4)
.scope()`}
/>
You can play around with `lpa` | `lpd` | `lps` | `lpd` to see what the filter envelope will do.
## Layering Sounds
We can layer sounds by separating them with ",":
@@ -183,7 +195,7 @@ A polyrhythm is when 2 different tempos happen at the same time.
This is a polymeter:
<MiniRepl client:visible tune={`s("<bd rim>,<hh hh oh>").fast(2)`} punchcard />
<MiniRepl client:visible tune={`s("<bd rim, hh hh oh>*4")`} punchcard />
A polymeter is when 2 different bar lengths play at the same tempo.
@@ -201,8 +213,8 @@ Using `run` with `n`, we can rush through a sample bank:
<MiniRepl
client:visible
tune={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
n(run(8)).s("ftabla")`}
tune={`await samples('bubo:fox')
n(run(8)).s("ftabla")`}
punchcard
/>
@@ -212,17 +224,17 @@ In this case, I hear the beginning at the third sample, which can be accounted f
<MiniRepl
client:visible
tune={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
n(run(8)).s("ftabla").early(2/8)`}
tune={`await samples('bubo:fox')
n(run(8)).s("ftabla").early(2/8)`}
/>
Let's add some randomness:
<MiniRepl
client:visible
tune={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
tune={`await samples('bubo:fox')
n(run(8)).s("ftabla").early(2/8)
.sometimes(mul(speed(1.5)))`}
.sometimes(mul(speed("1.5")))`}
/>
## Tape Warble
@@ -231,7 +243,7 @@ We can emulate a pitch warbling effect like this:
<MiniRepl
client:visible
tune={`note("c4 bb f eb")
tune={`note("<c4 bb f eb>*8")
.add(note(perlin.range(0,.5))) // <------ warble
.clip(2).s("gm_electric_guitar_clean")`}
/>
@@ -243,7 +255,7 @@ There are a number of ways to change the sound duration. Using clip:
<MiniRepl
client:visible
tune={`note("f ab bb c")
.clip("<2 1 .5 .25>/2")`}
.clip("<2 1 .5 .25>")`}
/>
The value of clip is relative to the duration of each event.
@@ -252,20 +264,18 @@ We can also create overlaps using release:
<MiniRepl
client:visible
tune={`note("f ab bb c")
.release("<2 1 .5 .002>/2")`}
.release("<2 1 .5 .25>")`}
/>
This will smoothly fade out each sound for the given number of seconds.
We could also make the notes shorter with decay / sustain:
We could also make the notes shorter by using a decay envelope:
<MiniRepl
client:visible
tune={`note("f ab bb c")
.decay("<.2 .1 .02>/2").sustain(0)`}
.decay("<2 1 .5 .25>")`}
/>
For now, there is a limitation where decay values that exceed the event duration may cause little cracks, so use higher numbers with caution..
When using samples, we also have `.end` to cut relative to the sample length:
<MiniRepl client:visible tune={`s("oh*4").end("<1 .5 .25 .1>")`} />
@@ -274,9 +284,9 @@ Compare that to clip:
<MiniRepl client:visible tune={`s("oh*4").clip("<1 .5 .25 .1>")`} />
or decay / sustain
or decay:
<MiniRepl client:visible tune={`s("oh*4").decay("<.2 .12 .06 .01>").sustain(0)`} />
<MiniRepl client:visible tune={`s("oh*4").decay("<1 .5 .25 .1>")`} />
## Wavetable Synthesis
+19
View File
@@ -0,0 +1,19 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = (await getCollection('blog')).filter((p) => !p.data.draft);
const options = {
title: 'Strudel Blog',
description:
'The Strudel Blog will keep you updated with the latest changes and things happening in the strudelsphere.',
site: context.site,
items: posts.map((post) => ({
link: `/blog/#${post.slug}`,
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
})),
};
return rss(options);
}
@@ -51,7 +51,7 @@ These packages provide bindings for different ways to output strudel patterns:
### No Longer Maintained
- [react](https://github.com/tidalcycles/strudel/tree/main/packages/react#strudelreact): react hooks and components for strudel
- [react](https://www.npmjs.com/package/@strudel.cycles/react): react hooks and components for strudel
- [eval](https://www.npmjs.com/package/@strudel.cycles/eval): old code transpiler
- [tone](https://www.npmjs.com/package/@strudel.cycles/tone): bindings for Tone.js instruments and effects
- [webdirt](https://www.npmjs.com/package/@strudel.cycles/webdirt): webdirt bindings, replaced by webaudio package
+7 -7
View File
@@ -15,20 +15,20 @@ Strudel's mother language, TidalCycles, even has it in its name.
## Cycles and BPM
In most music software, the unit BPM (beats per minute) is used to set the tempo.
Strudel expresses tempo as CPS (cycles per second), with a default of 1CPS:
Strudel expresses tempo as CPS (cycles per second), with a default of 0.5 CPS:
<MiniRepl client:visible tune={`s("bd")`} />
Here we can hear the 1CPS in action: The kick repeats once per second like a clock.
We could say 1CPS = 1BPS (beats per second) = 60BPM. Let's add another kick:
Here we can hear the 0.5CPS in action: The kick repeats once every two seconds.
Let's make it 4 kicks:
<MiniRepl client:visible tune={`s("bd bd")`} />
<MiniRepl client:visible tune={`s("bd bd bd bd")`} />
Now we have 2 kicks per second, but the whole pattern still plays at 1CPS.
Now we have 4 kicks per cycle, but the whole pattern still plays at 0.5CPS.
In terms of BPM, most musicians would tell you this is playing at 120bpm.
What about this one:
<MiniRepl client:visible tune={`s("bd hh")`} />
<MiniRepl client:visible tune={`s("bd hh bd hh")`} />
Because the second sound is now a hihat, the tempo feels slower again.
This brings us to an important realization:
@@ -103,7 +103,7 @@ To get a time signature, just change the number of elements per bar. Here is a r
or with 5:
<MiniRepl client:visible tune={`s("<bd hh hh bd hh hh bd rim bd hh>*5")`} />
<MiniRepl client:visible tune={`s("bd hh hh bd hh hh bd rim bd hh")`} />
We could also write multiple bars with different time signatures:
+1 -1
View File
@@ -35,7 +35,7 @@ The upper limit decreases with age. What's your upper limit?
In Strudel, we can play frequencies directly with the `freq` control:
<MiniRepl client:visible tune={`freq("200 [300,500] 400 [500,<600 670 712 670>]")`} />
<MiniRepl client:visible tune={`freq("<200 [300,500] 400 [500,<600 670 712 670>]>*8")`} />
## Frequency vs Pitch Perception
+40 -41
View File
@@ -18,7 +18,7 @@ We have sounds, we have notes, now let's look at effects!
<MiniRepl
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth").lpf(800)`}
/>
@@ -35,8 +35,8 @@ lpf = **l**ow **p**ass **f**ilter
<MiniRepl
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000")`}
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth").lpf("200 1000 200 1000")`}
/>
<Box>
@@ -52,8 +52,8 @@ We will learn how to automate with waves later...
<MiniRepl
client:visible
tune={`note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")`}
tune={`note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
.sound("sawtooth").vowel("<a e i o>")`}
/>
**gain**
@@ -61,8 +61,8 @@ We will learn how to automate with waves later...
<MiniRepl
client:visible
tune={`stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
sound("hh*16").gain("[.25 1]*4"),
sound("bd*4,[~ sd:1]*2")
) `}
punchcard
/>
@@ -84,13 +84,13 @@ Let's combine all of the above into a little tune:
client:visible
tune={`stack(
stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
sound("hh*8").gain("[.25 1]*4"),
sound("bd*4,[~ sd:1]*2")
),
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth").lpf("200 1000 200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
.sound("sawtooth").vowel("<a e i o>")
) `}
/>
@@ -105,7 +105,7 @@ The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked toget
<MiniRepl
client:visible
tune={`note("<c3 bb2 f3 eb3>")
tune={`note("c3 bb2 f3 eb3")
.sound("sawtooth").lpf(600)
.attack(.1)
.decay(.1)
@@ -141,7 +141,7 @@ Can you guess what they do?
<MiniRepl
client:visible
tune={`note("<c3 bb2 f3 eb3>")
tune={`note("c3 bb2 f3 eb3")
.sound("sawtooth").lpf(600)
.adsr(".1:.1:.5:.2")
`}
@@ -152,7 +152,7 @@ Can you guess what they do?
<MiniRepl
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707")
).delay(".5")`}
@@ -182,7 +182,7 @@ What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third num
<MiniRepl
client:visible
tune={`n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
tune={`n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2)`}
/>
@@ -200,10 +200,10 @@ Add a delay too!
<MiniRepl
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.5)
)`}
@@ -214,13 +214,13 @@ Let's add a bass to make this complete:
<MiniRepl
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4),
n("<0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~>*2")
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
.scale("D2:minor")
.sound("sawtooth,triangle").lpf(800)
)`}
@@ -237,19 +237,18 @@ Try adding `.hush()` at the end of one of the patterns in the stack...
<MiniRepl
client:visible
tune={`sound("numbers:1 numbers:2 numbers:3 numbers:4")
.pan("0 0.3 .6 1")
.slow(2)`}
.pan("0 0.3 .6 1")`}
/>
**speed**
<MiniRepl client:visible tune={`sound("bd rim").speed("<1 2 -1 -2>").room(.2)`} />
<MiniRepl client:visible tune={`sound("bd rim [~ bd] rim").speed("<1 2 -1 -2>").room(.2)`} />
**fast and slow**
We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation:
<MiniRepl client:visible tune={`sound("bd*2,~ rim").slow(2)`} />
<MiniRepl client:visible tune={`sound("bd*4,~ rim ~ cp").slow(2)`} />
<Box>
@@ -261,13 +260,13 @@ What happens if you use a pattern like `.fast("<1 [2 4]>")`?
By the way, inside Mini-Notation, `fast` is `*` and `slow` is `/`.
<MiniRepl client:visible tune={`sound("[bd*2,~ rim]*<1 [2 4]>")`} />
<MiniRepl client:visible tune={`sound("[bd*4,~ rim ~ cp]*<1 [2 4]>")`} />
## automation with signals
Instead of changing values stepwise, we can also control them with signals:
<MiniRepl client:visible tune={`sound("hh*16").gain(sine)`} punchcard punchcardLabels={false} />
<MiniRepl client:visible tune={`sound("hh*32").gain(sine)`} punchcard punchcardLabels={false} />
<Box>
@@ -283,7 +282,7 @@ The gain is visualized as transparency in the pianoroll.
By default, waves oscillate between 0 to 1. We can change that with `range`:
<MiniRepl client:visible tune={`sound("hh*8").lpf(saw.range(500, 2000))`} />
<MiniRepl client:visible tune={`sound("hh*16").lpf(saw.range(500, 2000))`} />
<Box>
@@ -295,9 +294,9 @@ We can change the automation speed with slow / fast:
<MiniRepl
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth")
.lpf(sine.range(100, 2000).slow(8))`}
.lpf(sine.range(100, 2000).slow(4))`}
/>
<Box>
@@ -308,15 +307,15 @@ The whole automation will now take 8 cycles to repeat.
## Recap
| name | example |
| ----- | --------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
| name | example |
| ----- | ---------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
+14 -14
View File
@@ -175,11 +175,11 @@ Try adding more notes inside the brackets and notice how it does **not** get fas
**Play one sequence per cycle**
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2 */}
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4> */}
<MiniRepl
client:visible
tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>/2")
tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>")
.sound("gm_acoustic_bass")`}
punchcard
/>
@@ -197,7 +197,7 @@ This is also useful for unpitched sounds:
<MiniRepl
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
tune={`sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
.bank("RolandTR909")`}
punchcard
/>
@@ -209,7 +209,7 @@ Finding the right notes can be difficult.. Scales are here to help:
<MiniRepl
client:visible
tune={`n("0 2 4 <[6,8] [7,9]>")
.scale("C:minor").sound("piano")`}
.scale("C:minor").sound("piano").cpm(60)`}
punchcard
/>
@@ -236,7 +236,7 @@ Just like anything, we can automate the scale with a pattern:
client:visible
tune={`n("<0 -3>, 2 4 <[6,8] [7,9]>")
.scale("<C:major D:mixolydian>/4")
.sound("piano")`}
.sound("piano").cpm(60)`}
punchcard
/>
@@ -269,7 +269,7 @@ Try changing that number!
client:visible
tune={`n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2")
.scale("<C2:mixolydian F2:mixolydian>/4")
.sound("gm_acoustic_bass")`}
.sound("gm_acoustic_bass").cpm(60)`}
punchcard
/>
@@ -283,7 +283,7 @@ This is also sometimes called triplet swing. You'll often find it in blues and j
**Replicate**
<MiniRepl client:visible tune={`note("c!2 [eb,<g a bb a>]").sound("piano")`} punchcard />
<MiniRepl client:visible tune={`note("c!2 [eb,<g a bb a>]").sound("piano").cpm(60)`} punchcard />
<Box>
@@ -300,7 +300,7 @@ Let's recap what we've learned in this chapter:
| Concept | Syntax | Example |
| --------- | ------ | -------------------------------------------------------- |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c <e g>")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> |
| Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
@@ -318,7 +318,7 @@ New functions:
<MiniRepl
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("gm_synth_bass_1")
.lpf(800) // <-- we'll learn about this soon`}
/>
@@ -332,7 +332,7 @@ New functions:
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
>*4\`).scale("C4:minor")
.sound("gm_synth_strings_1")`}
/>
@@ -340,7 +340,7 @@ New functions:
<MiniRepl
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
tune={`sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
.bank("RolandTR909")`}
/>
@@ -355,16 +355,16 @@ It's called `stack` 😙
<MiniRepl
client:visible
tune={`stack(
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("gm_synth_bass_1").lpf(800),
n(\`<
[~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
>*4\`).scale("C4:minor")
.sound("gm_synth_strings_1"),
sound("bd*2, ~ <sd cp>, [~ hh]*2")
sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
.bank("RolandTR909")
)`}
/>
+30 -25
View File
@@ -147,11 +147,11 @@ We will look at other ways to change the tempo later!
**Add a rests in a sequence with '~'**
<MiniRepl client:visible tune={`sound("bd hh ~ rim")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh ~ rim ~ bd hh rim")`} punchcard />
**Sub-Sequences with [brackets]**
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd]")`} punchcard />
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd] bd ~ [hh sd] cp")`} punchcard />
<Box>
@@ -163,15 +163,15 @@ Similar to the whole sequence, the content of a sub-sequence will be squished to
**Multiplication: Speed things up**
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3 bd [~ hh*2] rim hh*2")`} punchcard />
**Multiplication: Speed up sequences**
**Multiplication: Speed up subsequences**
<MiniRepl client:visible tune={`sound("bd [hh rim]*2")`} punchcard />
<MiniRepl client:visible tune={`sound("bd [hh rim]*2 bd [hh rim]*1.5")`} punchcard />
**Multiplication: Speeeeeeeeed things up**
<MiniRepl client:visible tune={`sound("bd hh*16 rim hh*8")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh*32 rim hh*16")`} punchcard />
<Box>
@@ -181,7 +181,7 @@ Pitch = really fast rhythm
**Sub-Sub-Sequences with [[brackets]]**
<MiniRepl client:visible tune={`sound("bd [[rim rim] hh]")`} punchcard />
<MiniRepl client:visible tune={`sound("bd [[rim rim] hh] bd cp")`} punchcard />
<Box>
@@ -234,34 +234,39 @@ This is shorter and more readable than:
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
This is what we've leared so far:
| Concept | Syntax | Example |
| ----------------- | -------- | --------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Concept | Syntax | Example |
| ----------------- | -------- | ----------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
The Mini-Notation is usually used inside some function. These are the functions we've seen so far:
| Name | Description | Example |
| ----- | ----------------------------------- | ----------------------------------------------------------------------- |
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd")`} /> |
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd").cpm(90)`} /> |
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
| Name | Description | Example |
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd")`} /> |
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").bank("RolandTR909")`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
## Examples
**Basic rock beat**
<MiniRepl client:visible tune={`sound("bd sd, hh*4").bank("RolandTR505").cpm(100/2)`} punchcard />
<MiniRepl
client:visible
tune={`sound("[bd sd]*2, hh*8").bank("RolandTR505")
.cpm(100/4)`}
punchcard
/>
**Classic house**
<MiniRepl client:visible tune={`sound("bd*2, ~ cp, [~ hh]*2").bank("RolandTR909")`} punchcard />
<MiniRepl client:visible tune={`sound("bd*4, [~ cp]*2, [~ hh]*4").bank("RolandTR909")`} punchcard />
<Box>
@@ -279,7 +284,7 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
.bank("RolandCompurhythm1000")`}
.bank("RolandCompurhythm1000").cpm(120/2)`}
punchcard
/>
+5 -31
View File
@@ -4,6 +4,7 @@ layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { examples } from '../../examples.mjs';
# Welcome
@@ -29,40 +30,13 @@ The best place to actually make music with Strudel is the [Strudel REPL](https:/
- teaching: focussing on a low barrier of entry, Strudel is a good fit for teaching music and code at the same time.
- integrate into your existing music setup: either via MIDI or OSC, you can use Strudel as a really flexible sequencer
## Example
## Examples
Here is an example of how strudel can sound:
Here are some examples of how strudel can sound:
<MiniRepl
client:idle
tune={`stack(
// drums
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8")
.speed(perlin.range(.8,.9)), // random sample speed variation
// bassline
"<a1 b1\*2 a1(3,8) e2>"
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)), // automate cutoff
// chords
"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand')
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
)
.slow(3/2)`}
/>
<MiniRepl client:idle tunes={examples} />
To hear more, go to the [Strudel REPL](https://strudel.cc/) and press shuffle to hear a random example pattern.
These examples cannot fully encompass the variety of things you can do, so [check out the showcase](/intro/showcase/) for some videos of how people use Strudel.
## Getting Started
+31 -31
View File
@@ -15,19 +15,19 @@ In this chapter, we are going to look at functions that are more unique to tidal
**reverse patterns with rev**
<MiniRepl client:visible tune={`n("0 1 [4 3] 2").sound("jazz").rev()`} />
<MiniRepl client:visible tune={`n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").rev()`} />
**play pattern left and modify it right with jux**
<MiniRepl client:visible tune={`n("0 1 [4 3] 2").sound("jazz").jux(rev)`} />
<MiniRepl client:visible tune={`n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").jux(rev)`} />
This is the same as:
<MiniRepl
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0),
n("0 1 [4 3] 2").sound("jazz").pan(1).rev()
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0),
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).rev()
)`}
/>
@@ -36,8 +36,8 @@ Let's visualize what happens here:
<MiniRepl
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0).color("cyan"),
n("0 1 [4 3] 2").sound("jazz").pan(1).color("magenta").rev()
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0).color("cyan"),
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).color("magenta").rev()
)`}
punchcard
/>
@@ -50,16 +50,16 @@ Try commenting out one of the two by adding `//` before a line
**multiple tempos**
<MiniRepl client:visible tune={`note("c2, eb3 g3 [bb3 c4]").sound("piano").slow("1,2,3")`} />
<MiniRepl client:visible tune={`note("c2, eb3 g3 [bb3 c4]").sound("piano").slow("0.5,1,1.5")`} />
This is like doing
<MiniRepl
client:visible
tune={`stack(
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('cyan'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(2).color('magenta'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(3).color('yellow')
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(0.5).color('cyan'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('magenta'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1.5).color('yellow')
)`}
punchcard
/>
@@ -74,9 +74,9 @@ Try commenting out one or more by adding `//` before a line
<MiniRepl
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>"))
tune={`note("c2 [eb3,g3] ".add("<0 <1 -1>>"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
.sound("gm_acoustic_bass").room(.5).cpm(60)`}
punchcard
/>
@@ -92,7 +92,7 @@ We can add as often as we like:
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
.sound("gm_acoustic_bass").room(.5).cpm(60)`}
punchcard
/>
@@ -100,7 +100,7 @@ We can add as often as we like:
<MiniRepl
client:visible
tune={`n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
tune={`n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
.scale("C5:minor").release(.5)
.sound("gm_xylophone").room(.5)`}
punchcard
@@ -111,7 +111,7 @@ We can add as often as we like:
<MiniRepl
client:visible
tune={`stack(
n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
.scale("C5:minor")
.sound("gm_xylophone")
.room(.4).delay(.125),
@@ -119,17 +119,17 @@ We can add as often as we like:
.adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass")
.room(.5),
n("0 1 [2 3] 2").sound("jazz").jux(rev).slow(2)
n("0 1 [2 3] 2").sound("jazz").jux(rev)
)`}
/>
**ply**
<MiniRepl client:visible tune={`sound("hh, bd rim").bank("RolandTR707").ply(2)`} punchcard />
<MiniRepl client:visible tune={`sound("hh hh, bd rim [~ cp] rim").bank("RolandTR707").ply(2)`} punchcard />
this is like writing:
<MiniRepl client:visible tune={`sound("hh*2, bd*2 rim*2").bank("RolandTR707")`} punchcard />
<MiniRepl client:visible tune={`sound("hh*2 hh*2, bd*2 rim*2 [~ cp*2] rim*2").bank("RolandTR707")`} punchcard />
<Box>
@@ -141,10 +141,10 @@ Try patterning the `ply` function, for example using `"<1 2 1 3>"`
<MiniRepl
client:visible
tune={`n("<0 [4 <3 2>] <2 3> [~ 1]>"
.off(1/8, x=>x.add(4))
//.off(1/4, x=>x.add(7))
).scale("<C5:minor Db5:mixolydian>/4")
tune={`n("0 [4 <3 2>] <2 3> [~ 1]"
.off(1/16, x=>x.add(4))
//.off(1/8, x=>x.add(7))
).scale("<C5:minor Db5:mixolydian>/2")
.s("triangle").room(.5).ds(".1:0").delay(.5)`}
punchcard
/>
@@ -159,14 +159,14 @@ off is also useful for sounds:
<MiniRepl
client:visible
tune={`s("bd sd,[~ hh]*2").bank("CasioRZ1")
.off(1/8, x=>x.speed(1.5).gain(.25))`}
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
.off(1/16, x=>x.speed(1.5).gain(.25))`}
/>
| name | description | example |
| ---- | ------------------------------ | ----------------------------------------------------------------------------------- |
| rev | reverse | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | split left/right, modify right | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | add numbers / notes | <MiniRepl client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | speed up each event n times | <MiniRepl client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
| off | copy, shift time & modify | <MiniRepl client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |
| name | description | example |
| ---- | ------------------------------ | ------------------------------------------------------------------------------------------- |
| rev | reverse | <MiniRepl client:visible tune={`n("0 2 4 6 ~ 7 9 5").scale("C:minor").rev()`} /> |
| jux | split left/right, modify right | <MiniRepl client:visible tune={`n("0 2 4 6 ~ 7 9 5").scale("C:minor").jux(rev)`} /> |
| add | add numbers / notes | <MiniRepl client:visible tune={`n("0 2 4 6 ~ 7 9 5".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | speed up each event n times | <MiniRepl client:visible tune={`s("bd sd [~ bd] sd").ply("<1 2 3>")`} /> |
| off | copy, shift time & modify | <MiniRepl client:visible tune={`s("bd sd [~ bd] sd, hh*8").off(1/16, x=>x.speed(2))`} /> |
+13 -13
View File
@@ -13,12 +13,12 @@ This page is just a listing of all functions covered in the workshop!
| Concept | Syntax | Example |
| ----------------- | -------- | --------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sn hh")`} /> |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sn]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sn*2 cp*3")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c <e g>")`} /> |
@@ -45,22 +45,22 @@ This page is just a listing of all functions covered in the workshop!
| name | example |
| ----- | --------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*4")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*32").lpf(saw.range(200,4000))`} /> |
## Pattern Effects
| name | description | example |
| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd").cpm(90)`} /> |
| fast | speed up | <MiniRepl client:visible tune={`sound("bd sd").fast(2)`} /> |
| slow | slow down | <MiniRepl client:visible tune={`sound("bd sd").slow(2)`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
| fast | speed up | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").fast(2)`} /> |
| slow | slow down | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").slow(2)`} /> |
| rev | reverse | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | split left/right, modify right | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | add numbers / notes | <MiniRepl client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
+2 -2
View File
@@ -19,7 +19,7 @@ export function Header({ context }) {
isDirty,
activeCode,
handleTogglePlay,
handleUpdate,
handleEvaluate,
handleShuffle,
handleShare,
} = context;
@@ -85,7 +85,7 @@ export function Header({ context }) {
)}
</button>
<button
onClick={() => handleUpdate()}
onClick={handleEvaluate}
title="update"
className={cx(
'flex items-center space-x-1',
+57 -34
View File
@@ -12,14 +12,15 @@ import { defaultAudioDeviceName } from '../settings.mjs';
import { getAudioDevices, setAudioDevice } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { useCallback, useEffect, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../settings.mjs';
import {
initUserCode,
setActivePattern,
setLatestCode,
settingsMap,
updateUserCode,
useSettings,
} from '../settings.mjs';
createPatternID,
userPattern,
getViewingPatternData,
setViewingPatternData,
} from '../user_pattern_utils.mjs';
import { Header } from './Header';
import Loader from './Loader';
import { Panel } from './panel/Panel';
@@ -29,8 +30,6 @@ import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import './Repl.css';
const { code: randomTune, name } = getRandomTune();
const { latestCode } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, isIframe;
@@ -46,7 +45,6 @@ if (typeof window !== 'undefined') {
export function Repl({ embedded = false }) {
const isEmbedded = embedded || isIframe;
const { panelPosition, isZen } = useSettings();
const init = useCallback(() => {
const drawTime = [-2, 2];
const drawContext = getDrawContext();
@@ -71,32 +69,48 @@ export function Repl({ embedded = false }) {
onUpdateState: (state) => {
setReplState({ ...state });
},
afterEval: ({ code }) => {
updateUserCode(code);
// setPending(false);
afterEval: (all) => {
const { code } = all;
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
const viewingPatternData = getViewingPatternData();
const data = { ...viewingPatternData, code };
let id = data.id;
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
if (isExamplePattern) {
const codeHasChanged = code !== viewingPatternData.code;
if (codeHasChanged) {
// fork example
const newPattern = userPattern.duplicate(data);
id = newPattern.id;
setViewingPatternData(newPattern.data);
}
} else {
id = userPattern.isValidID(id) ? id : createPatternID();
setViewingPatternData(userPattern.update(id, data).data);
}
setActivePattern(id);
},
bgFill: false,
});
// init settings
initCode().then((decoded) => {
initCode().then(async (decoded) => {
let msg;
if (decoded) {
editor.setCode(decoded);
initUserCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
editor.setCode(latestCode);
msg = `Your last session has been loaded!`;
} else {
const { code: randomTune, name } = await getRandomTune();
editor.setCode(randomTune);
msg = `A random code snippet named "${name}" has been loaded!`;
}
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
// setPending(false);
});
editorRef.current = editor;
@@ -138,29 +152,37 @@ export function Repl({ embedded = false }) {
// UI Actions
//
const handleTogglePlay = async () => editorRef.current?.toggle();
const handleUpdate = async (newCode, reset = false) => {
if (reset) {
clearCanvas();
resetLoadedSounds();
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
}
if (newCode) {
editorRef.current.setCode(newCode);
editorRef.current.repl.evaluate(newCode);
} else if (isDirty) {
editorRef.current.evaluate();
}
const handleTogglePlay = async () => {
editorRef.current?.toggle();
};
const handleShuffle = async () => {
// window.postMessage('strudel-shuffle');
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
setActivePattern(name);
const resetEditor = async () => {
clearCanvas();
resetLoadedSounds();
editorRef.current.repl.setCps(0.5);
await prebake(); // declare default samples
};
const handleUpdate = async (patternData, reset = false) => {
setViewingPatternData(patternData);
editorRef.current.setCode(patternData.code);
if (reset) {
await resetEditor();
handleEvaluate();
}
};
const handleEvaluate = () => {
editorRef.current.evaluate();
};
const handleShuffle = async () => {
const patternData = await getRandomTune();
const code = patternData.code;
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
setActivePattern(patternData.id);
setViewingPatternData(patternData);
clearCanvas();
resetLoadedSounds();
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
editorRef.current.setCode(code);
editorRef.current.repl.evaluate(code);
@@ -177,6 +199,7 @@ export function Repl({ embedded = false }) {
handleUpdate,
handleShuffle,
handleShare,
handleEvaluate,
};
return (
+171 -159
View File
@@ -1,178 +1,190 @@
import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid';
import { useMemo } from 'react';
import {
$featuredPatterns,
$publicPatterns,
clearUserPatterns,
deleteActivePattern,
duplicateActivePattern,
exportPatterns,
getUserPattern,
importPatterns,
newUserPattern,
renameActivePattern,
setActivePattern,
patternFilterName,
useActivePattern,
useSettings,
} from '../../settings.mjs';
import * as tunes from '../tunes.mjs';
import { useStore } from '@nanostores/react';
useViewingPatternData,
userPattern,
} from '../../user_pattern_utils.mjs';
import { useMemo } from 'react';
import { getMetadata } from '../../metadata_parser';
import { useExamplePatterns } from '../useExamplePatterns';
import { parseJSON } from '../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../settings.mjs';
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
export function PatternsTab({ context }) {
const { userPatterns } = useSettings();
const activePattern = useActivePattern();
const featuredPatterns = useStore($featuredPatterns);
const publicPatterns = useStore($publicPatterns);
const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]);
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
let title = meta.title;
if (title == null) {
const date = new Date(pattern.created_at);
if (!isNaN(date)) {
title = date.toLocaleDateString();
}
}
if (title == null) {
title = pattern.hash;
}
if (title == null) {
title = 'unnamed';
}
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
}
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-4 pb-4">
<section>
{activePattern && (
<div className="flex items-center mb-2 space-x-2 overflow-auto">
<h1 className="text-xl">{activePattern}</h1>
<div className="space-x-4 flex w-min">
{!isExample && (
<button className="hover:opacity-50" onClick={() => renameActivePattern()} title="Rename">
<PencilSquareIcon className="w-5 h-5" />
{/* <PencilIcon className="w-5 h-5" /> */}
</button>
)}
<button className="hover:opacity-50" onClick={() => duplicateActivePattern()} title="Duplicate">
<DocumentDuplicateIcon className="w-5 h-5" />
</button>
{!isExample && (
<button className="hover:opacity-50" onClick={() => deleteActivePattern()} title="Delete">
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
</div>
)}
<div className="font-mono text-sm">
{Object.entries(userPatterns).map(([key, up]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
const { code } = up;
setActivePattern(key);
context.handleUpdate(code, true);
}}
>
{key}
</a>
))}
</div>
<div className="pr-4 space-x-4 border-b border-foreground mb-2 h-8 flex overflow-auto max-w-full items-center">
<button
className="hover:opacity-50"
onClick={() => {
const name = newUserPattern();
const { code } = getUserPattern(name);
context.handleUpdate(code, true);
}}
>
new
</button>
<button className="hover:opacity-50" onClick={() => clearUserPatterns()}>
clear
</button>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
<a
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1',
showHiglight && 'bg-selection',
)}
onClick={onClick}
>
<PatternLabel pattern={pattern} />
</a>
);
}
function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternID = viewingPatternData.id;
return (
<div className="font-mono text-sm">
{Object.values(patterns)
.reverse()
.map((pattern) => {
const id = pattern.id;
return (
<PatternButton
pattern={pattern}
key={id}
showHiglight={id === viewingPatternID}
showOutline={id === activePattern && started}
onClick={() => onClick(id)}
/>
import
</label>
<button className="hover:opacity-50" onClick={() => exportPatterns()}>
export
</button>
</div>
</section>
{featuredPatterns && (
<section>
<h2 className="text-xl mb-2">Featured Patterns</h2>
<div className="font-mono text-sm">
{featuredPatterns.map((pattern) => (
<a
key={pattern.id}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer block',
pattern.hash === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
setActivePattern(pattern.hash);
context.handleUpdate(pattern.code, true);
}}
>
<PatternLabel pattern={pattern} />
</a>
))}
</div>
</section>
)}
{publicPatterns && (
<section>
<h2 className="text-xl mb-2">Last Creations</h2>
<div className="font-mono text-sm">
{publicPatterns.map((pattern) => (
<a
key={'public-' + pattern.id}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer block', // inline-block
pattern.hash === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
setActivePattern(pattern.hash);
context.handleUpdate(pattern.code, true);
}}
>
<PatternLabel pattern={pattern} />
</a>
))}
</div>
</section>
)}
<section>
<h2 className="text-xl mb-2">Stock Examples</h2>
<div className="font-mono text-sm">
{Object.entries(tunes).map(([key, tune]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
setActivePattern(key);
context.handleUpdate(tune, true);
}}
>
{key}
</a>
))}
</div>
</section>
);
})}
</div>
);
}
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<>
{pattern.id}. {meta.title || pattern.hash} by {Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}
</>
<button className="hover:opacity-50" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
);
}
export function PatternsTab({ context }) {
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter } = useSettings();
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const updateCodeWindow = (patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const viewingPatternID = viewingPatternData?.id;
const autoResetPatternOnChange = !window.parent?.location.pathname.includes('oodles');
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user && (
<div>
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow({ ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(data);
}}
/>
</div>
</div>
)}
<section className="flex overflow-y-scroll max-h-full flex-col">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
)}
{patternFilter !== patternFilterName.user &&
Array.from(collections.keys()).map((collection) => {
const patterns = collections.get(collection);
return (
<section key={collection} className="py-2">
<h2 className="text-xl mb-2">{collection}</h2>
<div className="font-mono text-sm">
<PatternButtons
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
</section>
);
})}
</section>
</div>
);
}
+4 -2
View File
@@ -314,7 +314,7 @@ stack(
)
.fast(2/3)
.pianoroll()`;
/*
export const bridgeIsOver = `// "Bridge is over"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos, bassline by BDP - The Bridge Is Over
@@ -333,7 +333,7 @@ stack(
s("mad").slow(2)
).cpm(78).slow(4)
.pianoroll()
`;
`; */
export const goodTimes = `// "Good times"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
@@ -709,6 +709,8 @@ export const dinofunk = `// "Dinofunk"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
setcps(1)
samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3',
dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}})
setVoicingRange('lefthand', ['c3','a4'])
+26
View File
@@ -0,0 +1,26 @@
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
import { useStore } from '@nanostores/react';
import { useMemo } from 'react';
import * as tunes from '../repl/tunes.mjs';
export const stockPatterns = Object.fromEntries(
Object.entries(tunes).map(([key, code], i) => [i, { id: i, code, collection: 'Stock Examples' }]),
);
export const useExamplePatterns = () => {
const featuredPatterns = useStore($featuredPatterns);
const publicPatterns = useStore($publicPatterns);
const collections = useMemo(() => {
const pats = new Map();
pats.set(collectionName.featured, featuredPatterns);
pats.set(collectionName.public, publicPatterns);
// pats.set(collectionName.stock, stockPatterns);
return pats;
}, [featuredPatterns, publicPatterns]);
const patterns = useMemo(() => {
return Object.assign({}, ...collections.values());
}, [collections]);
return { patterns, collections };
};
+20 -28
View File
@@ -4,12 +4,12 @@ import { getAudioContext, initializeAudioOutput, setDefaultAudioContext } from '
import { isTauri } from '../tauri.mjs';
import './Repl.css';
import * as tunes from './tunes.mjs';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
import { writeText } from '@tauri-apps/api/clipboard';
import { createContext } from 'react';
import { $publicPatterns, $featuredPatterns } from '../settings.mjs';
import { $featuredPatterns, loadDBPatterns } from '@src/user_pattern_utils.mjs';
// Create a single supabase client for interacting with your database
export const supabase = createClient(
@@ -17,27 +17,9 @@ export const supabase = createClient(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
export function loadPublicPatterns() {
return supabase.from('code').select().eq('public', true).limit(20).order('id', { ascending: false });
}
export function loadFeaturedPatterns() {
return supabase.from('code').select().eq('featured', true).limit(20).order('id', { ascending: false });
}
async function loadDBPatterns() {
try {
const { data: publicPatterns } = await loadPublicPatterns();
const { data: featuredPatterns } = await loadFeaturedPatterns();
$publicPatterns.set(publicPatterns);
$featuredPatterns.set(featuredPatterns);
} catch (err) {
console.error('error loading patterns');
}
}
let dbLoaded;
if (typeof window !== 'undefined') {
loadDBPatterns();
dbLoaded = loadDBPatterns();
}
export async function initCode() {
@@ -52,7 +34,7 @@ export async function initCode() {
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
.from('code_v1')
.select('code')
.eq('hash', hash)
.then(({ data, error }) => {
@@ -70,11 +52,21 @@ export async function initCode() {
}
}
export function getRandomTune() {
const allTunes = Object.entries(tunes);
export const parseJSON = (json) => {
json = json != null && json.length ? json : '{}';
try {
return JSON.parse(json);
} catch {
return '{}';
}
};
export async function getRandomTune() {
await dbLoaded;
const featuredTunes = Object.entries($featuredPatterns.get());
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const [name, code] = randomItem(allTunes);
return { name, code };
const [_, data] = randomItem(featuredTunes);
return data;
}
export function loadModules() {
@@ -120,7 +112,7 @@ export async function shareCode(codeToShare) {
// generate uuid in the browser
const hash = nanoid(12);
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
const { error } = await supabase.from('code').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
if (!error) {
lastShared = codeToShare;
// copy shareUrl to clipboard
+10 -198
View File
@@ -1,12 +1,6 @@
import { atom } from 'nanostores';
import { persistentMap, persistentAtom } from '@nanostores/persistent';
import { persistentMap } from '@nanostores/persistent';
import { useStore } from '@nanostores/react';
import { register } from '@strudel/core';
import * as tunes from './repl/tunes.mjs';
import { logger } from '@strudel/core';
export let $publicPatterns = atom([]);
export let $featuredPatterns = atom([]);
export const defaultAudioDeviceName = 'System Standard';
@@ -26,6 +20,7 @@ export const defaultSettings = {
latestCode: '',
isZen: false,
soundsFilter: 'all',
patternFilter: 'community',
panelPosition: 'right',
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
@@ -33,26 +28,15 @@ export const defaultSettings = {
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
// active pattern is separate, because it shouldn't sync state across tabs
// reason: https://github.com/tidalcycles/strudel/issues/857
const $activePattern = persistentAtom('activePattern', '', { listen: false });
export function setActivePattern(key) {
$activePattern.set(key);
}
export function getActivePattern() {
return $activePattern.get();
}
export function useActivePattern() {
return useStore($activePattern);
}
export function initUserCode(code) {
const userPatterns = getUserPatterns();
const match = Object.entries(userPatterns).find(([_, pat]) => pat.code === code);
setActivePattern(match?.[0] || '');
}
export function useSettings() {
const state = useStore(settingsMap);
const userPatterns = JSON.parse(state.userPatterns);
Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key];
data.id = data.id ?? key;
userPatterns[key] = data;
});
return {
...state,
isZen: [true, 'true'].includes(state.isZen) ? true : false,
@@ -65,13 +49,12 @@ export function useSettings() {
isFlashEnabled: [true, 'true'].includes(state.isFlashEnabled) ? true : false,
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
userPatterns: JSON.parse(state.userPatterns),
userPatterns: userPatterns,
};
}
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) =>
@@ -90,174 +73,3 @@ export const fontFamily = patternSetting('fontFamily');
export const fontSize = patternSetting('fontSize');
export const settingPatterns = { theme, fontFamily, fontSize };
export function getUserPatterns() {
return JSON.parse(settingsMap.get().userPatterns);
}
function getSetting(key) {
return settingsMap.get()[key];
}
export function setUserPatterns(obj) {
settingsMap.setKey('userPatterns', JSON.stringify(obj));
}
export function addUserPattern(name, config) {
if (typeof config !== 'object') {
throw new Error('addUserPattern expected object as second param');
}
if (!config.code) {
throw new Error('addUserPattern expected code as property of second param');
}
const userPatterns = getUserPatterns();
setUserPatterns({ ...userPatterns, [name]: config });
}
export function newUserPattern() {
const userPatterns = getUserPatterns();
const date = new Date().toISOString().split('T')[0];
const todays = Object.entries(userPatterns).filter(([name]) => name.startsWith(date));
const num = String(todays.length + 1).padStart(3, '0');
const defaultNewPattern = 's("hh")';
const name = date + '_' + num;
addUserPattern(name, { code: defaultNewPattern });
setActivePattern(name);
return name;
}
export function clearUserPatterns() {
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
return;
}
setUserPatterns({});
}
export function getNextCloneName(key) {
const userPatterns = getUserPatterns();
const clones = Object.entries(userPatterns).filter(([name]) => name.startsWith(key));
const num = String(clones.length + 1).padStart(3, '0');
return key + '_' + num;
}
export function getUserPattern(key) {
const userPatterns = getUserPatterns();
return userPatterns[key];
}
export function renameActivePattern() {
let activePattern = getActivePattern();
let userPatterns = getUserPatterns();
if (!userPatterns[activePattern]) {
alert('Cannot rename examples');
return;
}
const newName = prompt('Enter new name', activePattern);
if (newName === null) {
// canceled
return;
}
if (userPatterns[newName]) {
alert('Name already taken!');
return;
}
userPatterns[newName] = userPatterns[activePattern]; // copy code
delete userPatterns[activePattern];
setUserPatterns({ ...userPatterns });
setActivePattern(newName);
}
export function updateUserCode(code) {
const userPatterns = getUserPatterns();
let activePattern = getActivePattern();
// check if code is that of an example tune
const [example] = Object.entries(tunes).find(([_, tune]) => tune === code) || [];
if (example && (!activePattern || activePattern === example)) {
// select example
setActivePattern(example);
return;
}
const publicPattern = $publicPatterns.get().find((pat) => pat.code === code);
if (publicPattern) {
setActivePattern(publicPattern.hash);
return;
}
const featuredPattern = $featuredPatterns.get().find((pat) => pat.code === code);
if (featuredPattern) {
setActivePattern(featuredPattern.hash);
return;
}
if (!activePattern) {
// create new user pattern
activePattern = newUserPattern();
setActivePattern(activePattern);
} else if (
(!!tunes[activePattern] && code !== tunes[activePattern]) || // fork example tune?
$publicPatterns.get().find((p) => p.hash === activePattern) || // fork public pattern?
$featuredPatterns.get().find((p) => p.hash === activePattern) // fork featured pattern?
) {
// fork example
activePattern = getNextCloneName(activePattern);
setActivePattern(activePattern);
}
setUserPatterns({ ...userPatterns, [activePattern]: { code } });
}
export function deleteActivePattern() {
let activePattern = getActivePattern();
if (!activePattern) {
console.warn('cannot delete: no pattern selected');
return;
}
const userPatterns = getUserPatterns();
if (!userPatterns[activePattern]) {
alert('Cannot delete examples');
return;
}
if (!confirm(`Really delete the selected pattern "${activePattern}"?`)) {
return;
}
setUserPatterns(Object.fromEntries(Object.entries(userPatterns).filter(([key]) => key !== activePattern)));
setActivePattern('');
}
export function duplicateActivePattern() {
let activePattern = getActivePattern();
let latestCode = getSetting('latestCode');
if (!activePattern) {
console.warn('cannot duplicate: no pattern selected');
return;
}
const userPatterns = getUserPatterns();
activePattern = getNextCloneName(activePattern);
setUserPatterns({ ...userPatterns, [activePattern]: { code: latestCode } });
setActivePattern(activePattern);
}
export async function importPatterns(fileList) {
const files = Array.from(fileList);
await Promise.all(
files.map(async (file, i) => {
const content = await file.text();
if (file.type === 'application/json') {
const userPatterns = getUserPatterns() || {};
setUserPatterns({ ...userPatterns, ...JSON.parse(content) });
} else if (file.type === 'text/plain') {
const name = file.name.replace(/\.[^/.]+$/, '');
addUserPattern(name, { code: content });
}
}),
);
logger(`import done!`);
}
export async function exportPatterns() {
const userPatterns = getUserPatterns() || {};
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
const date = new Date().toISOString().split('T')[0];
downloadLink.download = `strudel_patterns_${date}.json`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
+188
View File
@@ -0,0 +1,188 @@
import { atom } from 'nanostores';
import { persistentAtom } from '@nanostores/persistent';
import { useStore } from '@nanostores/react';
import { logger } from '@strudel/core';
import { nanoid } from 'nanoid';
import { settingsMap } from './settings.mjs';
import { parseJSON, supabase } from './repl/util.mjs';
export let $publicPatterns = atom([]);
export let $featuredPatterns = atom([]);
export const collectionName = {
user: 'user',
public: 'Last Creations',
stock: 'Stock Examples',
featured: 'Featured',
};
export const patternFilterName = {
community: 'community',
user: 'user',
};
export let $viewingPatternData = persistentAtom(
'viewingPatternData',
{
id: '',
code: '',
collection: collectionName.user,
created_at: Date.now(),
},
{ listen: false },
);
export const getViewingPatternData = () => {
return parseJSON($viewingPatternData.get());
};
export const useViewingPatternData = () => {
return useStore($viewingPatternData);
};
export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data));
};
export function loadPublicPatterns() {
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false });
}
export function loadFeaturedPatterns() {
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false });
}
export async function loadDBPatterns() {
try {
const { data: publicPatterns } = await loadPublicPatterns();
const { data: featuredPatterns } = await loadFeaturedPatterns();
const featured = {};
const pub = {};
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
$publicPatterns.set(pub);
$featuredPatterns.set(featured);
} catch (err) {
console.error('error loading patterns', err);
}
}
// reason: https://github.com/tidalcycles/strudel/issues/857
const $activePattern = persistentAtom('activePattern', '', { listen: false });
export function setActivePattern(key) {
$activePattern.set(key);
}
export function getActivePattern() {
return $activePattern.get();
}
export function useActivePattern() {
return useStore($activePattern);
}
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
const defaultCode = '';
export const userPattern = {
collection: collectionName.user,
getAll() {
const patterns = parseJSON(settingsMap.get().userPatterns);
return patterns ?? {};
},
getPatternData(id) {
const userPatterns = this.getAll();
return userPatterns[id];
},
exists(id) {
return this.getPatternData(id) != null;
},
isValidID(id) {
return id != null && id.length > 0;
},
create() {
const newID = createPatternID();
const code = defaultCode;
const data = { code, created_at: Date.now(), id: newID, collection: this.collection };
return { id: newID, data };
},
createAndAddToDB() {
const newPattern = this.create();
return this.update(newPattern.id, newPattern.data);
},
update(id, data) {
const userPatterns = this.getAll();
data = { ...data, id, collection: this.collection };
setUserPatterns({ ...userPatterns, [id]: data });
return { id, data };
},
duplicate(data) {
const newPattern = this.create();
return this.update(newPattern.id, { ...newPattern.data, code: data.code });
},
clearAll() {
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
return;
}
const viewingPatternData = getViewingPatternData();
setUserPatterns({});
if (viewingPatternData.collection !== this.collection) {
return { id: viewingPatternData.id, data: viewingPatternData };
}
setActivePattern(null);
return this.create();
},
delete(id) {
const userPatterns = this.getAll();
delete userPatterns[id];
if (getActivePattern() === id) {
setActivePattern(null);
}
setUserPatterns(userPatterns);
const viewingPatternData = getViewingPatternData();
const viewingID = viewingPatternData?.id;
if (viewingID === id) {
return { id: null, data: { code: defaultCode } };
}
return { id: viewingID, data: userPatterns[viewingID] };
},
};
function setUserPatterns(obj) {
return settingsMap.setKey('userPatterns', JSON.stringify(obj));
}
export const createPatternID = () => {
return nanoid(12);
};
export async function importPatterns(fileList) {
const files = Array.from(fileList);
await Promise.all(
files.map(async (file, i) => {
const content = await file.text();
if (file.type === 'application/json') {
const userPatterns = userPattern.getAll();
setUserPatterns({ ...userPatterns, ...parseJSON(content) });
} else if (file.type === 'text/plain') {
const id = file.name.replace(/\.[^/.]+$/, '');
userPattern.update(id, { code: content });
}
}),
);
logger(`import done!`);
}
export async function exportPatterns() {
const userPatterns = userPattern.getAll();
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
const date = new Date().toISOString().split('T')[0];
downloadLink.download = `strudel_patterns_${date}.json`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}