Compare commits

...

243 Commits

Author SHA1 Message Date
Karol Libardi 3daacba2c3 Added ability to mute a synth/sound after it was triggered 2024-03-04 10:27:55 +01:00
Felix Roos fa7c16dbe7 Merge pull request #959 from tidalcycles/euclid-inverted
feat: can now invert euclid pulses with negative numbers
2024-02-29 15:33:12 +01:00
Felix Roos 6a43eb6755 Merge pull request #950 from eefano/pickr-pickrmod
pickOut(), pickRestart(), pickReset()
2024-02-29 15:31:23 +01:00
Felix Roos c524ebc7f3 fix: add name tags 2024-02-29 15:25:31 +01:00
Felix Roos 0deb905cdf rename pick*Outer > pickOut
+ add pick*Squeeze alias to inhabit*
2024-02-29 15:21:44 +01:00
Felix Roos 4cadbcbbdf Merge pull request #965 from tidalcycles/legato-fix
remove legacy legato + duration implementations
2024-02-29 15:12:45 +01:00
Felix Roos 96eef75f74 fix: end / begin. sampler now needs clip to choose duration... 2024-02-29 10:33:27 +01:00
Felix Roos 51d9e02e07 Merge pull request #966 from eefano/transpose-fix
fix for transpose(): preserve hap value object structure
2024-02-29 08:47:53 +01:00
Felix Roos c4c21c4f0c Merge pull request #968 from tidalcycles/log-debounce
add debounce to logger
2024-02-29 08:32:00 +01:00
Felix Roos fcb247b62a add debounce to logger 2024-02-29 04:25:00 +01:00
Felix Roos 7556da7839 snapshot 2024-02-29 04:06:15 +01:00
Felix Roos eb93a6c149 refactor: duration is now a regular control 2024-02-29 04:05:05 +01:00
eefano b870ab204a fix for transpose(): preserve hap value object structure 2024-02-29 00:13:08 +01:00
Felix Roos f779e81993 remove old snapshots 2024-02-28 18:52:08 +01:00
Felix Roos 5c71bb95a0 remove legacy legato
+ make legato a synonym of clip
2024-02-28 18:51:07 +01:00
Felix Roos 1525992d73 Merge pull request #962 from tidalcycles/control-refactoring
controls refactoring: simplify exports
2024-02-28 18:43:51 +01:00
Felix Roos abe25be05e fix examples 2024-02-28 00:36:59 +01:00
Felix Roos 664a19a5c4 refactor: remove now redundant controls imports 2024-02-28 00:36:44 +01:00
Felix Roos 8efadd2547 refactor: controls are now exported directly 2024-02-28 00:34:39 +01:00
eefano ed1f3e1e8c Merge remote-tracking branch 'upstream/main' into pickr-pickrmod 2024-02-26 01:15:27 +01:00
eefano 4aaf9eaed4 mux operator, muxers.mjs with pick variants
prettier

prettier2

clamp?

Revert "clamp?"

This reverts commit 83a3b9123aece9f0e366c83d97f99e8b8d20db54.

Revert "prettier2"

This reverts commit de13871a8be4a450b79a4d9e65b9d1982485577e.

Revert "prettier"

This reverts commit 1550bf49527f591d05fa2766ca66ccb16e142011.

Revert "mux operator, muxers.mjs with pick variants"

This reverts commit af062f93c23ded4440f968186bfc69851d8aec67.
2024-02-26 01:11:56 +01:00
Felix Roos 0416e4d212 Merge pull request #960 from tidalcycles/reset-global-fx
fix: reset global fx on pattern change
2024-02-25 19:17:00 +01:00
Felix Roos b9247db001 fix: reset global fx on pattern change 2024-02-25 15:09:15 +01:00
eefano 74ef157fa1 Merge remote-tracking branch 'upstream/main' into pickr-pickrmod 2024-02-25 14:52:12 +01:00
Felix Roos d4724b0e12 feat: can now invert euclid pulses with negative numbers 2024-02-25 14:43:02 +01:00
eefano dbfdbcf2b7 typo 2024-02-25 14:39:38 +01:00
eefano 0986b14508 added pickOuter, pickmodOuter 2024-02-25 14:31:07 +01:00
Felix Roos 325152c782 Merge pull request #936 from Enelg52/midi-issues
fix midi issue on firefox and added quote error
2024-02-25 14:15:30 +01:00
Felix Roos c136d12194 Merge pull request #956 from eefano/bracketMatching
'Enable Bracket Matching' option in Codemirror
2024-02-25 13:19:46 +01:00
Felix Roos 7557952155 format 2024-02-25 13:17:52 +01:00
Felix Roos b0b41e6718 revert unintended change 2024-02-25 13:17:29 +01:00
Felix Roos 22e3067b8c set bracket matching true by default 2024-02-25 13:16:53 +01:00
Felix Roos e9ab01ab06 Merge pull request #957 from tidalcycles/web-iife
fix script importable packages (web + repl)
2024-02-23 14:37:45 +01:00
Felix Roos 058e554b0f update example 2024-02-23 14:33:07 +01:00
Felix Roos 6d9327aebc adapt repl package similar to web package 2024-02-23 14:30:29 +01:00
Felix Roos c23d7850c7 simplify web example 2024-02-23 14:30:07 +01:00
Felix Roos 2854f0cf34 try different main / module setting + bump 2024-02-23 14:28:43 +01:00
Felix Roos a89e87ca2e add web package example using script tag 2024-02-23 14:21:59 +01:00
Felix Roos 5419ca431b bump web package version 2024-02-23 14:17:11 +01:00
Felix Roos e8b674badd add iife build to web package 2024-02-23 14:16:54 +01:00
eefano 8af40a9c61 added <> for mininotation 2024-02-22 23:35:00 +01:00
eefano f54aa3d823 'Enable Bracket Matching' option in Codemirror 2024-02-22 22:04:57 +01:00
Jade (Rose) Rowland 5e91287e3e Merge pull request #954 from daslyfe/midi_duration_fix
account for cps in midi time duration
2024-02-21 10:17:36 -05:00
Felix Roos 4e3d4a7060 hotfix: some samples were broken 2024-02-21 15:35:59 +01:00
Felix Roos 3d80da9816 Merge pull request #955 from tidalcycles/auto-await-samples
Auto await samples
2024-02-21 10:27:12 +01:00
Felix Roos bf63b716af refactor: remove await before samples calls in docs etc..
+ remove /master from dirt-samples calls (there is now a main branch)
+ remove /main endings from some places
2024-02-21 10:17:45 +01:00
Felix Roos 38d389b838 add test 2024-02-21 09:50:21 +01:00
Felix Roos ebdca32c80 automatically add await to samples call 2024-02-21 09:48:26 +01:00
Jade (Rose) Rowland c06a3710c1 fixed 2024-02-19 23:43:35 -05:00
eefano d5e67fe13f Update signal.mjs
typo
2024-02-18 19:01:05 +01:00
eefano 00c7da45c0 Update signal.mjs
now pickRestart() and pickReset()
2024-02-18 18:55:05 +01:00
eefano 3347817350 Update signal.mjs
using trigZeroJoin in pickr() instead
2024-02-18 18:41:06 +01:00
eefano 88ece92a78 Update signal.mjs
Make pickr() handle dictionaries also
2024-02-18 16:40:27 +01:00
eefano 03a73fb95e Update signal.mjs
Fixed pickr() description
2024-02-18 16:21:50 +01:00
eefano 23c55c9636 Update signal.mjs
prettier fix
2024-02-18 16:03:01 +01:00
eefano 12c6f4e95f Update signal.mjs
added pickr() and pickrmod()
2024-02-18 15:30:24 +01:00
Felix Roos a468ddb85b Merge pull request #945 from tidalcycles/cjs-fixes
remove cjs builds
2024-02-08 13:16:15 +01:00
Felix Roos 051bdaccc7 Publish
- @strudel/codemirror@1.0.1
 - @strudel/core@1.0.1
 - @strudel/csound@1.0.1
 - @strudel/hydra@1.0.1
 - @strudel/midi@1.0.1
 - @strudel/mini@1.0.1
 - @strudel/osc@1.0.1
 - @strudel/repl@1.0.1
 - @strudel/serial@1.0.1
 - @strudel/soundfonts@1.0.1
 - superdough@1.0.1
 - @strudel/tonal@1.0.1
 - @strudel/transpiler@1.0.1
 - @strudel/web@1.0.1
 - @strudel/webaudio@1.0.1
 - @strudel/xen@1.0.1
2024-02-08 13:04:25 +01:00
Felix Roos 5dc04d9574 make sure to overwrite main field 2024-02-08 12:36:04 +01:00
Felix Roos 60129413d3 remove cjs builds
+ add type module everywhere
2024-02-08 12:19:11 +01:00
Felix Roos d2bf9370ea Update README.md
add link to usage example for repl package
2024-02-04 23:24:43 +01:00
Felix Roos fd11aa5eec Merge pull request #941 from mhetrick/patch-1
Minor documentation error: Update first-sounds.mdx
2024-02-04 23:15:37 +01:00
Michael Hetrick fb7c2b61d3 Update first-sounds.mdx
Update the tutorial text to reflect that the default cpm setting has changed from 60 to 30.
2024-01-26 07:13:09 -08:00
Felix Roos dd64bf0fc9 Merge pull request #938 from tidalcycles/pianoroll-sorting
fix: pianoroll sorting
2024-01-24 16:48:57 +01:00
Felix Roos 434eb3b256 fix: pianoroll sorting 2024-01-24 16:40:26 +01:00
Enelg52 6da98a5f31 fix midi issue on firefox and quote error 2024-01-23 16:47:46 +01:00
Felix Roos 8fef75385a Update README.md 2024-01-23 01:35:28 +01:00
Felix Roos c66b9219a8 Update README.md 2024-01-23 01:31:11 +01:00
Felix Roos 2a1f8823e8 Merge pull request #935 from tidalcycles/v1-release-notes
V1 release notes
2024-01-23 00:04:03 +01:00
Felix Roos 3da65f90e3 shorter titles for release notes 2024-01-22 23:55:57 +01:00
Felix Roos a36cc24087 v1 release notes 2024-01-22 23:55:49 +01:00
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
Felix Roos b04a262433 Publish
- @strudel/codemirror@0.11.0
 - @strudel/core@0.11.0
 - @strudel/csound@0.11.0
 - @strudel/embed@0.11.0
 - @strudel/hydra@0.11.0
 - @strudel/midi@0.11.0
 - @strudel/mini@0.11.0
 - @strudel/osc@0.11.0
 - @strudel/repl@0.11.0
 - @strudel/serial@0.11.0
 - @strudel/soundfonts@0.11.0
 - @strudel/tonal@0.11.0
 - @strudel/transpiler@0.11.0
 - @strudel/web@0.11.0
 - @strudel/webaudio@0.11.0
 - @strudel/xen@0.11.0
2024-01-18 09:59:57 +01:00
Felix Roos 96bafa7f0b the big rename: @strudel.cycles/* -> @strudel/* 2024-01-18 09:54:37 +01:00
Felix Roos b10612da5c Publish
- @strudel/codemirror@0.10.0
 - @strudel.cycles/core@0.10.0
 - @strudel.cycles/csound@0.10.0
 - @strudel.cycles/embed@0.10.0
 - @strudel/hydra@0.10.0
 - @strudel.cycles/midi@0.10.0
 - @strudel.cycles/mini@0.10.0
 - @strudel.cycles/osc@0.10.0
 - @strudel/repl@0.10.0
 - @strudel.cycles/serial@0.10.0
 - @strudel.cycles/soundfonts@0.10.0
 - superdough@0.10.0
 - @strudel.cycles/tonal@0.10.0
 - @strudel.cycles/transpiler@0.10.0
 - @strudel/web@0.10.0
 - @strudel.cycles/webaudio@0.10.0
 - @strudel.cycles/xen@0.10.0
2024-01-18 09:35:20 +01:00
Felix Roos 171f4206a8 Merge branch 'main' into deprecate-old-package-names 2024-01-18 09:33:16 +01:00
Felix Roos bd83d19197 Merge pull request #913 from tidalcycles/pitch-envelopes
pitch envelope
2024-01-18 06:54:33 +01:00
Felix Roos 95cf201c6d fix: heading 2024-01-18 06:49:09 +01:00
Felix Roos 4107711dae snapshot 2024-01-18 06:48:21 +01:00
Felix Roos 67f0758cb5 document pitch envelope 2024-01-18 06:47:54 +01:00
Felix Roos 1c99944a32 pitch envelope now also works when setting one of the controls (penv not needed) 2024-01-18 06:47:44 +01:00
Felix Roos d3ea4959b4 - more flexible pitch envelopes
- fix: exponential envelopes could break due to 0 values
2024-01-18 06:47:12 +01:00
Felix Roos 70b27fbff2 Merge pull request #916 from oscarbyrne/fix-swatch-static-paths
Fix: swatch/[name].png.js static path
2024-01-18 05:09:38 +01:00
Oscar Byrne 96c2144857 Use astro Response class for swatch/[name].png.js endpoint 2024-01-17 17:09:11 -08:00
Felix Roos 0655af4d4f add pcurve 2024-01-17 17:35:35 +01:00
Jade (Rose) Rowland 7adfe6f2df fixing edge cases... 2024-01-17 09:45:00 -05:00
Felix Roos 3fc0fdbe62 remove todo 2024-01-16 17:57:52 +01:00
Felix Roos fd73571744 make sure the sustained pitch is the selected note 2024-01-16 17:56:38 +01:00
Felix Roos 80d861c12c Merge pull request #907 from fnordomat/more-vowels
Add more vowel qualities for the vowels function
2024-01-16 08:22:27 +01: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 3506de8d1a vibrato for soundfonts
+ add getVibratoOscillator helper
2024-01-16 00:07:56 +01:00
Felix Roos 32456d6966 pitch envelope for sampler and soundfonts
+ added getPitchEnvelope helper
2024-01-15 23:55:49 +01:00
Felix Roos bdc2af9733 basic pitch envelope 2024-01-15 23:25:22 +01:00
Felix Roos a7daf41934 Merge pull request #911 from tidalcycles/blog
adds a blog
2024-01-14 23:56:36 +01:00
Felix Roos fe6ff2528a Merge pull request #894 from rjulian/feat/add-header-workshop
Remove hideHeader for better mobile UI and consistency
2024-01-14 23:52:41 +01:00
Felix Roos 7ceb8708b0 fix: do not format autogenerated file 2024-01-14 23:47:50 +01:00
Felix Roos 4d22d957a1 add more videos to showcase 2024-01-14 23:45:39 +01:00
Felix Roos 79f1a863b6 add date + author to posts + dedupe headings 2024-01-14 23:39:03 +01:00
Felix Roos 98db3d37ef add anchor links 2024-01-14 23:30:26 +01:00
Felix Roos c02976782b add video component 2024-01-14 23:22:10 +01:00
Felix Roos ceb4376eb8 format autogenerated types 2024-01-14 23:16:44 +01:00
Felix Roos 0566b7bcb3 basic blog with release notes 2024-01-14 23:16:09 +01:00
Jade (Rose) Rowland 6d0ecb9f5e Merge pull request #868 from daslyfe/envelope_improvements
Further Envelope improvements
2024-01-14 16:43:06 -05:00
Felix Roos 0901948564 fanchor default to 0.5 for now 2024-01-14 21:32:38 +01:00
Felix Roos e6d028fd6b soundfont mixing 2024-01-14 16:27:11 +01:00
Alex McLean 945e7082cd Update metadata.mdx 2024-01-14 10:15:40 +00:00
Alex McLean 0d6bdfb514 Link to creative commons chooser 2024-01-14 10:14:43 +00:00
fnordomat 70712bdab6 fix codeformat 2024-01-14 10:19:49 +01:00
Felix Roos 53771d7f95 hotfix: avoid crash for anonymous patterns 2024-01-14 01:50:48 +01:00
Felix Roos 6d32d4024d hotfix: fix built version loading at runtime.. 2024-01-14 01:08:01 +01:00
Felix Roos f387589342 hotfix: fix build error 2024-01-14 00:53:43 +01:00
Felix Roos 47a5080352 Merge pull request #910 from tidalcycles/browse
public sharing
2024-01-14 00:48:03 +01:00
Felix Roos e7e60d3ed8 Merge branch 'main' into pattern_selection 2024-01-14 00:18:57 +01:00
Felix Roos 5314c83534 delete browse page for now 2024-01-14 00:02:27 +01:00
Felix Roos 204c964050 integrate public patterns into patterns tab 2024-01-13 23:53:46 +01:00
Felix Roos 1ede99f71e ask for public sharing (very cheap) + order browse list 2024-01-13 23:12:22 +01:00
Felix Roos 3c1844046a basic browse page 2024-01-13 23:01:16 +01:00
Felix Roos eefa90a2bc fix: synth default envelope 2024-01-13 22:24:59 +01:00
fnordomat efa0737660 added some aliases for the vowels (cosmetics) 2024-01-13 15:11:30 +01:00
fnordomat 6232560273 document extra vowels in quick reference 2024-01-13 11:11:31 +01:00
fnordomat e051d383a0 add 10 new vowel qualities to formant table 2024-01-13 11:11:31 +01:00
Felix Roos 4fb5c1828d i have no clue why this works 2024-01-13 01:16:26 +01:00
Felix Roos 6f3b2fa66b negative fenv values now stay in the same range as positives 2024-01-13 00:26:16 +01:00
Felix Roos f17459fdc3 Merge branch 'main' into envelope_improvements 2024-01-12 19:41:05 +01:00
Felix Roos 86bc28dfb3 add deprecation notes 2024-01-11 20:46:22 +01:00
Felix Roos 1bc0934228 Merge branch 'main' into envelope_improvements 2024-01-10 15:15:22 +01:00
Felix Roos f26fd18c7a use 0.001 as linear mintime as well (to prevent cracks) 2024-01-10 15:02:10 +01:00
Felix Roos c68dba8c21 fix: use end 2024-01-10 14:28:38 +01:00
Felix Roos f99557cc05 fix: fanchor + default fanchor to 0 (breaking change) 2024-01-10 00:08:49 +01:00
Felix Roos f785823e00 add fenv to filter cutoff controls 2024-01-10 00:08:13 +01:00
Felix Roos 62cd2263c2 add silent flag to nanFallback 2024-01-10 00:03:18 +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
Richard Julian 569137eb22 Formatting for passing CI 2024-01-06 12:05:39 -08:00
Richard Julian 4651066472 Remove hideHeader for better mobile UI and consistency 2024-01-06 11:55:00 -08:00
Jade (Rose) Rowland 150a1b8eed restore buffer hold behavior 2024-01-05 17:45:09 -05:00
Jade (Rose) Rowland b2f63897f9 change default FM env to exp because it modulates frequency and sounds way better :) 2024-01-05 01:07:54 -05:00
Jade (Rose) Rowland 2ee392be9b fixed all the things 2024-01-05 01:00:22 -05:00
Felix Roos a2974099c1 add dec synonym for decay 2024-01-03 23:06:23 +01:00
Felix Roos 041a809b07 add ar function 2024-01-03 22:41:03 +01:00
Felix Roos 2ecc6b30aa add ad function 2024-01-03 22:37:35 +01:00
Felix Roos d582bbbb60 fix: format 2024-01-03 22:17:11 +01: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
Jade (Rose) Rowland 83c820a18c prettier again 2024-01-01 18:50:23 -05:00
Jade (Rose) Rowland 3eface706c Merge branch 'tidalcycles:main' into envelope_improvements 2024-01-01 18:47:28 -05:00
Jade (Rose) Rowland 9756d63cf1 prettier 2023-12-31 11:58:06 -05:00
Jade (Rose) Rowland a800b32529 Merge branch 'main' into envelope_improvements 2023-12-31 11:51:49 -05:00
Jade (Rose) Rowland 12451ba5fc Merge branch 'tidalcycles:main' into envelope_improvements 2023-12-29 22:27:52 -05:00
Jade (Rose) Rowland 77fee0b866 fixed popping on font envelope 2023-12-20 17:39:11 -05:00
Jade (Rose) Rowland 5671b40707 account for phase complete 2023-12-20 15:24:14 -05:00
Jade (Rose) Rowland dec039ead3 fixed filter envelope popping... 2023-12-20 13:09:04 -05:00
Jade (Rose) Rowland fb81f6f268 still working on popping issue with firefox 2023-12-20 10:50:06 -05:00
Jade (Rose) Rowland deb973afa5 fixed hold behavior 2023-12-20 10:17:12 -05:00
Jade (Rose) Rowland 2dea3911ba remove unused variable 2023-12-20 09:29:07 -05:00
Jade (Rose) Rowland e0a7fb8290 filter should decay to set frequency 2023-12-20 09:28:40 -05:00
Jade (Rose) Rowland 30e402b9f1 fixed release bug 2023-12-20 01:52:43 -05:00
Jade (Rose) Rowland f6d9ad51c6 trying to fix divergent firefox behavior 2023-12-20 01:44:34 -05:00
Jade (Rose) Rowland 34ba81a841 fixed test complaint 2023-12-20 00:25:56 -05:00
Jade (Rose) Rowland d7fae2620e create release audio param method, make volume envelopes consistant 2023-12-20 00:21:14 -05:00
Jade Rowland fe60f34015 removed accidental file commit 2023-12-17 12:13:05 -05:00
Jade Rowland b5dc65d52a format 2023-12-16 13:36:05 -05:00
Jade Rowland 93a4efcf6d updated fontloader 2023-12-16 13:21:23 -05:00
Jade Rowland 96e0cce2d2 improve response 2023-12-16 00:30:35 -05:00
Jade Rowland 88f677e910 fix envelope behavior 2023-12-15 18:54:04 -05:00
Jade (Rose) Rowland 9f50f6ef0e Merge branch 'tidalcycles:main' into envelope_improvements 2023-12-15 18:27:23 -05:00
Jade Rowland 9663c2ec85 fileter envelopes 2023-12-15 12:16:28 -07:00
Jade Rowland 7da7554493 it works 2023-12-15 11:32:23 -07:00
225 changed files with 9802 additions and 5374 deletions
+1
View File
@@ -10,3 +10,4 @@ paper
pnpm-lock.yaml
pnpm-workspace.yaml
**/dev-dist
website/.astro
+1 -1
View File
@@ -114,7 +114,7 @@ You can run the same check with `pnpm check`
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/<package-name>` to get the local version,
allowing to develop multiple packages at the same time.
## Package Publishing
+2 -1
View File
@@ -2,12 +2,13 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully.
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally
+1 -1
View File
@@ -7,7 +7,7 @@
/>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
const input = document.getElementById('text');
const getEvents = () => {
+1 -1
View File
@@ -8,7 +8,7 @@
/>
<canvas id="canvas"></canvas>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
// this adds all strudel functions to the global scope, to be used by eval
Object.assign(window, strudel);
// setup elements
@@ -6,7 +6,7 @@
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
+20 -13
View File
@@ -16,38 +16,45 @@
</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';
// TODO: refactor to use newer version without controls import
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')
return `samples('github:tidalcycles/dirt-samples')
setcps(1);
stack(
// amen
n("0 1 2 3 4 5 6 7")
+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>
<!--
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/repl@0.9.4"></script>
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
<strudel-editor>
<!--
// @date 23-08-15
+8
View File
@@ -0,0 +1,8 @@
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">PLAY</button>
<script>
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
document.getElementById('play').addEventListener('click', () => s('bd sd').play());
</script>
+9 -10
View File
@@ -1,11 +1,11 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import { drawPianoroll, evalScope } from '@strudel/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel/webaudio';
import { registerSoundfonts } from '@strudel/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
@@ -25,11 +25,10 @@ const editor = new StrudelMirror({
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
+6 -6
View File
@@ -13,11 +13,11 @@
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
}
}
+6 -6
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
@@ -33,8 +33,8 @@ await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tid
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -69,8 +69,8 @@ export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+1 -1
View File
@@ -15,7 +15,7 @@
<script type="module">
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
+5 -10
View File
@@ -1,20 +1,15 @@
import { controls, repl, evalScope } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { repl, evalScope } from '@strudel/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
import tune from './tune.mjs';
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = tune;
initAudioOnFirstClick();
registerSynthSounds();
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal'),
);
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal'));
const { evaluate } = repl({
defaultOutput: webaudioOutput,
+5 -5
View File
@@ -13,10 +13,10 @@
"vite": "^5.0.10"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
export default `await samples('github:tidalcycles/Dirt-Samples/master')
export default `samples('github:tidalcycles/dirt-samples')
setcps(1)
stack(
// amen
n("0 1 2 3 4 5 6 7")
+1 -1
View File
@@ -12,7 +12,7 @@
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
samples('github:tidalcycles/dirt-samples'),
registerSynthSounds(),
]);
+7 -7
View File
@@ -1,5 +1,5 @@
{
"name": "@strudel.cycles/monorepo",
"name": "@strudel/monorepo",
"version": "0.5.0",
"private": true,
"description": "Port of tidalcycles to javascript",
@@ -45,12 +45,12 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/xen": "workspace:*"
},
"devDependencies": {
"dependency-tree": "^10.0.9",
+1 -1
View File
@@ -1,5 +1,5 @@
# Packages
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
+7 -2
View File
@@ -2,7 +2,7 @@ import { closeBrackets } from '@codemirror/autocomplete';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
EditorView,
@@ -12,7 +12,7 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
@@ -24,6 +24,7 @@ import { persistentAtom } from '@nanostores/persistent';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
@@ -37,6 +38,7 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
export const defaultSettings = {
keybindings: 'codemirror',
isBracketMatchingEnabled: false,
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
@@ -290,6 +292,9 @@ export class StrudelMirror {
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
+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 };
}
}
+5 -6
View File
@@ -1,11 +1,10 @@
{
"name": "@strudel/codemirror",
"version": "0.9.0",
"version": "1.0.1",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -41,14 +40,14 @@
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@lezer/highlight": "^1.2.0",
"@nanostores/persistent": "^0.9.1",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@strudel/core": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5",
"@nanostores/persistent": "^0.9.1"
"nanostores": "^0.9.5"
},
"devDependencies": {
"vite": "^5.0.10"
+1 -1
View File
@@ -1,4 +1,4 @@
import { ref, pure } from '@strudel.cycles/core';
import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+4 -4
View File
@@ -1,17 +1,17 @@
# @strudel.cycles/core
# @strudel/core
This package contains the bare essence of strudel.
## Install
```sh
npm i @strudel.cycles/core --save
npm i @strudel/core --save
```
## Example
```js
import { sequence } from '@strudel.cycles/core';
import { sequence } from '@strudel/core';
const pattern = sequence('a', ['b', 'c']);
@@ -33,7 +33,7 @@ b: 3/2 - 7/4
c: 7/4 - 2
```
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [play with @strudel/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
- [open minimal vite example](./examples/vite-vanilla-repl/)
+1 -3
View File
@@ -1,6 +1,4 @@
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
import { Pattern, getDrawContext, silence, register, pure, createParams } from './index.mjs';
let clearColor = '#22222210';
+1456 -1341
View File
File diff suppressed because it is too large Load Diff
+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.endClipped >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
+8 -2
View File
@@ -41,11 +41,17 @@ const _bjork = function (n, x) {
};
export const bjork = function (ons, steps) {
const inverted = ons < 0;
ons = Math.abs(ons);
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
return flatten(result[1][0]).concat(flatten(result[1][1]));
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
if (inverted) {
return p.map((x) => (x === 0 ? 1 : 0));
}
return p;
};
/**
@@ -148,7 +154,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) {
+11 -1
View File
@@ -3,6 +3,7 @@ hap.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from './fraction.mjs';
export class Hap {
/*
@@ -32,7 +33,16 @@ export class Hap {
}
get duration() {
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
let duration;
if (typeof this.value?.duration === 'number') {
duration = Fraction(this.value.duration);
} else {
duration = this.whole.end.sub(this.whole.begin);
}
if (typeof this.value?.clip === 'number') {
return duration.mul(this.value.clip);
}
return duration;
}
get endClipped() {
+5 -4
View File
@@ -4,11 +4,12 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import controls from './controls.mjs';
import * as controls from './controls.mjs'; // legacy
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls };
export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
@@ -30,12 +31,12 @@ export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
export const version = p.version; */
logger('🌀 @strudel.cycles/core loaded 🌀');
logger('🌀 @strudel/core loaded 🌀');
if (globalThis._strudelLoaded) {
console.warn(
`@strudel.cycles/core was loaded more than once...
`@strudel/core was loaded more than once...
This might happen when you have multiple versions of strudel installed.
Please check with "npm ls @strudel.cycles/core".`,
Please check with "npm ls @strudel/core".`,
);
}
globalThis._strudelLoaded = true;
+10
View File
@@ -1,6 +1,16 @@
export const logKey = 'strudel.log';
let debounce = 1000,
lastMessage,
lastTime;
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
return;
}
lastMessage = message;
lastTime = t;
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
+3 -4
View File
@@ -1,12 +1,11 @@
{
"name": "@strudel.cycles/core",
"version": "0.9.0",
"name": "@strudel/core",
"version": "1.0.1",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"test": "vitest run",
+125 -125
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);
@@ -1154,8 +1154,8 @@ export function isPattern(thing) {
/* if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel.cycles/core.
Please check by running "npm ls @strudel.cycles/core".`,
This may happen if you are using multiple versions of @strudel/core.
Please check by running "npm ls @strudel/core".`,
);
console.log(thing);
} */
@@ -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,17 +2196,13 @@ 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)
*/
export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles)));
// sets absolute duration of haps
// TODO - fix
export const duration = register('duration', function (value, pat) {
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
});
export const hsla = register('hsla', (h, s, l, a, pat) => {
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
});
@@ -2221,21 +2235,6 @@ export const velocity = register('velocity', function (velocity, pat) {
return pat.withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
});
/**
*
* Multiplies the hap duration with the given factor.
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
* @name legato
* @memberof Pattern
* @example
* note("c3 eb3 g3 c4").legato("<.25 .5 1 2>")
*/
// TODO - fix
export const legato = register('legato', function (value, pat) {
value = Fraction(value);
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
});
//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of
// objects
@@ -2251,7 +2250,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 +2268,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 +2284,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')
@@ -2302,11 +2301,11 @@ const _loopAt = function (factor, pat, cps = 1) {
* @memberof Pattern
* @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)
* samples('github:tidalcycles/dirt-samples')
* 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>")
* samples('github:tidalcycles/dirt-samples')
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@@ -2331,53 +2330,54 @@ export const slice = register(
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* samples('github:tidalcycles/dirt-samples')
* 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',
})),
),
),
);
+7 -2
View File
@@ -160,8 +160,13 @@ export function pianoroll({
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
foldValues = values.sort((a, b) =>
typeof a === 'number' && typeof b === 'number'
? a - b
: typeof a === 'number'
? 1
: String(a).localeCompare(String(b)),
);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
+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 };
}
+192 -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,188 @@ 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));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).trigzeroJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).trigzeroJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).trigJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).trigJoin();
});
/**
* 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.
* @name inhabit
* @synonyms pickSqueeze
* @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, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).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.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).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 +425,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 +470,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 +512,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 +536,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 +544,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 +562,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 +571,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 +579,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 +601,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);
+5 -5
View File
@@ -4,23 +4,23 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import controls from '../controls.mjs';
import { s } from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
describe('controls', () => {
it('should support controls', () => {
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
expect(s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
});
it('should support compound controls', () => {
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
expect(s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 },
]);
});
it('should support ignore extra elements in compound controls', () => {
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
expect(s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 },
]);
+61 -2
View File
@@ -46,13 +46,13 @@ import {
rev,
time,
run,
pick,
} from '../index.mjs';
import { steady } from '../signal.mjs';
import controls from '../controls.mjs';
import { n, s } from '../controls.mjs';
const { n, s } = controls;
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
@@ -1057,4 +1057,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),
),
);
});
});
});
+1 -2
View File
@@ -6,8 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { describe, it, expect } from 'vitest';
import { map, valued, mul } from '../value.mjs';
import controls from '../controls.mjs';
const { n } = controls;
import { n } from '../controls.mjs';
describe('Value', () => {
it('unionWith', () => {
+8 -1
View File
@@ -86,7 +86,7 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback) {
export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;
@@ -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)]));
}
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+1
View File
@@ -0,0 +1 @@
# @strudel/csound
+2 -2
View File
@@ -1,5 +1,5 @@
import { getFrequency, logger, register } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { getFrequency, logger, register } from '@strudel/core';
import { getAudioContext } from '@strudel/webaudio';
import csd from './project.csd?raw';
// import livecodeOrc from './livecode.orc?raw';
import presetsOrc from './presets.orc?raw';
+6 -6
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/csound",
"version": "0.9.0",
"name": "@strudel/csound",
"version": "1.0.1",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -33,8 +33,8 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+3 -3
View File
@@ -1,16 +1,16 @@
import { Invoke } from './utils.mjs';
import { Pattern, noteToMidi } from '@strudel.cycles/core';
import { Pattern, noteToMidi } from '@strudel/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
return this.onTrigger((time, hap, currentTime, cps) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC';
+1 -1
View File
@@ -1,4 +1,4 @@
import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { parseNumeral, Pattern } from '@strudel/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
+1 -1
View File
@@ -22,7 +22,7 @@
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^1.5.3"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
+3 -3
View File
@@ -1,13 +1,13 @@
# @strudel.cycles/embed
# @strudel/embed
This package contains a embeddable web component for the Strudel REPL.
## Usage
Either install with `npm i @strudel.cycles/embed` or just use a cdn to import the script:
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
```html
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl>
<!--
note(`[[e5 [b4 c5] d5 [c5 b4]]
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/embed",
"version": "0.2.0",
"name": "@strudel/embed",
"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
@@ -27,7 +27,7 @@ npm i @strudel/hydra
Then add the import to your evalScope:
```js
import { evalScope } from '@strudel.cycles/core';
import { evalScope } from '@strudel/core';
evalScope(
import('@strudel/hydra')
+1 -1
View File
@@ -1,4 +1,4 @@
import { getDrawContext } from '@strudel.cycles/core';
import { getDrawContext } from '@strudel/core';
let latestOptions;
+4 -4
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel/hydra",
"version": "0.9.0",
"version": "1.0.1",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"server": "node server.js",
@@ -33,7 +33,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel/core": "workspace:*",
"hydra-synth": "^1.3.29"
},
"devDependencies": {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel.cycles/midi
# @strudel/midi
This package adds midi functionality to strudel Patterns.
## Install
```sh
npm i @strudel.cycles/midi --save
npm i @strudel/midi --save
```
+11 -5
View File
@@ -5,8 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -129,7 +129,7 @@ Pattern.prototype.midi = function (output) {
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
@@ -167,10 +167,16 @@ let listeners = {};
const refs = {};
export async function midin(input) {
if (isPattern(input)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
if (initial || WebMidi.enabled) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
+6 -6
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/midi",
"version": "0.9.0",
"name": "@strudel/midi",
"version": "1.0.1",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -29,8 +29,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"webmidi": "^3.1.8"
},
"devDependencies": {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+4 -4
View File
@@ -1,17 +1,17 @@
# @strudel.cycles/mini
# @strudel/mini
This package contains the mini notation parser and pattern generator.
## Install
```sh
npm i @strudel.cycles/mini --save
npm i @strudel/mini --save
```
## Example
```js
import { mini } from '@strudel.cycles/mini';
import { mini } from '@strudel/mini';
const pattern = mini('a [b c*2]');
@@ -28,7 +28,7 @@ yields:
(7/8 -> 1/1, 7/8 -> 1/1, c)
```
[Play with @strudel.cycles/mini codesandbox](https://codesandbox.io/s/strudel-mini-example-oe9wcu?file=/src/index.js)
[Play with @strudel/mini codesandbox](https://codesandbox.io/s/strudel-mini-example-oe9wcu?file=/src/index.js)
## Mini Notation API
+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]
+4 -1
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
import * as strudel from '@strudel/core';
const randOffset = 0.0003;
@@ -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);
+4 -5
View File
@@ -1,12 +1,11 @@
{
"name": "@strudel.cycles/mini",
"version": "0.9.0",
"name": "@strudel/mini",
"version": "1.0.1",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"test": "vitest run",
@@ -32,7 +31,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*"
"@strudel/core": "workspace:*"
},
"devDependencies": {
"peggy": "^3.0.2",
+15 -1
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel.cycles/core/euclid.mjs';
import '@strudel/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
@@ -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', () => {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+1 -1
View File
@@ -1,4 +1,4 @@
# @strudel.cycles/osc
# @strudel/osc
OSC output for strudel patterns! Currently only tested with super collider / super dirt.
+1 -1
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
import { logger, parseNumeral, Pattern } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
+5 -5
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/osc",
"version": "0.9.0",
"name": "@strudel/osc",
"version": "1.0.1",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"server": "node server.js",
@@ -36,7 +36,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel/core": "workspace:*",
"osc-js": "^2.4.0"
},
"devDependencies": {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'osc.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+2
View File
@@ -1,3 +1,5 @@
# @strudel/repl
The Strudel REPL as a web component.
[Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
+10 -10
View File
@@ -1,8 +1,8 @@
{
"name": "@strudel/repl",
"version": "0.9.4",
"version": "1.0.2",
"description": "Strudel REPL as a Web Component",
"main": "index.mjs",
"module": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
@@ -33,15 +33,15 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/midi": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@strudel/hydra": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.5",
+16 -17
View File
@@ -1,23 +1,22 @@
import { controls, noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel.cycles/core';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio';
import * as core from '@strudel.cycles/core';
import { noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio';
import * as core from '@strudel/core';
export async function prebake() {
const modulesLoading = evalScope(
// import('@strudel.cycles/core'),
// import('@strudel/core'),
core,
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
import('@strudel/codemirror'),
import('@strudel/hydra'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/midi'),
// import('@strudel.cycles/xen'),
// import('@strudel.cycles/serial'),
// import('@strudel.cycles/csound'),
// import('@strudel.cycles/osc'),
controls, // sadly, this cannot be exported from core directly (yet)
import('@strudel/soundfonts'),
import('@strudel/midi'),
// import('@strudel/xen'),
// import('@strudel/serial'),
// import('@strudel/csound'),
// import('@strudel/osc'),
);
// load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
@@ -26,10 +25,10 @@ export async function prebake() {
registerSynthSounds(),
registerZZFXSounds(),
//registerSoundfonts(),
// need dynamic import here, because importing @strudel.cycles/soundfonts fails on server:
// => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically
// need dynamic import here, because importing @strudel/soundfonts fails on server:
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
// seems to be a problem with soundfont2
import('@strudel.cycles/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
+3 -3
View File
@@ -1,6 +1,6 @@
import { getDrawContext, silence } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import { getDrawContext, silence } from '@strudel/core';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
import { prebake } from './prebake.mjs';
+1 -1
View File
@@ -1,3 +1,3 @@
# @strudel.cycles/serial
# @strudel/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
+5 -5
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/serial",
"version": "0.9.0",
"name": "@strudel/serial",
"version": "1.0.1",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -29,7 +29,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*"
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
+1 -1
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, isPattern } from '@strudel.cycles/core';
import { Pattern, isPattern } from '@strudel/core';
var writeMessagers = {};
var choosing = false;
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'serial.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+1
View File
@@ -0,0 +1 @@
# @strudel/soundfonts
+33 -11
View File
@@ -1,5 +1,12 @@
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core';
import {
getAudioContext,
registerSound,
getParamADSR,
getADSRValues,
getPitchEnvelope,
getVibratoOscillator,
} from '@strudel/webaudio';
import gm from './gm.mjs';
let loadCache = {};
@@ -130,24 +137,39 @@ export function registerSoundfonts() {
registerSound(
name,
async (time, value, onended) => {
const [attack, decay, sustain, release] = getADSRValues([
value.attack,
value.decay,
value.sustain,
value.release,
]);
const { duration } = value;
const n = getSoundIndex(value.n, fonts.length);
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
};
const envGain = ctx.createGain();
const node = bufferSource.connect(envGain);
const holdEnd = time + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear');
let envEnd = holdEnd + release + 0.01;
// vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
// pitch envelope
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
bufferSource.stop(envEnd);
const stop = (releaseTime) => {};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
vibratoOscillator?.stop();
node.disconnect();
onended();
};
return { node: envelope, stop };
return { node, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);
+5 -6
View File
@@ -1,11 +1,10 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.9.0",
"name": "@strudel/soundfonts",
"version": "1.0.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -30,8 +29,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
},
+2 -2
View File
@@ -1,5 +1,5 @@
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel/core';
import { getAudioContext, registerSound } from '@strudel/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+2 -2
View File
@@ -19,7 +19,7 @@ import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
samples('github:tidalcycles/dirt-samples'),
registerSynthSounds(),
]);
@@ -148,7 +148,7 @@ The json file is expected to have the same format as described above.
Because it is common to use github for samples, there is a short way to load a sample map from github:
```js
samples('github:tidalcycles/Dirt-Samples/master')
samples('github:tidalcycles/dirt-samples')
```
The format is `github:<user>/<repo>/<branch>`.
+132 -94
View File
@@ -1,5 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs';
import { clamp, nanFallback } from './util.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
@@ -7,78 +7,73 @@ export function gainNode(value) {
return node;
}
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
let phase = begin;
gainNode.gain.setValueAtTime(0, begin);
phase += attack;
gainNode.gain.linearRampToValueAtTime(velocity, phase); // attack
phase += decay;
let sustainLevel = sustain * velocity;
gainNode.gain.linearRampToValueAtTime(sustainLevel, phase); // decay / sustain
// sustain end
return {
node: gainNode,
stop: (t) => {
// to make sure the release won't begin before sustain is reached
phase = Math.max(t, phase);
// see https://github.com/tidalcycles/strudel/issues/522
gainNode.gain.setValueAtTime(sustainLevel, phase);
phase += release;
gainNode.gain.linearRampToValueAtTime(0, phase); // release
return phase;
},
};
};
export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => {
sustain = Math.max(0.001, sustain);
velocity = Math.max(0.001, velocity);
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0.0001, begin);
gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack);
gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay);
return {
node: gainNode,
stop: (t) => {
// similar to getEnvelope, this will glitch if sustain level has not been reached
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
return 0;
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
return (y2 - y1) / (x2 - x1);
};
export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => {
export const getParamADSR = (
param,
attack,
decay,
sustain,
release,
min,
max,
begin,
end,
//exponential works better for frequency modulations (such as filter cutoff) due to human ear perception
curve = 'exponential',
) => {
attack = nanFallback(attack);
decay = nanFallback(decay);
sustain = nanFallback(sustain);
release = nanFallback(release);
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
if (curve === 'exponential') {
min = min === 0 ? 0.001 : min;
max = max === 0 ? 0.001 : max;
}
const range = max - min;
const peak = min + range;
const sustainLevel = min + sustain * range;
const peak = max;
const sustainVal = min + sustain * range;
const duration = end - begin;
const envValAtTime = (time) => {
let val;
if (attack > time) {
let slope = getSlope(min, peak, 0, attack);
val = time * slope + (min > peak ? min : 0);
} else {
val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
}
if (curve === 'exponential') {
val = val || 0.001;
}
return val;
};
param.setValueAtTime(min, begin);
param.linearRampToValueAtTime(peak, begin + attack);
param.linearRampToValueAtTime(sustainLevel, begin + attack + decay);
param.setValueAtTime(sustainLevel, end);
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
if (attack > duration) {
//attack
param[ramp](envValAtTime(duration), end);
} else if (attack + decay > duration) {
//attack
param[ramp](envValAtTime(attack), begin + attack);
//decay
param[ramp](envValAtTime(duration), end);
} else {
//attack
param[ramp](envValAtTime(attack), begin + attack);
//decay
param[ramp](envValAtTime(attack + decay), begin + attack + decay);
//sustain
param.setValueAtTime(sustainVal, end);
}
//release
param[ramp](min, end + release);
};
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
@@ -92,38 +87,44 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
return new DynamicsCompressorNode(ac, options);
}
export function createFilter(
context,
type,
frequency,
Q,
attack,
decay,
sustain,
release,
fenv,
start,
end,
fanchor = 0.5,
) {
// changes the default values of the envelope based on what parameters the user has defined
// so it behaves more like you would expect/familiar as other synthesis tools
// ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc.
export const getADSRValues = (params, curve = 'linear', defaultValues) => {
const envmin = curve === 'exponential' ? 0.001 : 0.001;
const releaseMin = 0.01;
const envmax = 1;
const [a, d, s, r] = params;
if (a == null && d == null && s == null && r == null) {
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
}
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
const filter = context.createBiquadFilter();
filter.type = type;
filter.Q.value = Q;
filter.frequency.value = frequency;
// envelope is active when any of these values is set
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
// Apply ADSR to filter frequency
if (!isNaN(fenv) && fenv !== 0) {
const offset = fenv * fanchor;
const min = clamp(2 ** -offset * frequency, 0, 20000);
const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000);
// console.log('min', min, 'max', max);
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end);
if (hasEnvelope !== undefined) {
fenv = nanFallback(fenv, 1, true);
fanchor = nanFallback(fanchor, 0, true);
const fenvAbs = Math.abs(fenv);
const offset = fenvAbs * fanchor;
let min = clamp(2 ** -offset * frequency, 0, 20000);
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
if (fenv < 0) [min, max] = [max, min];
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end, curve);
return filter;
}
return filter;
}
@@ -148,3 +149,40 @@ export function drywet(dry, wet, wetAmount = 0) {
wet_gain.connect(mix);
return mix;
}
let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
if (!hasEnvelope) {
return;
}
const penv = nanFallback(value.penv, 1, true);
const curve = curves[value.pcurve ?? 0];
let [pattack, pdecay, psustain, prelease] = getADSRValues(
[value.pattack, value.pdecay, value.psustain, value.prelease],
curve,
[0.2, 0.001, 1, 0.001],
);
let panchor = value.panchor ?? psustain;
const cents = penv * 100; // penv is in semitones
const min = 0 - cents * panchor;
const max = cents - cents * panchor;
getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, curve);
}
export function getVibratoOscillator(param, value, t) {
const { vibmod = 0.5, vib } = value;
let vibratoOscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(param);
vibratoOscillator.start(t);
return vibratoOscillator;
}
}
+2 -3
View File
@@ -1,12 +1,11 @@
{
"name": "superdough",
"version": "0.9.12",
"version": "1.0.1",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.cjs",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"directories": {
"example": "examples"
+55 -44
View File
@@ -1,6 +1,6 @@
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
@@ -99,6 +99,27 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
function resolveSpecialPaths(base) {
if (base.startsWith('bubo:')) {
const [_, repo] = base.split(':');
base = `github:Bubobubobubobubo/dough-${repo}`;
}
return base;
}
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
return Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
@@ -108,15 +129,19 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
baseUrl = resolveSpecialPaths(baseUrl);
if (baseUrl.startsWith('github:')) {
baseUrl = githubPath(baseUrl, '');
}
const fullUrl = (v) => baseUrl + v;
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
value = value.map(fullUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
return [note, (typeof samples === 'string' ? [samples] : samples).map(fullUrl)];
}),
);
}
@@ -142,7 +167,7 @@ function getSamplesPrefixHandler(url) {
/**
* Loads a collection of samples to use with `s`
* @example
* samples('github:tidalcycles/Dirt-Samples/master');
* samples('github:tidalcycles/dirt-samples');
* s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples({
@@ -165,18 +190,9 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
if (handler) {
return handler(sampleMap);
}
if (sampleMap.startsWith('bubo:')) {
const [_, repo] = sampleMap.split(':');
sampleMap = `github:Bubobubobubobubo/dough-${repo}`;
}
sampleMap = resolveSpecialPaths(sampleMap);
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
sampleMap = `https://raw.githubusercontent.com/${path}/strudel.json`;
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
@@ -235,7 +251,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = undefined, // if 1, samples will be cut off when the hap ends
clip = undefined, // if set, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
@@ -243,8 +259,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
begin = 0,
loopEnd = 1,
end = 1,
vib,
vibmod = 0.5,
duration,
} = value;
// load sample
if (speed === 0) {
@@ -254,24 +269,15 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
loop = s.startsWith('wt_') ? 1 : value.loop;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// vibrato
let vibratoOscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(bufferSource.detune);
vibratoOscillator.start(0);
}
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -298,26 +304,31 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const envGain = ac.createGain();
const node = bufferSource.connect(envGain);
if (clip == null && loop == null && value.release == null) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
duration = (end - begin) * bufferDuration;
}
let holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
// pitch envelope
getPitchEnvelope(bufferSource.detune, value, t, holdEnd);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
node.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
vibratoOscillator?.stop();
envelope.disconnect();
node.disconnect();
out.disconnect();
onended();
};
const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
releaseTime = t + (end - begin) * bufferDuration;
}
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime, playWholeBuffer) => {};
const handle = { node: out, bufferSource, stop };
// cut groups
+19 -12
View File
@@ -253,6 +253,11 @@ function effectSend(input, effect, wet) {
return send;
}
export function resetGlobalEffects() {
delays = {};
reverbs = {};
}
export const superdough = async (value, deadline, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
@@ -280,26 +285,26 @@ export const superdough = async (value, deadline, hapDuration) => {
// low pass
cutoff,
lpenv,
lpattack = 0.01,
lpdecay = 0.01,
lpsustain = 1,
lprelease = 0.01,
lpattack,
lpdecay,
lpsustain,
lprelease,
resonance = 1,
// high pass
hpenv,
hcutoff,
hpattack = 0.01,
hpdecay = 0.01,
hpsustain = 1,
hprelease = 0.01,
hpattack,
hpdecay,
hpsustain,
hprelease,
hresonance = 1,
// band pass
bpenv,
bandf,
bpattack = 0.01,
bpdecay = 0.01,
bpsustain = 1,
bprelease = 0.01,
bpattack,
bpdecay,
bpsustain,
bprelease,
bandq = 1,
channels = [1, 2],
//phaser
@@ -333,6 +338,7 @@ export const superdough = async (value, deadline, hapDuration) => {
compressorAttack,
compressorRelease,
} = value;
gain = nanFallback(gain, 1);
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
@@ -512,6 +518,7 @@ export const superdough = async (value, deadline, hapDuration) => {
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
return { mute: (dt) => post.gain.linearRampToValueAtTime(0, ac.currentTime + dt) };
};
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
+45 -46
View File
@@ -1,6 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
@@ -29,8 +29,11 @@ export function registerSynthSounds() {
registerSound(
s,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
let sound;
if (waveforms.includes(s)) {
@@ -45,21 +48,24 @@ export function registerSynthSounds() {
// turn down
const g = gainNode(0.3);
// gain envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
const silentAt = releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
stop(silentAt);
},
node,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
@@ -99,28 +105,24 @@ export function waveformN(partials, type) {
}
// expects one of waveforms as s
export function getOscillator(
s,
t,
{
export function getOscillator(s, t, value) {
let {
n: partials,
note,
freq,
vib = 0,
vibmod = 0.5,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
},
) {
duration,
} = value;
let ac = getAudioContext();
let o;
// If no partials are given, use stock waveforms
@@ -148,42 +150,39 @@ export function getOscillator(
o.start(t);
// FM
let stopFm, fmEnvelope;
let stopFm;
let envGain = ac.createGain();
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = t + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
t,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(o.detune);
vibratoOscillator.start(t);
}
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
let noiseMix;
if (noise) {
@@ -199,7 +198,7 @@ export function getOscillator(
o.stop(time);
},
triggerRelease: (time) => {
fmEnvelope?.stop(time);
// envGain?.stop(time);
},
};
}
+2 -2
View File
@@ -54,9 +54,9 @@ export const valueToMidi = (value, fallbackValue) => {
return fallbackValue;
};
export function nanFallback(value, fallback) {
export function nanFallback(value, fallback = 0, silent) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
!silent && logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;
}
return value;
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' })[ext],
},
rollupOptions: {
+31
View File
@@ -5,6 +5,37 @@ export var vowelFormant = {
i: { freqs: [270, 1850, 2900, 3350, 3590], gains: [1, 0.0631, 0.0631, 0.0158, 0.0158], qs: [40, 90, 100, 120, 120] },
o: { freqs: [430, 820, 2700, 3000, 3300], gains: [1, 0.3162, 0.0501, 0.0794, 0.01995], qs: [40, 80, 100, 120, 120] },
u: { freqs: [370, 630, 2750, 3000, 3400], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] },
ae: { freqs: [650, 1515, 2400, 3000, 3350], gains: [1, 0.5, 0.1008, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
aa: { freqs: [560, 900, 2570, 3000, 3300], gains: [1, 0.5, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
oe: { freqs: [500, 1430, 2300, 3000, 3300], gains: [1, 0.2, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] },
ue: { freqs: [250, 1750, 2150, 3200, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] },
y: { freqs: [400, 1460, 2400, 3000, 3300], gains: [1, 0.2, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] },
uh: { freqs: [600, 1250, 2100, 3100, 3500], gains: [1, 0.3, 0.0608, 0.0316, 0.01995], qs: [40, 70, 100, 120, 130] },
un: { freqs: [500, 1240, 2280, 3000, 3500], gains: [1, 0.1, 0.1708, 0.0216, 0.02995], qs: [40, 60, 100, 120, 120] },
en: { freqs: [600, 1480, 2450, 3200, 3300], gains: [1, 0.15, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] },
an: { freqs: [700, 1050, 2500, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] },
on: { freqs: [500, 1080, 2350, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] },
get æ() {
return this.ae;
},
get ø() {
return this.oe;
},
get ɑ() {
return this.aa;
},
get å() {
return this.aa;
},
get ö() {
return this.oe;
},
get ü() {
return this.ue;
},
get ı() {
return this.y;
},
};
if (typeof GainNode !== 'undefined') {
class VowelNode extends GainNode {
+5 -5
View File
@@ -1,18 +1,18 @@
# @strudel.cycles/tonal
# @strudel/tonal
This package adds tonal / harmonic functions to strudel Patterns.
## Install
```sh
npm i @strudel.cycles/tonal --save
npm i @strudel/tonal --save
```
## Example
```js
import { sequence } from '@strudel.cycles/core';
import '@strudel.cycles/tonal';
import { sequence } from '@strudel/core';
import '@strudel/tonal';
const pattern = sequence(0, [1, 2]).scale('C major');
@@ -27,7 +27,7 @@ yields:
(3/4 -> 1/1, 3/4 -> 1/1, E3)
```
[play with @strudel.cycles/tonal codesandbox](https://codesandbox.io/s/strudel-tonal-example-rgc5if?file=/src/index.js)
[play with @strudel/tonal codesandbox](https://codesandbox.io/s/strudel-tonal-example-rgc5if?file=/src/index.js)
## Tonal API
+4 -5
View File
@@ -1,11 +1,10 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.9.0",
"name": "@strudel/tonal",
"version": "1.0.1",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -31,7 +30,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.1.8"
+1 -2
View File
@@ -7,10 +7,9 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, controls, seq } from '@strudel.cycles/core';
import { pure, n, seq } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
const { n } = controls;
describe('tonal', () => {
it('Should run tonal functions ', () => {
+10 -3
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, silence, logger, pure, isNote } from '@strudel.cycles/core';
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale } from './tonleiter.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -103,6 +103,8 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return hap.withValue(() => hap.value + semitones);
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({ ...hap.value, note: Note.simplify(Note.transpose(hap.value.note, interval)) }));
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
@@ -133,6 +135,11 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
@@ -157,9 +164,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,4 +1,4 @@
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core';
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel/core';
import { Interval, Scale } from '@tonaljs/tonal';
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

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