Compare commits

...

371 Commits

Author SHA1 Message Date
Felix Roos 2b8b0154da more complicated test pattern 2024-04-10 00:43:10 +02:00
Felix Roos 2b5c653c56 simplify again 2024-04-10 00:36:47 +02:00
Felix Roos 244d91cc8d take that eslint 2024-04-10 00:32:50 +02:00
Felix Roos 3f368d6438 simplify 2024-04-10 00:22:31 +02:00
Felix Roos 36ee4cfc98 much simpler clock poc 2024-04-09 23:59:00 +02:00
Felix Roos e8ed57f588 Merge pull request #1048 from tidalcycles/transpose-numbers
transpose: support all combinations of numbers and strings for notes and intervals
2024-04-08 17:25:27 +02:00
Alex McLean ebeefc0ac4 More experimental step functions: stepwax, stepwane, steptaper, steptour and taperlist (#1042)
* group tactus-oriented functions
* add stepwax, stepwane, steptaper and steptour
* taperlist
2024-04-08 09:46:18 +01:00
Felix Roos 3d06528393 transpose: support alls combinations of numbers and strings for notes and intervals 2024-04-07 01:25:47 +02:00
Jade (Rose) Rowland 558e9e36ff Merge pull request #1047 from daslyfe/mfix
Midi Time hotfix for scheduler updates
2024-04-06 17:44:57 -04:00
Jade (Rose) Rowland 0a6373f64c adjust latency 2024-04-06 17:40:08 -04:00
Jade (Rose) Rowland 0f24e97855 Merge branch 'mfix' of github.com:daslyfe/strudel into mfix 2024-04-06 17:32:07 -04:00
Jade (Rose) Rowland cbfa09a2da remove unessecary changes 2024-04-06 17:31:56 -04:00
Jade (Rose) Rowland 3805195109 Merge branch 'tidalcycles:main' into mfix 2024-04-06 17:29:37 -04:00
Jade (Rose) Rowland 183d3ea4a1 fix desktop midi 2024-04-06 17:28:30 -04:00
Jade (Rose) Rowland 8e4549d6d3 mfix 2024-04-06 17:07:54 -04:00
Felix Roos 506989b0d7 Merge pull request #1041 from tidalcycles/freq-wheel
pitchwheel visual
2024-04-05 12:48:02 +02:00
Felix Roos 8f475515e9 Merge pull request #1046 from tidalcycles/fix-cyclist
fix cyclist fizzling out
2024-04-05 12:47:19 +02:00
Felix Roos df23ea6cdf fix: cyclist desync 2024-04-05 12:23:04 +02:00
Felix Roos bea25a28cb add superdough warning when scheduling in the past 2024-04-05 12:21:45 +02:00
Felix Roos e242e82b3f ireal voicings add aug alias for + 2024-04-01 23:21:55 +02:00
Felix Roos 6479c60387 tweaks 2024-04-01 23:21:32 +02:00
Felix Roos c7f811e14d fix: ignore haps that error at getFrequency 2024-04-01 17:04:03 +02:00
Felix Roos 539160630d more settings 2024-04-01 15:30:19 +02:00
Felix Roos 8a3e9171a4 basic pitchwheel vis 2024-04-01 15:19:50 +02:00
Felix Roos 010329dcfc fix: proper error log with call stack 2024-04-01 15:19:22 +02:00
Felix Roos 7839d6b4bf Merge pull request #1030 from tidalcycles/forbidden-arithmetic
remove dangerous arithmetic feature
2024-03-30 16:05:59 +01:00
Felix Roos eb3e6d60b9 Merge pull request #1034 from tidalcycles/fix-sampler-paths
Fix sampler paths
2024-03-30 16:05:27 +01:00
Felix Roos fd08b25388 simplify readme 2024-03-30 16:03:43 +01:00
Felix Roos ff923a6b6e remove starting sampler by default 2024-03-30 16:02:02 +01:00
Felix Roos 30a15cb4b7 bump version 2024-03-30 16:00:21 +01:00
Felix Roos 7d8cb59a7a fix: char encoding 2024-03-30 16:00:10 +01:00
Felix Roos 1aca5db447 better warning 2024-03-30 15:05:52 +01:00
Felix Roos 2fbd9a3d54 Merge pull request #1033 from tidalcycles/local-sample-server
local sample server cli
2024-03-30 14:43:08 +01:00
Felix Roos 934a3cdec4 bump 2024-03-30 14:40:59 +01:00
Felix Roos f987633498 fix: lint 2024-03-30 14:40:43 +01:00
Felix Roos 1a209c8b82 doc: options 2024-03-30 14:39:26 +01:00
Felix Roos 3e66e1ee93 add LOG env option 2024-03-30 14:38:23 +01:00
Felix Roos e39cd2c9cd bump 2024-03-30 14:27:38 +01:00
Felix Roos 8a8f6a66a4 improve logging + skip folders that throw 2024-03-30 14:27:18 +01:00
Felix Roos b6171600c2 fix: lint 2024-03-30 14:10:35 +01:00
Felix Roos 3a60f63fd2 improve log 2024-03-30 05:07:43 +01:00
Felix Roos 422a9ad525 fix: lint 2024-03-30 05:00:49 +01:00
Felix Roos 29fe4fd907 make sample server available over local network 2024-03-30 04:59:43 +01:00
Felix Roos 22774596ca run sampler in parallel by default + move samples folder to root 2024-03-30 04:48:55 +01:00
Felix Roos 8f355dcf96 add sampler script + samples folder with README 2024-03-30 04:42:45 +01:00
Felix Roos abe775c3fd fix: lint 2024-03-30 03:26:36 +01:00
Felix Roos 95e0496d51 proper sample server 2024-03-30 03:25:42 +01:00
Felix Roos bd9a80827e clean lockfile 2024-03-30 01:59:30 +01:00
Felix Roos 39204ad94d add simple local sample serving 2024-03-30 01:48:07 +01:00
Felix Roos 1e8a3c2918 Merge pull request #1032 from tidalcycles/font-caching
add font file types to offline cache
2024-03-29 17:14:27 +01:00
Felix Roos 1af55c0d19 add font file types to offline cache 2024-03-29 17:13:01 +01:00
Felix Roos a1e07f3079 Merge pull request #1031 from tidalcycles/close-brackets-setting
add closeBrackets setting
2024-03-29 17:10:16 +01:00
Felix Roos 4e7f5e9cda add closeBrackets setting 2024-03-29 17:07:48 +01:00
Felix Roos 12d8cdff92 remove dangerous arithmetic feature 2024-03-29 16:58:34 +01:00
Alex McLean 9c66d9f1a7 Tactus tidy (#1027)
* preserve tactus across calls to filterValues (fixes tactus for struct)
* timeCat -> timecat (maintaining old name as an alias), beatCat -> stepcat
2024-03-29 13:55:05 +00:00
Felix Roos e2b4f6f5ac Merge pull request #1025 from tidalcycles/sync-toggle
add setting for sync flag
2024-03-28 17:06:44 +01:00
Felix Roos 50ed466895 disable sync by default? 2024-03-28 12:00:12 +01:00
Felix Roos 126556634a add setting for sync flag 2024-03-28 11:49:32 +01:00
Felix Roos aa25265cc2 Merge pull request #1024 from tidalcycles/fix-draw-bugs
better theme integration for visuals + various fixes
2024-03-28 11:37:56 +01:00
Felix Roos 73558eb49c Merge pull request #1023 from tidalcycles/more-fonts
More fonts
2024-03-28 11:33:25 +01:00
Felix Roos cc49e0f78e clean 2024-03-28 11:15:48 +01:00
Felix Roos 6c07feb312 add 3 new fonts 2024-03-28 11:15:23 +01:00
Felix Roos a8c2586da6 fix: lint 2024-03-28 10:23:46 +01:00
Felix Roos c400deac74 visuals now adhere to theme by default 2024-03-28 10:23:07 +01:00
Felix Roos 6a2486d9f2 fix: id widgets by type index to prevent erasing memory in some cases 2024-03-28 09:31:43 +01:00
Felix Roos b5720355f9 fix: allow multiple visuals for the same pattern
+ using .tag function instead of single .id
2024-03-28 09:31:12 +01:00
Felix Roos ab015ff48a remove old .draw, rename .onFrame to .draw
+ breaking: from, to are now called lookbehind, lookahead
+ migrate .pianoroll
2024-03-28 07:35:01 +01:00
Felix Roos eb8ac29136 add memory to onFrame 2024-03-28 07:26:32 +01:00
Felix Roos 14b1a1203f fix: cyclist.now() should return 0 when stopped 2024-03-28 07:25:44 +01:00
Felix Roos 060d2cfe2b another hap time introspection method 2024-03-28 06:50:13 +01:00
Felix Roos 04a0952a22 simplify draw plumbing 2024-03-28 06:25:24 +01:00
Felix Roos 1a0dff4081 add some hap methods for time introspection 2024-03-28 05:47:50 +01:00
Alex McLean 2fd2bdba60 Feature: tactus marking (#1021)
* rename `beat` option to `stackBy` to `repeat`
* fix parse error reporting
* rename `weight` to `tactus` (it might in the end be pulse, step, or tap)
* tactus marking with ^
* and add some tests
2024-03-27 12:06:05 +00:00
Jade (Rose) Rowland 2e8ae38d33 Merge pull request #1020 from daslyfe/hotfix_now_calculation
hotfix for 1017
2024-03-25 01:02:01 -04:00
Jade (Rose) Rowland bbfdd2cc32 working 2024-03-25 00:59:42 -04:00
Felix Roos 4086df95b7 Merge pull request #1015 from ilesinge/document_signals
Document signals
2024-03-24 15:06:33 +01:00
Alexandre Gravel-Raymond aba4e72d29 Fewer undocumented functions 2024-03-24 14:18:25 +01:00
Alexandre Gravel-Raymond 8b96e3a6d1 Fix snapshot undegrade function name 2024-03-24 14:17:48 +01:00
Alexandre Gravel-Raymond 731d545a3f Fix undegrade doc naming 2024-03-24 14:15:20 +01:00
Alexandre Gravel-Raymond e6eb6c19a8 Add random modifiers docs to its own page 2024-03-24 14:15:01 +01:00
Alexandre Gravel-Raymond af35a1af09 Add examples test snapshots 2024-03-24 12:40:53 +01:00
Alexandre Gravel-Raymond fc4c70c94f Fix codestyle 2024-03-24 12:25:54 +01:00
Alexandre Gravel-Raymond 064bff6e40 Document conditional modifiers 2024-03-24 12:22:46 +01:00
Alexandre Gravel-Raymond 69211b8701 Document signals 2024-03-24 12:22:26 +01:00
Felix Roos 33f4f5fccc Merge pull request #1014 from daslyfe/repl_sync_fixes
Repl sync fixes
2024-03-24 10:16:11 +01:00
Jade (Rose) Rowland af740c3abb Merge branch 'tidalcycles:main' into repl_sync_fixes 2024-03-23 16:47:22 -04:00
Jade (Rose) Rowland 8e2407cfa9 cleanup 2024-03-23 16:45:34 -04:00
Jade (Rose) Rowland 86c945114d removed unessecary variable 2024-03-23 16:35:48 -04:00
Jade (Rose) Rowland bd16b403e3 build working properly 2024-03-23 16:28:37 -04:00
Jade (Rose) Rowland f00a08ef9e working 2024-03-23 16:18:16 -04:00
Felix Roos 31067d5cb5 Merge pull request #1004 from tidalcycles/worker-repl
eliminate chromium clock jitter
2024-03-23 20:21:51 +01:00
Jade (Rose) Rowland bc917dcf8b sync false 2024-03-23 14:53:07 -04:00
Jade (Rose) Rowland 619ca5385d sync mostly working 2024-03-23 14:52:44 -04:00
Jade (Rose) Rowland aa36d25b9a still kinda working 2024-03-23 14:45:35 -04:00
Felix Roos fe1fc81f42 Merge pull request #1013 from tidalcycles/undocumented-update
update undocumented script
2024-03-23 15:51:07 +01:00
Felix Roos 34373a9d73 Merge pull request #1012 from tidalcycles/fix-flok-setcps
fix: await injectPatternMethods
2024-03-23 15:26:52 +01:00
Felix Roos 16210e121f fix: await injectPatternMethods 2024-03-23 15:24:49 +01:00
Felix Roos 53af147ede Merge pull request #1010 from tidalcycles/trigzero-restart
rename trig -> reset, trigzero -> restart
2024-03-23 15:18:12 +01:00
Felix Roos fc32981886 update undocumented script 2024-03-23 14:08:02 +01:00
Felix Roos aede6e4b26 Merge pull request #1008 from tidalcycles/inline-punchcard
Inline punchcard
2024-03-23 12:30:03 +01:00
Felix Roos ac36423f85 Merge remote-tracking branch 'origin/main' into inline-punchcard 2024-03-23 12:28:23 +01:00
Felix Roos 4071c1af18 Merge pull request #1007 from tidalcycles/color-in-hap-value
Color in hap value
2024-03-23 12:27:22 +01:00
Felix Roos 49e381f258 make spiral size param 3 in one 2024-03-23 12:26:32 +01:00
Felix Roos 21f5d639a3 fix _pianoroll 2024-03-23 12:20:01 +01:00
Felix Roos 0eccbae048 improve spiral colors 2024-03-23 12:17:25 +01:00
Felix Roos a629c5c931 fix: id at wrong place 2024-03-23 12:14:36 +01:00
Felix Roos f83a156317 Merge branch 'color-in-hap-value' into inline-punchcard 2024-03-23 12:07:11 +01:00
Felix Roos 2857e816d2 add Pattern.id + use it for _punchcard and _spiral 2024-03-23 12:06:31 +01:00
Felix Roos c2ef90d67c rename trig -> reset, trigzero -> restart 2024-03-23 11:22:30 +01:00
Felix Roos 2cc92a2b93 Merge branch 'main' into inline-punchcard 2024-03-23 00:33:33 +01:00
Felix Roos 13d56b6b28 snapshots 2024-03-23 00:28:38 +01:00
Felix Roos d036635915 move color to hap value 2024-03-23 00:20:00 +01:00
Felix Roos 5537652497 Merge pull request #1006 from tidalcycles/update-examples
using strudel in your project guide + cleanup examples
2024-03-22 23:53:51 +01:00
Felix Roos f1d491d52e Merge pull request #1000 from eefano/scale-accidentals
accidentals in scale degrees
2024-03-22 23:41:49 +01:00
Felix Roos d299e76c82 remove embed and repl deps from website 2024-03-22 16:25:10 +01:00
Felix Roos 52d3ee0f85 remove demo + scripts on all doc pages 2024-03-22 16:23:18 +01:00
Felix Roos 4f0f388ca2 a few more words 2024-03-22 16:19:45 +01:00
Felix Roos df03e9a379 finish project start page + link it in main readme 2024-03-22 16:17:09 +01:00
Felix Roos 52a311aae6 add link to StrudelMirror 2024-03-22 15:55:12 +01:00
Felix Roos 6365e1626c begin project-start doc
+ improve embed + repl package doc
2024-03-22 15:52:38 +01:00
Felix Roos 8d96a071d6 document umbrella packages 2024-03-22 14:52:28 +01:00
Felix Roos e8f6124a07 + make cyclist trigger backwards compatible for now
+ add absolute time as new 5th onTrigger param
2024-03-22 01:22:35 +01:00
Felix Roos 14a5e7dcb6 make regular cyclist work in the background
+ use worker-timers in Repl
+ repl / cyclist now accept custom interval functions
2024-03-22 01:01:17 +01:00
Felix Roos 7889017434 delete now obsolete poc 2024-03-22 00:46:08 +01:00
Felix Roos ba8f996103 fix: clock jitter in cyclist 2024-03-22 00:41:30 +01:00
Felix Roos 8d8c843ccf working poc cyclist with cps change 2024-03-22 00:29:25 +01:00
Felix Roos a46bd9b36e Merge pull request #978 from daslyfe/oscillator_enhancements
supersaw oscillator
2024-03-21 22:53:55 +01:00
Felix Roos 96a977990a add missing dep 2024-03-21 22:47:03 +01:00
Felix Roos 41a40c74ef add run instruction 2024-03-21 22:45:11 +01:00
Felix Roos 837dfcaba4 add readme 2024-03-21 22:44:50 +01:00
Felix Roos ac441b4340 add working poc for precise cyclist 2024-03-21 22:41:45 +01:00
Felix Roos 4362cb9317 export createClock from core 2024-03-21 22:39:01 +01:00
Felix Roos dc977b292d more options for zyklus api:
+ allow setting custom interval functions
+ allow disabling phase rounding
2024-03-21 22:38:20 +01:00
Felix Roos 10bd668033 allow absolute time for superdough with prefix = 2024-03-21 22:37:15 +01:00
Felix Roos 5d0abe4151 disable sync 2024-03-21 21:31:02 +01:00
Felix Roos 89ec87d32a Merge branch 'oscillator_enhancements' of https://github.com/daslyfe/strudel into oscillator_enhancements 2024-03-21 21:26:46 +01:00
Felix Roos 1d48d5d5ef halve detune range 2024-03-21 21:26:43 +01:00
Felix Roos 0565558e21 scheduling in a worker, draft 2024-03-21 13:27:53 +01:00
Felix Roos 6320d3c08c Merge pull request #1003 from tidalcycles/cleanup
remove canvas, externalize samples, delete junk
2024-03-21 13:00:04 +01:00
Felix Roos 4511732252 remove canvas dependency 2024-03-21 08:35:44 +01:00
Felix Roos f5c087fc2a outsource piano samples 2024-03-21 07:30:21 +01:00
Felix Roos d2fc784060 delete astro favicon 2024-03-21 07:30:05 +01:00
Felix Roos be2ec969c4 use script tag instead of skypack 2024-03-21 07:20:07 +01:00
Felix Roos 456250a176 delete redundant emu samples 2024-03-21 07:19:47 +01:00
Jade (Rose) Rowland cbe7e69e13 remove unecessary multiplier 2024-03-20 15:02:20 -04:00
Felix Roos d3a17891f8 move rollup plugin to dev dependencies 2024-03-20 17:11:35 +01:00
eefano e32c965a4b accidents in scale grades 2024-03-19 16:04:35 +01:00
Felix Roos 5982e3ebea fix: center supersaw if only 1 voice 2024-03-19 09:00:25 +01:00
Felix Roos ea58238453 Merge branch 'main' into oscillator_enhancements 2024-03-19 08:48:09 +01:00
Felix Roos 1e1dbd6085 add inline _punchcard 2024-03-19 08:44:43 +01:00
Felix Roos 6b98e414c6 make onPaint work with mutiple repls on screen
+ add warning if stock onPaint is not overloaded
2024-03-19 08:44:05 +01:00
Alex McLean 29cb6195b3 Fix pure mini highlight (#994)
* preserve locations for pure values via mininotation

* preserve weight
2024-03-18 10:37:55 +00:00
Felix Roos d99af7c4ad Merge pull request #989 from tidalcycles/claviature
inline viz / widgets package
2024-03-18 07:12:14 +01:00
Jade (Rose) Rowland 48e4051310 remove unessecary math 2024-03-17 13:18:52 -04:00
Felix Roos 95446a76ef breakout widgets package for now 2024-03-17 12:22:24 +01:00
Jade (Rose) Rowland 6d874af45f fixed end time 2024-03-17 01:27:11 -04:00
Jade (Rose) Rowland dad18a24fb disconnect worklet at scheduled time to destroy faster 2024-03-17 01:15:07 -04:00
Felix Roos 8b9a2e1d84 Merge remote-tracking branch 'origin/main' into claviature 2024-03-17 04:49:12 +01:00
Felix Roos dd16a1cbf4 hotfix: disable sync, it somehow breaks playback in the built version
(pnpm build && pnpm preview)
2024-03-17 04:48:25 +01:00
Felix Roos 2900401d3e Merge remote-tracking branch 'origin/main' into claviature 2024-03-17 04:38:05 +01:00
Felix Roos b5312c27dc move stuff for less changes 2024-03-17 04:36:48 +01:00
Felix Roos 4319c43ceb comment out _spiral for now 2024-03-17 04:31:30 +01:00
Felix Roos be77882d70 rename inline functions to match global ones, prefixed with _ 2024-03-17 04:15:59 +01:00
Felix Roos 78f816978c Merge pull request #991 from tidalcycles/labeled-statements
Labeled statements
2024-03-17 04:06:59 +01:00
Felix Roos 076b6f1c82 move canvas functions to codemirror package
+ fix id collisions
2024-03-17 04:00:28 +01:00
Felix Roos 0978fbb15e Merge branch 'labeled-statements' into claviature 2024-03-17 03:19:40 +01:00
Felix Roos 2de18846b0 add comment 2024-03-17 03:17:47 +01:00
Felix Roos a71a167133 fix: enable locations again 2024-03-17 03:16:17 +01:00
Felix Roos 6359bbe139 transpile label statements to p calls 2024-03-17 03:14:05 +01:00
Felix Roos d1c713fa29 make labeled statements look good on custom themes 2024-03-17 02:29:48 +01:00
Felix Roos 16506c5ae2 improve scope memory footprint 2024-03-17 01:58:30 +01:00
Alex McLean 398533877c Beat-oriented functionality (#976)
* annotate pure values with their value, allowing single mininotation values to maintain their labels as pure
* Don't use any 'patternified' arguments if they're all 'pure'
* allow pattern weights (roughly, beats-per-cycle) to be inferred where possible, including from mininotation and across many transformations (e.g. `fast` with a 'pure' factor)
* Add `beatCat`, similar to `timeCat` but funkier
* `silence` has a weight of 1, add alternative `nothing` with a weight of 0, and `gap` function with weight argument
* preserve weight across applicative operations (weight comes with the structure)
* add `stack` alternatives that take advantage of pattern weights to align patterns differently - `stackLeft`, `stackRight`, `stackCentre`, `stackExpand`, with `stackBy` with an argument for patterning the alignment.
2024-03-16 17:24:37 +00:00
Felix Roos cdc200e1da draw straight line when no analyser is defined yet
+ add todo for memory leak
2024-03-16 03:12:00 +01:00
Felix Roos dd78dc9606 fix: animation frame cleanup 2024-03-16 02:42:26 +01:00
Felix Roos ef284e9d13 fix: bugs catched by linter 2024-03-16 02:16:50 +01:00
Felix Roos f4032dad22 inline multichannel scopes 2024-03-16 02:14:24 +01:00
Jade (Rose) Rowland 5d55b9c9ef remove hyperpop test 2024-03-15 11:55:18 -04:00
Felix Roos d9d05e21c0 full size piano by default 2024-03-15 12:24:11 +01:00
Felix Roos 1043baf08a fix: catch errors in pianoroll getValue 2024-03-15 12:24:03 +01:00
Felix Roos dfd22b5e44 fix: claviature height
+ better defaults
+ break out solid web component creation
2024-03-15 11:04:35 +01:00
Felix Roos 4a95bf67da move widget registry to codemirror package
+ add transpiler as dependency to codemirror
2024-03-15 10:00:07 +01:00
Felix Roos 8742d50bad simplify widget creation + fix bugs 2024-03-15 09:50:17 +01:00
Felix Roos 6dce9d5deb support multiple animationFrames
+ break out spiral draw logic
2024-03-15 09:49:00 +01:00
Felix Roos a8712fd8ce remove this mess 2024-03-15 09:48:04 +01:00
Jade (Rose) Rowland 8f291b0f80 fix test again 2024-03-14 22:31:26 -04:00
Jade (Rose) Rowland 7f9cc7cd2d merging main 2024-03-14 22:19:45 -04:00
Jade (Rose) Rowland d102e1dbbc Merge pull request #900 from daslyfe/repl_sync
REPL sync between windows
2024-03-14 20:47:35 -04:00
Felix Roos 83ea3bb138 encapsulate canvas logic 2024-03-15 01:47:35 +01:00
Jade (Rose) Rowland 1f39e1c04d fix tests 2024-03-14 20:41:47 -04:00
Felix Roos 48e0691eec add pianoroll widget 2024-03-15 01:40:21 +01:00
Jade (Rose) Rowland fd946107df use lerp 2024-03-14 20:35:56 -04:00
Felix Roos 094b30cd26 use hap color for claviature 2024-03-15 00:39:31 +01:00
Felix Roos 708675077f rename claviature package to widgets 2024-03-15 00:34:13 +01:00
Felix Roos 82c4926f19 allow any web component to become a widget 2024-03-15 00:27:16 +01:00
Felix Roos 29dab578e7 can now load claviature as a codemirror widget 2024-03-14 23:49:38 +01:00
Felix Roos 1e75f045b7 remove other watch tasks for now 2024-03-14 15:31:50 +01:00
Felix Roos cfdd4ac36c add readme 2024-03-14 15:28:06 +01:00
Felix Roos f2e16f946c add working claviature method 2024-03-14 15:17:19 +01:00
Felix Roos 0a1da6f651 claviature lockfile 2024-03-14 14:20:56 +01:00
Felix Roos 062e45ee6a Merge remote-tracking branch 'origin/main' into claviature 2024-03-14 14:20:45 +01:00
Jade (Rose) Rowland 83145784d7 remove console statement 2024-03-14 00:18:59 -04:00
Jade (Rose) Rowland 486e6457a9 Merge pull request #984 from andresgottlieb/patch-1
Update synths.mdx
2024-03-13 19:20:07 -04:00
Felix Roos 7efea5726b Merge pull request #985 from tidalcycles/share-fix
fix: share now shares what's visible instead of active
2024-03-13 00:36:22 +01:00
Felix Roos 7eb2f7659e fix: share now shares what's visible instead of active 2024-03-12 08:38:11 +01:00
Jade (Rose) Rowland 112ca1875a stop clock if all stopped 2024-03-10 14:35:43 -04:00
Andrés Gottlieb bc641d8e4c Update synths.mdx
Typo fix
2024-03-10 12:49:12 +01:00
Felix Roos d0c68e91dc hotfix: hydra-canvas might already be removed 2024-03-10 02:09:46 +01:00
Felix Roos c373a0473b Merge pull request #967 from tidalcycles/default-dict
use ireal as default voicing dict
2024-03-10 01:18:57 +01:00
Felix Roos 708fea637c fix update migration snippet 2024-03-10 01:10:37 +01:00
Felix Roos c5a4184437 Merge branch 'main' into default-dict 2024-03-10 00:54:13 +01:00
Felix Roos 446cf8c694 Merge pull request #980 from eefano/little-bag-fix
little fix for withVal
2024-03-10 00:50:23 +01:00
Felix Roos 3df776d062 Merge pull request #974 from tidalcycles/velocity-in-value
Velocity in value
2024-03-10 00:46:51 +01:00
Felix Roos ae7807643b Merge remote-tracking branch 'origin/main' into velocity-in-value 2024-03-10 00:44:02 +01:00
Felix Roos 5ebaa3e1ce Merge pull request #983 from tidalcycles/clear-hydra
fix: clear hydra on reset
2024-03-10 00:42:50 +01:00
Felix Roos 4a25eab01a Merge remote-tracking branch 'origin/main' into clear-hydra 2024-03-10 00:40:29 +01:00
Felix Roos e885e05a7a Merge pull request #971 from tidalcycles/draw-package
move canvas related helpers from core to new draw package
2024-03-10 00:38:43 +01:00
Felix Roos 012b19c79e fix: clear hydra on reset 2024-03-10 00:29:52 +01:00
Jade (Rose) Rowland 0875e805b4 Merge pull request #982 from daslyfe/fix_distort_doc
replace shape with distort in learn doc
2024-03-07 23:38:07 -05:00
Jade (Rose) Rowland f1700d5e47 fixed 2024-03-07 23:35:04 -05:00
Jade (Rose) Rowland 30a5176090 fix android support 2024-03-07 23:04:46 -05:00
Felix Roos 43c40f7bb0 no need for publishConfig 2024-03-08 01:07:17 +01:00
Felix Roos 9b3509c062 global watch task 2024-03-08 00:55:19 +01:00
Felix Roos 85fee0cc01 add claviature package 2024-03-08 00:53:48 +01:00
Felix Roos 0f07bfc9d7 rename: canvas -> draw 2024-03-07 13:24:52 +01:00
Felix Roos 75098adbdb allow offsetting onFrame 2024-03-07 13:17:40 +01:00
Felix Roos 8dba865ca7 add note to future 2024-03-07 12:35:39 +01:00
Felix Roos 2fcbffeaf9 add sync flag for neocyclist 2024-03-07 12:14:17 +01:00
Felix Roos 619ffdd5e1 rename zyklus 2024-03-07 12:01:08 +01:00
Felix Roos 1af2da0063 remove changes to prevent conflicts with draw branch 2024-03-07 12:00:27 +01:00
Jade (Rose) Rowland 9d1231ce0a comments 2024-03-06 22:25:31 -05:00
Jade (Rose) Rowland 4d1110b3f3 faster is odd 2024-03-06 20:27:05 -05:00
Jade (Rose) Rowland 6a5bf521c2 unity gain 2024-03-06 16:05:51 -05:00
Jade (Rose) Rowland 3410c332d5 gain adjustment 2024-03-06 10:13:56 -05:00
Alex McLean 8e41332dfa alias - for ~ (#981) 2024-03-06 11:14:48 +00:00
eefano 9f56a335b7 little fix for withVal 2024-03-05 21:41:11 +01:00
Jade (Rose) Rowland 8db8307c9e fix test 2024-03-05 00:48:47 -05:00
Jade (Rose) Rowland 641d9f5ebd Merge branch 'main' into oscillator_enhancements 2024-03-05 00:45:56 -05:00
Jade (Rose) Rowland 05887967e0 added back (possibly unessecary? stop fm 2024-03-05 00:44:45 -05:00
Jade (Rose) Rowland 232b9c13d7 disable lint for global worklet variables 2024-03-05 00:34:59 -05:00
Jade (Rose) Rowland c0b00339c5 implemented modulators 2024-03-05 00:23:19 -05:00
Jade (Rose) Rowland bfcb2070d1 adding modulators 2024-03-04 23:17:58 -05:00
Jade (Rose) Rowland 1e3a23e017 100 2024-03-04 20:37:48 -05:00
Jade (Rose) Rowland 524109056f Merge pull request #963 from daslyfe/worklet_improvements
Worklet Improvents / fixes
2024-03-04 10:04:23 -05:00
Jade (Rose) Rowland c2df8aa6b9 cleaning 2024-03-04 00:43:03 -05:00
Jade (Rose) Rowland 1666b50857 params 2024-03-03 22:19:26 -05:00
Jade (Rose) Rowland e839c271cd fixed output 2024-03-03 22:05:00 -05:00
Jade (Rose) Rowland 823c208c71 cleaning 2024-03-03 21:22:17 -05:00
Jade (Rose) Rowland ee01b4a3a0 working on it 2024-03-03 18:57:30 -05:00
Felix Roos d5ed065011 fix: hydra canvas adjust to screen size
+ simplify hydra init (use getDrawContext)
+ make getDrawContext more versatile
2024-03-03 22:50:51 +01:00
Jade (Rose) Rowland 78a6ec2f98 stereo 2024-03-03 13:16:32 -05:00
Jade (Rose) Rowland 7f60a9e52b yay 2024-03-03 01:23:08 -05:00
Jade (Rose) Rowland 7383dca9ee working 2024-03-03 00:37:33 -05:00
Jade (Rose) Rowland 8a7d72bf24 working 2024-03-02 22:35:26 -05:00
Jade (Rose) Rowland 4af7262f45 fixed performance issue 2024-03-02 01:16:41 -05:00
Jade (Rose) Rowland daa62202fa default values needed in superdough for worklet input 2024-03-01 13:12:52 -05:00
Jade (Rose) Rowland f911fceb9e trying things 2024-03-01 13:04:44 -05:00
Felix Roos e9e5fac610 fix tunes + tests 2024-03-01 18:29:24 +01:00
Felix Roos 5017ac6f74 delete file from wrong branch 2024-03-01 18:01:51 +01:00
Felix Roos 1d92ee76b9 make velocity a regular value prop 2024-03-01 18:00:34 +01:00
Felix Roos 7d01764a30 Merge pull request #973 from tidalcycles/nested-controls
Nested controls
2024-03-01 17:30:19 +01:00
Felix Roos 62d8955da9 onFrame function 2024-03-01 16:46:34 +01:00
Felix Roos 4f94de86db fix: migrate more imports 2024-03-01 13:09:14 +01:00
Jade (Rose) Rowland a6f7c57d19 working 2024-02-29 22:55:56 -05:00
Jade (Rose) Rowland 1690030b1d envelope 2024-02-29 21:07:43 -05:00
Felix Roos bf343eb499 simplify _composeOp 2024-03-01 00:32:45 +01:00
Felix Roos 4f55144232 nested controls poc 2024-03-01 00:31:33 +01:00
Jade (Rose) Rowland c9b18f6fa4 fix test 2024-02-29 18:15:30 -05:00
Jade (Rose) Rowland 416a03aea1 updated params 2024-02-29 18:09:26 -05:00
Jade (Rose) Rowland f62b268f1d playing 2024-02-29 17:53:26 -05:00
Felix Roos 6ce8a3feba move draw logic to separate package 2024-02-29 22:14:53 +01:00
Felix Roos 94dd547a74 canvas package boilerplate 2024-02-29 21:56:31 +01:00
Jade (Rose) Rowland f5c7b58b3a testing 2024-02-29 14:49:08 -05: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
Jade (Rose) Rowland c00ed14bc3 playing 2024-02-29 00:54:24 -05:00
Felix Roos 8f39a5eeac snapshot 2024-02-29 05:02:22 +01:00
Felix Roos 305715277c repl reset default dict
+ add mechanism to get a module by name from dynamic imports
2024-02-29 04:54:03 +01:00
Felix Roos d4eebf1a77 set ireal as default dict
+ add global way to set and reset default dict
2024-02-29 04:53:11 +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
Jade (Rose) Rowland fba2422a64 update test 2024-02-28 17:32:30 -05:00
Jade (Rose) Rowland da674d3e05 updated controls 2024-02-28 17:26:59 -05:00
Jade (Rose) Rowland 57e1c1bd89 Merge branch 'tidalcycles:main' into worklet_improvements 2024-02-28 17:10:09 -05:00
Jade (Rose) Rowland e3463b3c3d merged new controls file 2024-02-28 17:08:58 -05:00
Jade (Rose) Rowland a3898c7589 cleanup 2024-02-28 14:36:50 -05:00
Jade (Rose) Rowland 1221c6144c added distortion effect 2024-02-28 14:31:07 -05:00
Jade (Rose) Rowland c77b0098a7 adjust param range 2024-02-28 13:17:36 -05: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
Jade (Rose) Rowland a227c69408 adjusting params 2024-02-28 10:24:26 -05:00
Jade (Rose) Rowland 972bfa3319 cleaning up 2024-02-28 00:04:12 -05:00
Jade (Rose) Rowland 360b920935 cleaning_up 2024-02-27 23:42:33 -05:00
Jade (Rose) Rowland 1416a0de53 working 2024-02-27 23:38:37 -05: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
Jade (Rose) Rowland e54b2fb207 lower weighted average 2024-02-23 20:13:08 -05: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 d544bf4665 seperated service worker zyklus and cyclist zyklus because of import constraints on service workers 2024-02-22 00:16:36 -05:00
Jade (Rose) Rowland e5570a6017 restore old cyclist 2024-02-21 23:42:19 -05: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 751a825616 fix test 2024-02-19 23:54:15 -05: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
Jade (Rose) Rowland 15df20d22d cleanup 2024-02-08 00:37:16 -05:00
Jade (Rose) Rowland b37c560ea5 importScripts 2024-02-08 00:20:40 -05:00
Jade (Rose) Rowland 5a3272fe29 fix repl merge 2024-02-07 22:23:24 -05:00
Felix Roos 0baa8383f2 fix: test 2024-02-04 23:32:28 +01:00
Jade (Rose) Rowland cea33783a2 remove console statement 2024-02-04 17:10:43 -05:00
Jade (Rose) Rowland 0d3eaf7f9a add a broadcast channel for recieving messages from clock to ensure that they hit at the same time 2024-02-04 17:06:28 -05:00
Jade (Rose) Rowland c761cd54b4 try to fix test again: 2024-02-04 14:32:42 -05:00
Jade (Rose) Rowland a8a055d32c fixing test 2024-02-04 14:25:16 -05:00
Jade (Rose) Rowland cc323d0d61 fixed import 2024-02-04 14:09:19 -05:00
Jade (Rose) Rowland fc87a6776a cleaning up 2024-02-04 14:04:12 -05:00
Jade (Rose) Rowland f5ac57f87b cleaning up 2024-02-04 13:56:46 -05:00
Jade (Rose) Rowland 90624abf2e added worker, cleaned up, added setcps function 2024-02-04 13:54:40 -05:00
Jade (Rose) Rowland e5ec62695c Merge branch 'tidalcycles:main' into repl_sync 2024-02-04 13:29:52 -05:00
Enelg52 6da98a5f31 fix midi issue on firefox and quote error 2024-01-23 16:47:46 +01:00
Jade (Rose) Rowland ccb42c831c cleaning 2024-01-12 18:21:23 -05:00
Jade (Rose) Rowland 2386782280 cleaning... 2024-01-12 00:24:23 -05:00
Jade (Rose) Rowland 4d92922263 fixed animation drops 2024-01-12 00:20:20 -05:00
Jade (Rose) Rowland 721f707c94 working but animation is weird 2024-01-10 23:49:02 -05:00
Jade (Rose) Rowland 0c7b731fa6 latency :( 2024-01-10 20:05:27 -05:00
Jade (Rose) Rowland f1eaa83af5 trying stuff 2024-01-10 14:43:18 -05:00
Jade (Rose) Rowland d329ccc4e3 converting 2024-01-10 01:40:19 -05:00
Jade (Rose) Rowland 19f2434694 workerizing 2024-01-09 22:49:39 -05:00
Jade (Rose) Rowland 0006f6483d playing with workers 2024-01-09 19:39:21 -05:00
Jade (Rose) Rowland 6eec4277c1 playing around 2024-01-08 23:34:12 -05:00
247 changed files with 16125 additions and 12863 deletions
+3
View File
@@ -127,3 +127,6 @@ fabric.properties
.idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains
samples/*
!samples/README.md
+4 -14
View File
@@ -15,25 +15,15 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
pnpm run setup
pnpm run repl
pnpm i
pnpm dev
```
## Using Strudel In Your Project
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel).
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- ... [and there are more](./packages/)
Click on the package names to find out more about each one.
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
## Contributing
+3 -4
View File
@@ -1,10 +1,9 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
<script>
strudel.initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -1,12 +1,12 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
<script>
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)`));
+3 -2
View File
@@ -16,6 +16,7 @@
</div>
<div id="output"></div>
<script type="module">
// 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';
@@ -52,8 +53,8 @@
});
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,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>
+1 -2
View File
@@ -1,6 +1,6 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel/core';
import { drawPianoroll, evalScope } from '@strudel/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
@@ -25,7 +25,6 @@ const editor = new StrudelMirror({
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
+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);
+4 -9
View File
@@ -1,5 +1,5 @@
import { controls, repl, evalScope } from '@strudel/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { repl, evalScope } from '@strudel/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
import tune from './tune.mjs';
@@ -7,14 +7,9 @@ const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = tune;
initAudioOnFirstClick();
registerSynthSounds();
evalScope(
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/webaudio'),
import('@strudel/tonal'),
);
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal'));
const { evaluate } = repl({
defaultOutput: webaudioOutput,
+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 -2
View File
@@ -1,15 +1,20 @@
// this barrel export is currently only used to find undocumented exports
export * from './packages/codemirror/index.mjs';
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
export * from './packages/react/index.mjs';
export * from './packages/repl/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs';
+3 -2
View File
@@ -25,6 +25,7 @@
"format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test",
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
},
"repository": {
@@ -53,10 +54,10 @@
"@strudel/xen": "workspace:*"
},
"devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"canvas": "^2.11.2",
"acorn": "^8.11.3",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",
+37 -17
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,18 +12,22 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
import { Pattern, repl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
@@ -37,6 +41,8 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
export const defaultSettings = {
keybindings: 'codemirror',
isBracketMatchingEnabled: false,
isBracketClosingEnabled: true,
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
@@ -69,9 +75,9 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
...initialSettings,
javascript(),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle),
history(),
EditorView.updateListener.of((v) => onChange(v)),
@@ -123,6 +129,7 @@ export class StrudelMirror {
id,
initialCode = '',
onDraw,
drawContext,
drawTime = [0, 0],
autodraw,
prebake,
@@ -135,23 +142,16 @@ export class StrudelMirror {
this.widgets = [];
this.painters = [];
this.drawTime = drawTime;
this.onDraw = onDraw;
const self = this;
this.drawContext = drawContext;
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
const currentFrame = haps.filter((hap) => hap.isActive(time));
this.highlight(currentFrame, time);
this.onDraw?.(haps, time, currentFrame, this.painters);
this.onDraw(haps, time, this.painters);
}, drawTime);
// this approach does not work with multiple repls on screen
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
@@ -177,6 +177,14 @@ export class StrudelMirror {
beforeEval: async () => {
cleanupDraw();
this.painters = [];
const self = this;
// this is similar to repl.mjs > injectPatternMethods
// maybe there is a solution without prototype hacking, but hey, it works
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
await this.prebaked;
await replOptions?.beforeEval?.();
},
@@ -184,7 +192,10 @@ export class StrudelMirror {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
updateWidgets(this.editor, this.widgets);
const sliders = this.widgets.filter((w) => w.type === 'slider');
updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.adjustDrawTime();
@@ -228,6 +239,9 @@ export class StrudelMirror {
// when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
}
draw(haps, time) {
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
@@ -238,7 +252,7 @@ export class StrudelMirror {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters);
} catch (err) {
console.warn('first frame could not be painted');
}
@@ -290,9 +304,15 @@ export class StrudelMirror {
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setBracketClosingEnabled(enabled) {
this.reconfigureExtension('isBracketClosingEnabled', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
+1 -1
View File
@@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.context.color ?? 'var(--foreground)';
const color = hap.value?.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
+1
View File
@@ -3,3 +3,4 @@ export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';
export * from './widget.mjs';
+2
View File
@@ -45,6 +45,8 @@
"@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
+15 -13
View File
@@ -1,6 +1,6 @@
import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
import { StateEffect } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
@@ -60,19 +60,21 @@ export class SliderWidget extends WidgetType {
}
}
export const setWidgets = StateEffect.define();
export const setSliderWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
export const updateSliderWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
function getSliders(widgetConfigs, view) {
return widgetConfigs
.filter((w) => w.type === 'slider')
.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
@@ -99,8 +101,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
}
}
for (let e of tr.effects) {
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
if (e.is(setSliderWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view));
}
}
});
+2
View File
@@ -37,6 +37,7 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal';
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
@@ -513,6 +514,7 @@ export function activateTheme(name) {
.map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')}
}`;
setTheme(themeSettings);
// tailwind dark mode
if (themeSettings.light) {
document.documentElement.classList.remove('dark');
+1
View File
@@ -18,6 +18,7 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
+1
View File
@@ -15,6 +15,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
+1
View File
@@ -18,6 +18,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
+1
View File
@@ -15,6 +15,7 @@ export default createTheme({
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
+1
View File
@@ -27,6 +27,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: colorB },
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
+1
View File
@@ -14,6 +14,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
+1
View File
@@ -16,6 +16,7 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
+135
View File
@@ -0,0 +1,135 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
import { Pattern } from '@strudel/core';
export const addWidget = StateEffect.define({
map: ({ from, to }, change) => {
return { from: change.mapPos(from), to: change.mapPos(to) };
},
});
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: addWidget.of(widgets) });
};
function getWidgets(widgetConfigs) {
return (
widgetConfigs
// codemirror throws an error if we don't sort
.sort((a, b) => a.to - b.to)
.map((widgetConfig) => {
return Decoration.widget({
widget: new BlockWidget(widgetConfig),
side: 0,
block: true,
}).range(widgetConfig.to);
})
);
}
const widgetField = StateField.define(
/* <DecorationSet> */ {
create() {
return Decoration.none;
},
update(widgets, tr) {
widgets = widgets.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addWidget)) {
try {
widgets = widgets.update({
filter: () => false,
add: getWidgets(e.value),
});
} catch (error) {
console.log('err', error);
}
}
}
return widgets;
},
provide: (f) => EditorView.decorations.from(f),
},
);
const widgetElements = {};
export function setWidget(id, el) {
widgetElements[id] = el;
el.id = id;
}
export class BlockWidget extends WidgetType {
constructor(widgetConfig) {
super();
this.widgetConfig = widgetConfig;
}
eq() {
return true;
}
toDOM() {
const id = getWidgetID(this.widgetConfig);
const el = widgetElements[id];
return el;
}
ignoreEvent(e) {
return true;
}
}
export const widgetPlugin = [widgetField];
// widget implementer API to create a new widget type
export function registerWidget(type, fn) {
registerWidgetType(type);
if (fn) {
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
// fn is expected to create a dom element and call setWidget(id, el);
// fn should also return the pattern
return fn(id, options, this);
};
}
}
// wire up @strudel/draw functions
function getCanvasWidget(id, options = {}) {
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
let canvas = document.getElementById(id) || document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
setWidget(id, canvas);
return canvas;
}
registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
});
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).spiral({ ...options, ctx, id });
});
registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).scope({ ...options, ctx, id });
});
registerWidget('_pitchwheel', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
+168
View File
@@ -0,0 +1,168 @@
// eslint-disable-next-line no-undef
// TODO: swap below line with above one when firefox supports esm imports in service workers
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
// import createClock from './zyklus.mjs';
function getTime() {
const precision = 10 ** 4;
const seconds = performance.now() / 1000;
return Math.round(seconds * precision) / precision;
}
let num_cycles_at_cps_change = 0;
let num_ticks_since_cps_change = 0;
let num_seconds_at_cps_change = 0;
let cps = 0.5;
// {id: {started: boolean}}
const clients = new Map();
const duration = 0.1;
const channel = new BroadcastChannel('strudeltick');
const sendMessage = (type, payload) => {
channel.postMessage({ type, payload });
};
const sendTick = (phase, duration, tick, time) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * cps;
const end = begin + eventLength;
const cycle = begin + secondsSinceLastTick * cps;
sendMessage('tick', {
begin,
end,
cps,
tickdeadline,
num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
cycle,
});
num_ticks_since_cps_change++;
};
//create clock method from zyklus
const clock = createClock(getTime, sendTick, duration);
let started = false;
const startClock = (id) => {
clients.set(id, { started: true });
if (started) {
return;
}
clock.start();
started = true;
};
const stopClock = async (id) => {
clients.set(id, { started: false });
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
//dont stop the clock if other instances are running...
if (!started || otherClientStarted) {
return;
}
clock.stop();
setCycle(0);
started = false;
};
const setCycle = (cycle) => {
num_ticks_since_cps_change = 0;
num_cycles_at_cps_change = cycle;
};
const processMessage = (message) => {
const { type, payload } = message;
switch (type) {
case 'cpschange': {
if (payload.cps !== cps) {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
cps = payload.cps;
num_ticks_since_cps_change = 0;
}
break;
}
case 'setcycle': {
setCycle(payload.cycle);
break;
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
} else {
stopClock(message.id);
}
break;
}
}
};
self.onconnect = function (e) {
// the incoming port
const port = e.ports[0];
port.addEventListener('message', function (e) {
processMessage(e.data);
});
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
};
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
function createClock(
getTime,
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick, t);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
};
let intervalID;
const start = () => {
clear(); // just in case start was called more than once
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
+1524 -1439
View File
File diff suppressed because it is too large Load Diff
+33 -20
View File
@@ -1,5 +1,5 @@
/*
cyclist.mjs - <short description TODO>
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.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/>.
*/
@@ -8,7 +8,7 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
this.started = false;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
@@ -16,41 +16,47 @@ export class Cyclist {
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_since_last_cps_change = 0;
this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
(phase, duration, _, t) => {
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_since_last_cps_change = this.lastEnd;
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
}
this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
try {
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
//convert ticks to cycles, so you can query the pattern for events
const eventLength = duration * this.cps;
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
if (phase < t) {
// avoid querying haps that are in the past anyway
console.log(`skip query: too late`);
return;
}
// query the pattern for events
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;
haps.forEach((hap) => {
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps;
onTrigger?.(hap, deadline, duration, this.cps);
// the following line is dumb and only here for backwards compatibility
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
}
});
} catch (e) {
@@ -59,9 +65,16 @@ export class Cyclist {
}
},
interval, // duration of each cycle
0.1,
0.1,
setInterval,
clearInterval,
);
}
now() {
if (!this.started) {
return 0;
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
}
@@ -71,7 +84,7 @@ export class Cyclist {
}
start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_since_last_cps_change = 0;
this.num_cycles_at_cps_change = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
+7 -1
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;
};
/**
+1
View File
@@ -22,6 +22,7 @@ export const evalScope = async (...args) => {
globalThis[name] = value;
});
});
return modules;
};
function safeEval(str, options = {}) {
+9
View File
@@ -51,6 +51,11 @@ Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other;
};
Fraction.prototype.maximum = function (...others) {
others = others.map((x) => new Fraction(x));
return others.reduce((max, other) => other.max(max), this);
};
Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other;
};
@@ -83,6 +88,10 @@ export const gcd = (...fractions) => {
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
};
export const lcm = (...fractions) => {
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
};
fraction._original = Fraction;
export default fraction;
+36 -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,13 +33,43 @@ 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() {
return this.whole.begin.add(this.duration);
}
isActive(currentTime) {
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
}
isInPast(currentTime) {
return currentTime > this.endClipped;
}
isInNearPast(margin, currentTime) {
return currentTime - margin <= this.endClipped;
}
isInFuture(currentTime) {
return currentTime < this.whole.begin;
}
isInNearFuture(margin, currentTime) {
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
}
isWithinTime(min, max) {
return this.whole.begin <= max && this.endClipped >= min;
}
wholeOrPart() {
return this.whole ? this.whole : this.part;
}
@@ -60,6 +91,10 @@ export class Hap {
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
}
hasTag(tag) {
return this.context.tags?.includes(tag);
}
resolveState(state) {
if (this.stateful && this.hasOnset()) {
console.log('stateful');
+4 -6
View File
@@ -4,11 +4,13 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import controls from './controls.mjs';
import * as controls from './controls.mjs'; // legacy
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls };
export { Fraction, controls, createClock };
export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
@@ -21,10 +23,6 @@ export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
+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(
+147
View File
@@ -0,0 +1,147 @@
/*
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.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 { logger } from './logger.mjs';
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
this.num_cycles_at_cps_change = 0;
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker_time_dif;
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
const maxWeight = 20;
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
if (this.worker_time_dif == null) {
this.worker_time_dif = time_dif;
} else {
const w = 1; //weight of new time diff;
const new_dif =
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
if (new_dif != this.worker_time_dif) {
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
weight = 4;
}
this.worker_time_dif = new_dif;
}
weight = Math.min(weight + 1, maxWeight);
};
const tickCallback = (payload) => {
const {
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
this.cps = cps;
this.cycle = cycle;
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
this.time_at_last_tick_message = this.getTime();
};
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
seconds_at_cps_change +
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime);
}
});
};
// receive messages from worker clock and process them
this.channel.onmessage = (message) => {
if (!this.started) {
return;
}
const { payload, type } = message.data;
switch (type) {
case 'tick': {
tickCallback(payload);
}
}
};
}
sendMessage(type, payload) {
this.worker.port.postMessage({ type, payload, id: this.id });
}
now() {
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
return this.cycle + gap;
}
setCps(cps = 1) {
this.sendMessage('cpschange', { cps });
}
setCycle(cycle) {
this.sendMessage('setcycle', { cycle });
}
setStarted(started) {
this.sendMessage('toggle', { started });
this.started = started;
this.onToggle?.(started);
}
start() {
logger('[cyclist] start');
this.setStarted(true);
}
stop() {
this.worker_time_dif = null;
logger('[cyclist] stop');
this.setStarted(false);
}
setPattern(pat, autostart = false) {
this.pattern = pat;
if (autostart && !this.started) {
this.start();
}
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
}
}
+573 -230
View File
File diff suppressed because it is too large Load Diff
+19 -11
View File
@@ -1,3 +1,4 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
@@ -6,9 +7,7 @@ import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
export function repl({
interval,
defaultOutput,
onSchedulerError,
onEvalError,
beforeEval,
afterEval,
@@ -17,6 +16,9 @@ export function repl({
onToggle,
editPattern,
onUpdateState,
sync = false,
setInterval,
clearInterval,
}) {
const state = {
schedulerError: undefined,
@@ -37,16 +39,20 @@ export function repl({
onUpdateState?.(state);
};
const scheduler = new Cyclist({
interval,
const schedulerOptions = {
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
},
});
setInterval,
clearInterval,
};
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
let pPatterns = {};
let allTransform;
@@ -104,7 +110,7 @@ export function repl({
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
evalScope({
return evalScope({
all,
hush,
cpm,
@@ -121,7 +127,7 @@ export function repl({
}
try {
updateState({ code, pending: true });
injectPatternMethods();
await injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler);
@@ -150,6 +156,7 @@ export function repl({
return pattern;
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
@@ -160,14 +167,15 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
async (hap, deadline, duration, cps, t) => {
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
await defaultOutput(hap, deadline, duration, cps, t);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
+121 -4
View File
@@ -143,7 +143,24 @@ export const rand = signal(timeToRand);
export const rand2 = rand.toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p);
/**
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
*/
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
/**
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @example
* s("hh*10").pan(brand)
*/
export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
@@ -244,9 +261,68 @@ 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).restartJoin();
});
/** * 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).restartJoin();
});
/** * 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).resetJoin();
});
/** * 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).resetJoin();
});
/**
/** * 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}
@@ -257,21 +333,23 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const inhabit = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
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 = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
@@ -336,6 +414,8 @@ export const chooseInWith = (pat, xs) => {
* Chooses randomly from the given list of elements.
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
*/
export const choose = (...xs) => chooseWith(rand, xs);
@@ -362,6 +442,7 @@ Pattern.prototype.choose2 = function (...xs) {
/**
* Picks one of the elements at random each cycle.
* @synonyms randcat
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(8)
@@ -390,10 +471,26 @@ const _wchooseWith = function (pat, ...pairs) {
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
*/
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
* @synonyms wrandcat
* @returns {Pattern}
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor);
@@ -462,6 +559,11 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* @returns Pattern
* @example
* s("hh*8").undegradeBy(0.2)
* @example
* s("hh*10").layer(
* x => x.degradeBy(0.2).pan(0),
* x => x.undegradeBy(0.8).pan(1)
* )
*/
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
@@ -470,6 +572,21 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
);
});
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @name undegrade
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").undegrade()
* @example
* s("hh*10").layer(
* x => x.degrade().pan(0),
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
+19 -5
View File
@@ -4,25 +4,39 @@ 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, pan } from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
import Fraction from '../fraction.mjs';
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 },
]);
});
it('should support nested controls', () => {
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
});
it('preserves tactus of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
});
it('preserves tactus of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
});
it('combines tactus of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
});
});
+60 -15
View File
@@ -51,9 +51,8 @@ import {
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);
@@ -182,18 +181,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trig(20, 30)
.add.reset(20, 30)
.early(2),
sequence(26, 27, 36, 37),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trigzero(20, 30)
.add.restart(20, 30)
.early(2),
sequence(21, 22, 31, 32),
);
@@ -234,18 +233,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trig(20, 30)
.keep.reset(20, 30)
.early(2),
sequence(6, 7, 6, 7),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trigzero(20, 30)
.keep.restart(20, 30)
.early(2),
sequence(1, 2, 1, 2),
);
@@ -280,18 +279,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trig(false, true)
.keepif.reset(false, true)
.early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trigzero(false, true)
.keepif.restart(false, true)
.early(2),
sequence(silence, silence, 1, 2),
);
@@ -605,7 +604,7 @@ describe('Pattern', () => {
});
});
describe('polymeter()', () => {
it('Can layer up cycles, stepwise', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
@@ -614,6 +613,9 @@ describe('Pattern', () => {
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
);
});
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
describe('firstOf()', () => {
@@ -1117,4 +1119,47 @@ describe('Pattern', () => {
);
});
});
describe('tactus', () => {
it('Is correctly preserved/calculated through transformations', () => {
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
});
});
describe('steptaper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
it('can taper backwards', () => {
expect(
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
);
});
});
describe('wax and wane, left', () => {
it('can wax from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
});
it('can wane to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
});
it('can wax from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
});
it('can wane to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
});
});
});
+1 -6
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', () => {
@@ -23,8 +22,4 @@ describe('Value', () => {
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9);
});
it('union bare numbers for numeral props', () => {
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
});
});
-18
View File
@@ -4,19 +4,6 @@ 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 { getTime } from './time.mjs';
function frame(callback) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const animate = (animationTime) => {
callback(animationTime, getTime());
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
export const backgroundImage = function (src, animateOptions = {}) {
const container = document.getElementById('code');
const bg = 'background-image:url(' + src + ');background-size:contain;';
@@ -35,11 +22,6 @@ export const backgroundImage = function (src, animateOptions = {}) {
if (funcOptions.length === 0) {
return;
}
frame((_, t) =>
funcOptions.forEach(([option, value]) => {
handleOption(option, value(t));
}),
);
};
export const cleanupUi = () => {
+20
View File
@@ -323,3 +323,23 @@ export function objectMap(obj, fn) {
}
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
}
// Floating point versions, see Fraction for rational versions
// // greatest common divisor
// export const gcd = function (x, y, ...z) {
// if (!y && z.length > 0) {
// return gcd(x, ...z);
// }
// if (!y) {
// return x;
// }
// return gcd(y, x % y, ...z);
// };
// // lowest common multiple
// export const lcm = function (x, y, ...z) {
// if (z.length == 0) {
// return (x * y) / gcd(x, y);
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
+5 -6
View File
@@ -5,14 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { curry } from './util.mjs';
import { logger } from './logger.mjs';
export function unionWithObj(a, b, func) {
if (typeof b?.value === 'number') {
// https://github.com/tidalcycles/strudel/issues/262
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
b = Object.assign(b, numerals);
delete b.value;
if (b?.value !== undefined && Object.keys(b).length === 1) {
// https://github.com/tidalcycles/strudel/issues/1026
logger(`[warn]: Can't do arithmetic on control pattern.`);
return a;
}
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
+9 -4
View File
@@ -7,6 +7,9 @@ function createClock(
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
setInterval = globalThis.setInterval,
clearInterval = globalThis.clearInterval,
round = true,
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
@@ -22,9 +25,8 @@ function createClock(
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase = round ? Math.round(phase * precision) / precision : phase;
callback(phase, duration, tick, t); // callback has to skip / handle phase < t!
phase += duration; // increment phase by duration
tick++;
}
@@ -35,7 +37,10 @@ function createClock(
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const clear = () => {
intervalID !== undefined && clearInterval(intervalID);
intervalID = undefined;
};
const pause = () => clear();
const stop = () => {
tick = 0;
+3 -1
View File
@@ -152,12 +152,14 @@ export const csoundm = register('csoundm', (instrument, pat) => {
const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity;
// Translate frequency to MIDI key number _without_ rounding.
const C4 = 261.62558;
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
const p4 = octave * 12.0 - 36.0;
// We prefer floating point precision, but over the MIDI range [0, 127].
const p5 = 127 * (hap.context?.velocity ?? 0.9);
const p5 = 127 * velocity;
// The Strudel controls as a string.
const p6 = Object.entries({ ...hap.value, frequency })
.flat()
+7 -5
View File
@@ -6,11 +6,13 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
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);
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow
const latency = 0.034;
const offset = (targetTime - currentTime + latency) * 1000;
velocity = Math.floor(gain * velocity * 100);
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';
+9
View File
@@ -0,0 +1,9 @@
# @strudel/canvas
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
```
@@ -1,13 +1,14 @@
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, silence, register, pure, createParams } from '@strudel/core';
import { getDrawContext } from './draw.mjs';
let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext();
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
let { clientWidth: ww, clientHeight: wh } = ctx.canvas;
ww *= window.devicePixelRatio;
wh *= window.devicePixelRatio;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`;
@@ -1,80 +1,88 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/draw.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
export const getDrawContext = (id = 'test-canvas') => {
export const getDrawContext = (id = 'test-canvas', options) => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth * pixelRatio;
canvas.height = window.innerHeight * pixelRatio;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
pixelated && (canvas.style.imageRendering = 'pixelated');
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth * pixelRatio;
canvas.height = window.innerHeight * pixelRatio;
}, 200);
});
}
return canvas.getContext('2d');
return canvas.getContext(contextType);
};
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
let animationFrames = {};
function stopAnimationFrame(id) {
if (animationFrames[id] !== undefined) {
cancelAnimationFrame(animationFrames[id]);
delete animationFrames[id];
}
}
function stopAllAnimations() {
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
}
let memory = {};
Pattern.prototype.draw = function (fn, options) {
if (typeof window === 'undefined') {
return this;
}
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const ctx = getDrawContext();
let cycle,
events = [];
const animate = (time) => {
const t = getTime();
if (from !== undefined && to !== undefined) {
const currentCycle = Math.floor(t);
if (cycle !== currentCycle) {
cycle = currentCycle;
const begin = currentCycle + from;
const end = currentCycle + to;
setTimeout(() => {
events = this.query(new State(new TimeSpan(begin, end)))
.filter(Boolean)
.filter((event) => event.part.begin.equals(event.whole.begin));
onQuery?.(events);
}, 0);
}
}
callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate);
let { id = 1, lookbehind = 0, lookahead = 0 } = options;
let __t = Math.max(getTime(), 0);
stopAnimationFrame(id);
lookbehind = Math.abs(lookbehind);
// init memory, clear future haps of old pattern
memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t));
let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset());
memory[id] = memory[id].concat(newFuture);
let last;
const animate = () => {
const _t = getTime();
const t = _t + lookahead;
// filter out haps that are too far in the past
memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t));
// begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps)
let begin = Math.max(last || t, t - 1 / 10);
const haps = this.queryArc(begin, t).filter((h) => h.hasOnset());
memory[id] = memory[id].concat(haps);
last = t; // makes sure no haps are missed
fn(memory[id], _t, t, this);
animationFrames[id] = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
animationFrames[id] = requestAnimationFrame(animate);
return this;
};
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
stopAllAnimations();
if (window.strudelScheduler) {
clearInterval(window.strudelScheduler);
}
};
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
Pattern.prototype.onPaint = function () {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work');
return this;
};
@@ -134,7 +142,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;
@@ -175,3 +183,18 @@ export class Drawer {
}
}
}
export function getComputedPropertyValue(name) {
if (typeof window === 'undefined') {
return '#fff';
}
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
let theme = {};
export function getTheme() {
return theme;
}
export function setTheme(_theme) {
theme = _theme;
}
+6
View File
@@ -0,0 +1,6 @@
export * from './animate.mjs';
export * from './color.mjs';
export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@strudel/draw",
"version": "1.0.1",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
@@ -1,10 +1,11 @@
/*
pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/pianoroll.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { Pattern, noteToMidi, freqToMidi } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
@@ -18,7 +19,13 @@ const getValue = (e) => {
}
note = note ?? n;
if (typeof note === 'string') {
return noteToMidi(note);
try {
// TODO: n(run(32)).scale("D:minor") fails when trying to query negative time..
return noteToMidi(note);
} catch (err) {
// console.warn(`error converting note to midi: ${err}`); // this spams to crazy
return 0;
}
}
if (typeof note === 'number') {
return note;
@@ -30,25 +37,24 @@ const getValue = (e) => {
};
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
this.draw(
(ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
(haps, time) => {
pianoroll({
...options,
time: t,
time,
ctx,
haps: haps.filter(inFrame),
haps: haps.filter((hap) => inFrame(hap, time)),
});
},
{
from: from - overscan,
to: to + overscan,
lookbehind: from - overscan,
lookahead: to + overscan,
id,
},
);
return this;
@@ -98,11 +104,8 @@ export function pianoroll({
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
inactive = getTheme().foreground,
active = getTheme().foreground,
background = 'transparent',
smear = 0,
playheadColor = 'white',
@@ -121,12 +124,17 @@ export function pianoroll({
colorizeInactive = 1,
fontFamily,
ctx,
id,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
@@ -181,13 +189,14 @@ export function pianoroll({
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
let color = event.value?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const { velocity = 1, gain = 1 } = event.value || {};
ctx.globalAlpha = velocity * gain;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
@@ -263,8 +272,8 @@ export function getDrawOptions(drawTime, options = {}) {
export const getPunchcardPainter =
(options = {}) =>
(ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
(ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options));
+127
View File
@@ -0,0 +1,127 @@
import { Pattern, midiToFreq, getFrequency } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const c = midiToFreq(36);
const circlePos = (cx, cy, radius, angle) => {
angle = angle * Math.PI * 2;
const x = Math.sin(angle) * radius + cx;
const y = Math.cos(angle) * radius + cy;
return [x, y];
};
const freq2angle = (freq, root) => {
return 0.5 - (Math.log2(freq / root) % 1);
};
export function pitchwheel({
haps,
ctx,
id,
hapcircles = 1,
circle = 0,
edo = 12,
root = c,
thickness = 3,
hapRadius = 6,
mode = 'flake',
margin = 10,
} = {}) {
const connectdots = mode === 'polygon';
const centerlines = mode === 'flake';
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
const size = Math.min(w, h);
const radius = size / 2 - thickness / 2 - hapRadius - margin;
const centerX = w / 2;
const centerY = h / 2;
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.lineWidth = thickness;
if (circle) {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
}
if (edo) {
Array.from({ length: edo }, (_, i) => {
const angle = freq2angle(root * Math.pow(2, i / edo), root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
ctx.beginPath();
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
});
ctx.stroke();
}
let shape = [];
ctx.lineWidth = hapRadius;
haps.forEach((hap) => {
let freq;
try {
freq = getFrequency(hap);
} catch (err) {
return;
}
const angle = freq2angle(freq, root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
shape.push([x, y, angle, hapColor, alpha]);
ctx.beginPath();
if (hapcircles) {
ctx.moveTo(x + hapRadius, y);
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
}
if (centerlines) {
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
}
ctx.stroke();
});
ctx.strokeStyle = color;
ctx.globalAlpha = 1;
if (connectdots && shape.length) {
shape = shape.sort((a, b) => a[2] - b[2]);
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
shape.forEach(([x, y, _, color, alpha]) => {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.lineTo(x, y);
});
ctx.lineTo(shape[0][0], shape[0][1]);
ctx.stroke();
}
return;
}
Pattern.prototype.pitchwheel = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
return this.tag(id).onPaint((_, time, haps) =>
pitchwheel({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
id,
}),
);
};
@@ -1,4 +1,5 @@
import { Pattern } from './index.mjs';
import { Pattern } from '@strudel/core';
import { getTheme } from './draw.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
@@ -19,7 +20,7 @@ function spiralSegment(options) {
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
color = getTheme().foreground,
cap = 'round',
stretch = 1,
fromOpacity = 1,
@@ -49,70 +50,81 @@ function spiralSegment(options) {
ctx.stroke();
}
Pattern.prototype.spiral = function (options = {}) {
const {
function drawSpiral(options) {
let {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadColor = '#ffffff',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
activeColor = getTheme().foreground,
inactiveColor = getTheme().gutterForeground,
colorizeInactive = 0,
fade = true,
// logSpiral = true,
ctx,
time,
haps,
drawTime,
id,
} = options;
function spiral({ ctx, time, haps, drawTime }) {
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,
...settings,
from,
to,
rotate,
color: colorizeInactive || isActive ? color : inactiveColor,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const hapColor = hap.value?.color || activeColor;
const color = colorizeInactive || isActive ? hapColor : inactiveColor;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,
...settings,
from,
to,
rotate,
color,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
}
Pattern.prototype.spiral = function (options = {}) {
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
};
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+51 -20
View File
@@ -2,32 +2,63 @@
This package contains a embeddable web component for the Strudel REPL.
## Usage
## Usage via Script Tag
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
Use this code in any HTML file:
```html
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl>
<!--
note(`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-repl>
```
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<div id="strudel"></div>
<script>
let editor = document.createElement('strudel-repl');
editor.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(editor);
</script>
```
When you're using JSX, you could also use the `code` attribute in your markup:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<strudel-repl code={`
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
`}></strudel-repl>
```
+1 -1
View File
@@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
}
connectedCallback() {
setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
+24 -15
View File
@@ -1,40 +1,49 @@
import { getDrawContext } from '@strudel/core';
import { getDrawContext } from '@strudel/draw';
import { controls } from '@strudel/core';
let latestOptions;
function appendCanvas(c) {
const { canvas: testCanvas } = getDrawContext();
c.canvas.id = 'hydra-canvas';
c.canvas.style.position = 'fixed';
c.canvas.style.top = '0px';
testCanvas.after(c.canvas);
return testCanvas;
}
let hydra;
export async function initHydra(options = {}) {
// reset if options have changed since last init
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
document.getElementById('hydra-canvas').remove();
document.getElementById('hydra-canvas')?.remove();
}
latestOptions = options;
//load and init hydra
if (!document.getElementById('hydra-canvas')) {
console.log('reinit..');
const {
src = 'https://unpkg.com/hydra-synth',
feedStrudel = false,
contextType = 'webgl',
pixelRatio = 1,
pixelated = true,
...hydraConfig
} = { detectAudio: false, ...options };
} = {
detectAudio: false,
...options,
};
const { canvas } = getDrawContext('hydra-canvas', { contextType, pixelRatio, pixelated });
hydraConfig.canvas = canvas;
await import(/* @vite-ignore */ src);
const hydra = new Hydra(hydraConfig);
hydra = new Hydra(hydraConfig);
if (feedStrudel) {
const { canvas } = getDrawContext();
canvas.style.display = 'none';
hydra.synth.s0.init({ src: canvas });
}
appendCanvas(hydra);
}
}
export function clearHydra() {
if (hydra) {
hydra.hush();
}
globalThis.s0?.clear();
document.getElementById('hydra-canvas')?.remove();
globalThis.speed = controls.speed;
globalThis.shape = controls.shape;
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
+1
View File
@@ -34,6 +34,7 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"hydra-synth": "^1.3.29"
},
"devDependencies": {
+16 -9
View File
@@ -112,24 +112,25 @@ Pattern.prototype.midi = function (output) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time, hap, currentTime, cps) => {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
hap.ensureObjectValue();
const offset = (time - currentTime) * 1000;
//magic number to get audio engine to line up, can probably be calculated somehow
const latency = 0.034;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${offset}`;
const timeOffsetString = `+${(targetTime - currentTime + latency) * 1000}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * 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 = (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 +168,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}". ${
+27 -15
View File
@@ -308,11 +308,11 @@ function peg$parse(input, options) {
}
return result;
};
var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); };
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$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 }}};
@@ -1477,24 +1477,36 @@ function peg$parse(input, options) {
}
function peg$parsesequence() {
var s0, s1, s2;
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = [];
s2 = peg$parseslice_with_ops();
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parseslice_with_ops();
}
if (input.charCodeAt(peg$currPos) === 94) {
s1 = peg$c9;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s1 !== peg$FAILED) {
if (s1 === peg$FAILED) {
s1 = null;
}
s2 = [];
s3 = peg$parseslice_with_ops();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseslice_with_ops();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f17(s1);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
s0 = s1;
return s0;
}
@@ -2476,10 +2488,10 @@ function peg$parse(input, options) {
this.location_ = location();
}
var PatternStub = function(source, alignment, seed)
var PatternStub = function(source, alignment, seed, tactus)
{
this.type_ = "pattern";
this.arguments_ = { alignment: alignment };
this.arguments_ = { alignment: alignment, tactus: tactus };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
+5 -5
View File
@@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment, seed)
var PatternStub = function(source, alignment, seed, tactus)
{
this.type_ = "pattern";
this.arguments_ = { alignment: alignment };
this.arguments_ = { alignment: alignment, tactus: tactus };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
@@ -165,8 +165,8 @@ slice_with_ops = s:slice ops:slice_op*
}
// a sequence is a combination of one or more successive slices (as an array)
sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat'); }
sequence = tactus:'^'? s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); }
// a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+
@@ -184,7 +184,7 @@ dot_tail = tail:(dot @sequence)+
// 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 / dot_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
{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?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
+66 -30
View File
@@ -6,6 +6,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/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003;
@@ -88,44 +89,79 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
if (alignment === 'stack') {
return strudel.stack(...children);
}
if (alignment === 'polymeter_slowcat') {
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
return strudel.stack(...aligned);
}
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const with_tactus = children.filter((child) => child.__tactus_source);
let pat;
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
return strudel.stack(...aligned);
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
pat = strudel.stack(...aligned);
break;
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce(
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
strudel.Fraction(0),
);
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
}
if (alignment === 'rand') {
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.__tactus_source = true;
}
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);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
pat.__weight = weightSum;
return pat;
}
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat;
}
case 'element': {
1;
return enter(ast.source_);
}
case 'atom': {
if (ast.source_ === '~') {
if (ast.source_ === '~' || ast.source_ === '-') {
return strudel.silence;
}
if (!ast.location_) {
@@ -161,7 +197,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
};
// takes quoted mini string, returns ast
export const mini2ast = (code, start, userCode) => {
export const mini2ast = (code, start = 0, userCode = code) => {
try {
return krill.parse(code);
} catch (error) {
+13
View File
@@ -6,6 +6,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/core/euclid.mjs';
import { Fraction } from '@strudel/core/index.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
@@ -117,6 +118,9 @@ describe('mini', () => {
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
});
it('supports the - alias for ~', () => {
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
});
it('supports the ? operator', () => {
expect(
mini('a?')
@@ -204,6 +208,15 @@ describe('mini', () => {
it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
});
it('supports ^ tactus marking', () => {
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
});
});
describe('getLeafLocation', () => {
+92 -1
View File
@@ -2,4 +2,95 @@
The Strudel REPL as a web component.
[Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
## Add Script Tag
First place this script tag once in your HTML:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
```
You can also pin the version like this:
```html
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
```
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
## Use Web Component
When you've added the script tag, you can use the `strudel-editor` web component:
```html
<strudel-editor>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-editor>
```
This will load the Strudel REPL using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(repl);
</script>
```
## Interacting with the REPL
If you get a hold of the `strudel-editor` element, you can interact with the strudel REPL from Javascript:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<strudel-editor id="repl">
<!-- ... -->
</strudel-editor>
<script>
const repl = document.getElementById('repl');
console.log(repl.editor);
</script>
```
or
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute('code', `...`);
document.getElementById('strudel').append(repl);
console.log(repl.editor);
</script>
```
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.
+5 -3
View File
@@ -1,10 +1,11 @@
{
"name": "@strudel/repl",
"version": "1.0.1",
"version": "1.0.2",
"description": "Strudel REPL as a Web Component",
"main": "index.mjs",
"module": "index.mjs",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -34,6 +35,7 @@
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
+2 -2
View File
@@ -1,4 +1,4 @@
import { controls, noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
import { noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio';
import * as core from '@strudel/core';
@@ -6,6 +6,7 @@ export async function prebake() {
const modulesLoading = evalScope(
// import('@strudel/core'),
core,
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
@@ -17,7 +18,6 @@ export async function prebake() {
// import('@strudel/serial'),
// import('@strudel/csound'),
// import('@strudel/osc'),
controls, // sadly, this cannot be exported from core directly (yet)
);
// load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
+3 -11
View File
@@ -1,4 +1,5 @@
import { getDrawContext, silence } from '@strudel/core';
import { silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
@@ -40,17 +41,8 @@ if (typeof HTMLElement !== 'undefined') {
initialCode: '// LOADING',
pattern: silence,
drawTime,
onDraw: (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
},
drawContext,
prebake,
afterEval: ({ code }) => {
// window.location.hash = '#' + code2hash(code);
},
onUpdateState: (state) => {
const event = new CustomEvent('update', {
detail: state,
+22
View File
@@ -0,0 +1,22 @@
# @strudel/sampler
This package allows you to serve your samples on disk to the strudel REPL.
```sh
cd ~/your/samples/
npx @strudel/sampler
```
This will run a server on `http://localhost:5432`.
You can now load the samples via:
```js
samples('http://localhost:5432')
```
## Options
```sh
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@strudel/sampler",
"version": "0.0.8",
"description": "",
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bin": "./sample-server.mjs",
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
}
}
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join } from 'path';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) {
let files = [];
const dirents = await readdir(directory, { withFileTypes: true });
for (const dirent of dirents) {
const fullPath = join(directory, dirent.name);
if (dirent.isDirectory()) {
if (dirent.name.startsWith('.')) {
LOG && console.warn(`ignore hidden folder: ${fullPath}`);
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
// const directory = resolve(__dirname, '.');
let files = await getFilesInDirectory(directory);
let banks = {};
files = files.map((url) => {
const [bank] = url.split('/').slice(-2);
banks[bank] = banks[bank] || [];
url = url.replace(directory, '');
banks[bank].push(url);
return url;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
// eslint-disable-next-line
const directory = process.cwd();
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
}
let subpath = decodeURIComponent(req.url);
if (!files.includes(subpath)) {
res.statusCode = 404;
res.end('File not found');
return;
}
const filePath = join(directory, subpath);
const readStream = createReadStream(filePath);
readStream.on('error', (err) => {
res.statusCode = 500;
res.end('Internal server error');
console.error(err);
});
readStream.pipe(res);
});
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0';
let IP;
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((key) => {
networkInterfaces[key].forEach((networkInterface) => {
if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
IP = networkInterface.address;
}
});
});
if (!IP) {
console.error("Unable to determine server's IP address.");
// eslint-disable-next-line
process.exit(1);
}
server.listen(PORT, IP_ADDRESS, () => {
console.log(`@strudel/sampler is now serving audio files from:
${directory}
To use them in the Strudel REPL, run:
samples('http://localhost:${PORT}')
Or on a machine in the same network:
samples('http://${IP}:${PORT}')
`);
});
+3 -3
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(),
]);
@@ -65,7 +65,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `bandf`: band pass filter cutoff
- `bandq`: band pass filter resonance
- `crush`: amplitude bit crusher using given number of bits
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `distort`: distortion effect. might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right)
- `phaser`: sets the speed of the modulation
- `phaserdepth`: the amount the signal is affected by the phaser effect.
@@ -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>`.
+73
View File
@@ -186,3 +186,76 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator;
}
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function applyFM(param, value, begin) {
const {
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let modulator;
let stop = () => {};
if (fmModulationIndex) {
const ac = getAudioContext();
const envGain = ac.createGain();
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = begin + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(param);
}
}
return { stop };
}
+35 -16
View File
@@ -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,12 @@ 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('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
@@ -235,7 +254,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
+40 -23
View File
@@ -50,8 +50,8 @@ function loadWorklets() {
return workletsLoading;
}
function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor);
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
});
@@ -215,35 +215,35 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
return reverbs[orbit];
}
export let analyser, analyserData /* s = {} */;
export let analysers = {},
analysersData = {};
export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analyser /*s [orbit] */) {
export function getAnalyserById(id, fftSize = 1024) {
if (!analysers[id]) {
// make sure this doesn't happen too often as it piles up garbage
const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize;
// getDestination().connect(analyserNode);
analyser /* s[orbit] */ = analyserNode;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
analysers[id] = analyserNode;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
}
if (analyser /* s[orbit] */.fftSize !== fftSize) {
analyser /* s[orbit] */.fftSize = fftSize;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
if (analysers[id].fftSize !== fftSize) {
analysers[id].fftSize = fftSize;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
}
return analyser /* s[orbit] */;
return analysers[id];
}
export function getAnalyzerData(type = 'time') {
export function getAnalyzerData(type = 'time', id = 1) {
const getter = {
time: () => analyser?.getFloatTimeDomainData(analyserData),
frequency: () => analyser?.getFloatFrequencyData(analyserData),
time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]),
frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]),
}[type];
if (!getter) {
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
}
getter();
return analyserData;
return analysersData[id];
}
function effectSend(input, effect, wet) {
@@ -253,7 +253,14 @@ function effectSend(input, effect, wet) {
return send;
}
export const superdough = async (value, deadline, hapDuration) => {
export function resetGlobalEffects() {
delays = {};
reverbs = {};
analysers = {};
analysersData = {};
}
export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
@@ -265,7 +272,13 @@ export const superdough = async (value, deadline, hapDuration) => {
// duration is passed as value too..
value.duration = hapDuration;
// calculate absolute time
let t = ac.currentTime + deadline;
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
if (t < ac.currentTime) {
console.warn(
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
);
return;
}
// destructure
let {
s = 'triangle',
@@ -311,6 +324,9 @@ export const superdough = async (value, deadline, hapDuration) => {
coarse,
crush,
shape,
shapevol = 1,
distort,
distortvol = 1,
pan,
vowel,
delay = 0,
@@ -339,7 +355,7 @@ export const superdough = async (value, deadline, hapDuration) => {
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
gain *= velocity; // legacy fix for velocity
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
@@ -452,7 +468,8 @@ export const superdough = async (value, deadline, hapDuration) => {
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
compressorThreshold !== undefined &&
chain.push(
@@ -503,8 +520,8 @@ export const superdough = async (value, deadline, hapDuration) => {
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, analyze);
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
}
// connect chain elements together
+135 -88
View File
@@ -1,31 +1,138 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getWorklet } from './superdough.mjs';
import {
applyFM,
gainNode,
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
const getFrequencyFromValue = (value) => {
let { note, freq } = value;
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = osc.frequency.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
[...waveforms, ...noises].forEach((s) => {
[...waveforms].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
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 = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
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,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
);
});
registerSound(
'supersaw',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
ac,
() => {
o.disconnect();
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
};
},
{ prebake: true, type: 'synth' },
);
[...noises].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
@@ -36,12 +143,9 @@ export function registerSynthSounds() {
);
let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
let { density } = value;
sound = getNoiseOscillator(s, t, density);
}
let { density } = value;
sound = getNoiseOscillator(s, t, density);
let { node: o, stop, triggerRelease } = sound;
@@ -106,24 +210,7 @@ export function waveformN(partials, type) {
// expects one of waveforms as s
export function getOscillator(s, t, value) {
let {
n: partials,
note,
freq,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let ac = getAudioContext();
let { n: partials, duration, noise = 0 } = value;
let o;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
@@ -134,55 +221,15 @@ export function getOscillator(s, t, value) {
else {
o = waveformN(partials, s);
}
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency
o.frequency.value = Number(freq);
o.frequency.value = getFrequencyFromValue(value);
o.start(t);
// FM
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 {
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 = getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix;
if (noise) {
@@ -192,9 +239,9 @@ export function getOscillator(s, t, value) {
return {
node: noiseMix?.node || o,
stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
o.stop(time);
},
triggerRelease: (time) => {
+218 -50
View File
@@ -1,6 +1,5 @@
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
// all the credit goes to dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// <3
class CoarseProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
@@ -9,28 +8,27 @@ class CoarseProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const coarse = parameters.coarse;
const blockSize = 128;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
output[0][0] = input[0][0];
for (let n = 1; n < blockSize; n++) {
for (let o = 0; o < output.length; o++) {
output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1];
}
let coarse = parameters.coarse[0] ?? 0;
coarse = Math.max(1, coarse);
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1];
}
}
return this.notStarted || hasInput;
return true;
}
}
registerProcessor('coarse-processor', CoarseProcessor);
class CrushProcessor extends AudioWorkletProcessor {
@@ -40,69 +38,239 @@ class CrushProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const crush = parameters.crush;
const blockSize = 128;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
if (crush.length === 1) {
const x = Math.pow(2, crush[0] - 1);
for (let n = 0; n < blockSize; n++) {
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
} else {
for (let n = 0; n < blockSize; n++) {
let x = Math.pow(2, crush[n] - 1);
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
let crush = parameters.crush[0] ?? 8;
crush = Math.max(1, crush);
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
const x = Math.pow(2, crush - 1);
output[i][n] = Math.round(input[i][n] * x) / x;
}
}
return this.notStarted || hasInput;
return true;
}
}
registerProcessor('crush-processor', CrushProcessor);
class ShapeProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{ name: 'shape', defaultValue: 0 }];
return [
{ name: 'shape', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
}
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const shape0 = parameters.shape[0];
const shape1 = shape0 < 1 ? shape0 : 1.0 - 4e-10;
const shape = (2.0 * shape1) / (1.0 - shape1);
const blockSize = 128;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
for (let n = 0; n < blockSize; n++) {
const value = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n]));
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
let shape = parameters.shape[0];
shape = shape < 1 ? shape : 1.0 - 4e-10;
shape = (2.0 * shape) / (1.0 - shape);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
}
}
registerProcessor('shape-processor', ShapeProcessor);
class DistortProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'distort', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
}
constructor() {
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const blockSize = 128;
const shape = Math.expm1(parameters.distort[0]);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
}
}
registerProcessor('distort-processor', DistortProcessor);
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
const polyBlep = (phase, dt) => {
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1;
}
// 0 otherwise
else {
return 0;
}
};
const saw = (phase, dt) => {
const v = 2 * phase - 1;
return v - polyBlep(phase, dt);
};
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
min: 0,
max: 1,
},
{
name: 'freqspread',
defaultValue: 0.2,
min: 0,
},
{
name: 'detune',
defaultValue: 0,
min: 0,
},
{
name: 'voices',
defaultValue: 5,
min: 1,
},
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
this.phase[n] = this.phase[n] ?? Math.random();
const v = saw(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
}
}
return this.notStarted || hasInput;
return true;
}
}
registerProcessor('shape-processor', ShapeProcessor);
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
+2
View File
@@ -5,3 +5,5 @@ export * from './tonal.mjs';
export * from './voicings.mjs';
import './ireal.mjs';
export const packageName = '@strudel/tonal';
+33 -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/core';
import { pure, n, seq, note } 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 ', () => {
@@ -45,4 +44,36 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(40, 40, 40)
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(40, 40, 40)
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note('c', 'c', 'c')
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note('c', 'c', 'c')
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
+54 -11
View File
@@ -96,16 +96,37 @@ function scaleOffset(scale, offset, note) {
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
if (typeof hap.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return hap.withValue(() => hap.value + semitones);
const note = hap.value.note ?? hap.value;
if (typeof note === 'number') {
// note is a number, so just add the number semitones of the interval
let semitones;
if (typeof intervalOrSemitones === 'number') {
semitones = intervalOrSemitones;
} else if (typeof intervalOrSemitones === 'string') {
semitones = Interval.semitones(intervalOrSemitones) || 0;
}
const targetNote = note + semitones;
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
}
if (typeof note !== 'string' || !isNote(note)) {
logger(`[tonal] transpose: not a note "${note}"`, 'warning');
return hap;
}
// note is a string, so we might be able to preserve harmonics if interval is a string as well
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones);
// 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)));
// TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
});
});
@@ -133,6 +154,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');
}
@@ -179,18 +205,35 @@ export const scale = register('scale', function (scale, pat) {
// legacy..
return pure(step);
}
const asNumber = Number(step);
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
logger(`[tonal] invalid scale step "${step}", expected number`, 'error');
return silence;
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
try {
let note;
if (value.anchor) {
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
+17 -2
View File
@@ -77,8 +77,13 @@ export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'], mode: 'below', anchor: 'a4' },
triads: { dictionary: triads, mode: 'below', anchor: 'a4' },
guidetones: { dictionary: guidetones, mode: 'above', anchor: 'a4' },
default: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
legacy: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
};
let defaultDict = 'ireal';
export const setDefaultVoicings = (dict) => (defaultDict = dict);
// e.g. typeof setDefaultVoicings !== 'undefined' && setDefaultVoicings('legacy');
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
/**
@@ -193,7 +198,7 @@ export const voicing = register('voicing', function (pat) {
.fmap((value) => {
// destructure voicing controls out
value = typeof value === 'string' ? { chord: value } : value;
let { dictionary = 'default', chord, anchor, offset, mode, n, octaves, ...rest } = value;
let { dictionary = defaultDict, chord, anchor, offset, mode, n, octaves, ...rest } = value;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
try {
@@ -230,7 +235,17 @@ Object.keys(simple).forEach((symbol) => {
let alias = symbol.replace('^', 'M');
voicingAlias(symbol, alias, [complex, simple]);
}
// add aliases for "+" === "aug"
if (symbol.includes('+')) {
let alias = symbol.replace('+', 'aug');
voicingAlias(symbol, alias, [complex, simple]);
}
});
registerVoicings('ireal', simple);
registerVoicings('ireal-ext', complex);
export function resetVoicings() {
lastVoicing = undefined;
setDefaultVoicings('ireal');
}
@@ -23,6 +23,9 @@ describe('transpiler', () => {
it('supports top level await', () => {
expect(transpiler("await samples('xxx');", simple).output).toEqual("await samples('xxx');");
});
it('adds await to bare samples call', () => {
expect(transpiler("samples('xxx');", simple).output).toEqual("await samples('xxx');");
});
/* it('parses dynamic imports', () => {
expect(
transpiler("const { default: foo } = await import('https://bar.com/foo.js');", {
+96 -8
View File
@@ -1,8 +1,12 @@
import escodegen from 'escodegen';
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import { isNoteWithOctave } from '@strudel/core';
import { getLeafLocations } from '@strudel/mini';
import { parse } from 'acorn';
import escodegen from 'escodegen';
import { walk } from 'estree-walker';
let widgetMethods = [];
export function registerWidgetType(type) {
widgetMethods.push(type);
}
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
@@ -35,7 +39,7 @@ export function transpiler(input, options = {}) {
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
if (isWidgetFunction(node)) {
if (isSliderFunction(node)) {
emitWidgets &&
widgets.push({
from: node.arguments[0].start,
@@ -44,8 +48,26 @@ export function transpiler(input, options = {}) {
min: node.arguments[1]?.value ?? 0,
max: node.arguments[2]?.value ?? 1,
step: node.arguments[3]?.value,
type: 'slider',
});
return this.replace(widgetWithLocation(node));
return this.replace(sliderWithLocation(node));
}
if (isWidgetMethod(node)) {
const type = node.callee.property.name;
const index = widgets.filter((w) => w.type === type).length;
const widgetConfig = {
to: node.end,
index,
type,
};
emitWidgets && widgets.push(widgetConfig);
return this.replace(widgetWithLocation(node, widgetConfig));
}
if (isBareSamplesCall(node, parent)) {
return this.replace(withAwait(node));
}
if (isLabelStatement(node)) {
return this.replace(labelToP(node));
}
},
leave(node, parent, prop, index) {},
@@ -103,11 +125,15 @@ function miniWithLocation(value, node) {
// these functions are connected to @strudel/codemirror -> slider.mjs
// maybe someday there will be pluggable transpiler functions, then move this there
function isWidgetFunction(node) {
function isSliderFunction(node) {
return node.type === 'CallExpression' && node.callee.name === 'slider';
}
function widgetWithLocation(node) {
function isWidgetMethod(node) {
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
}
function sliderWithLocation(node) {
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
@@ -119,3 +145,65 @@ function widgetWithLocation(node) {
node.callee.name = 'sliderWithID';
return node;
}
export function getWidgetID(widgetConfig) {
// the widget id is used as id for the dom element + as key for eventual resources
// for example, for each scope widget, a new analyser + buffer (large) is created
// that means, if we use the index index of line position as id, less garbage is generated
// return `widget_${widgetConfig.to}`; // more gargabe
//return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
return `widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
}
function widgetWithLocation(node, widgetConfig) {
const id = getWidgetID(widgetConfig);
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
type: 'Literal',
value: id,
raw: id,
});
return node;
}
function isBareSamplesCall(node, parent) {
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
}
function withAwait(node) {
return {
type: 'AwaitExpression',
argument: node,
};
}
function isLabelStatement(node) {
return node.type === 'LabeledStatement';
}
// converts label expressions to p calls: "x: y" to "y.p('x')"
// see https://github.com/tidalcycles/strudel/issues/990
function labelToP(node) {
return {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: node.body.expression,
property: {
type: 'Identifier',
name: 'p',
},
},
arguments: [
{
type: 'Literal',
value: node.label.name,
raw: `'${node.label.name}'`,
},
],
},
};
}
+6 -9
View File
@@ -7,20 +7,17 @@ This package provides an easy to use bundle of multiple strudel packages for the
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
<script>
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').jux(rev).play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
@@ -43,7 +40,7 @@ By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
document.getElementById('play').addEventListener('click',
@@ -79,4 +76,4 @@ There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
Check out the examples folder for more examples, both using plain html and vite!
+6 -4
View File
@@ -1,10 +1,11 @@
{
"name": "@strudel/web",
"version": "1.0.1",
"version": "1.0.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"module": "web.mjs",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -39,6 +40,7 @@
"@strudel/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^5.0.10",
"@rollup/plugin-replace": "^5.0.5"
}
}
+11 -3
View File
@@ -1,6 +1,7 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
import replace from '@rollup/plugin-replace';
// https://vitejs.dev/config/
export default defineConfig({
@@ -8,11 +9,18 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
name: 'strudel',
formats: ['es', 'iife'],
fileName: (ext) => ({ es: 'index.mjs', iife: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
// external: [...Object.keys(dependencies)],
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: true,
}),
],
},
target: 'esnext',
},
+1 -2
View File
@@ -5,7 +5,7 @@ export * from '@strudel/transpiler';
export * from '@strudel/mini';
export * from '@strudel/tonal';
export * from '@strudel/webaudio';
import { Pattern, evalScope, controls } from '@strudel/core';
import { Pattern, evalScope } from '@strudel/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel/webaudio';
// import { registerSoundfonts } from '@strudel/soundfonts';
import { evaluate as _evaluate } from '@strudel/transpiler';
@@ -15,7 +15,6 @@ import { miniAllStrings } from '@strudel/mini';
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
+1 -2
View File
@@ -12,9 +12,8 @@ npm i @strudel/webaudio --save
## Example
```js
import { repl, controls } from "@strudel/core";
import { repl, note } from "@strudel/core";
import { initAudioOnFirstClick, getAudioContext, webaudioOutput } from "@strudel/webaudio";
const { note } = controls;
initAudioOnFirstClick();
const ctx = getAudioContext();
+1
View File
@@ -34,6 +34,7 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
},
"devDependencies": {
+56 -20
View File
@@ -1,18 +1,37 @@
import { Pattern, getDrawContext, clamp } from '@strudel/core';
import { analyser, getAnalyzerData } from 'superdough';
import { Pattern, clamp } from '@strudel/core';
import { getDrawContext, getTheme } from '@strudel/draw';
import { analysers, getAnalyzerData } from 'superdough';
export function drawTimeScope(
analyser,
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {},
{
align = true,
color = 'white',
thickness = 3,
scale = 0.25,
pos = 0.75,
trigger = 0,
ctx = getDrawContext(),
id = 1,
} = {},
) {
const ctx = getDrawContext();
const dataArray = getAnalyzerData('time');
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
let canvas = ctx.canvas;
if (!analyser) {
// if analyser is undefined, draw straight line
// it may be undefined when no sound has been played yet
ctx.beginPath();
let y = pos * canvas.height;
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
return;
}
const dataArray = getAnalyzerData('time', id);
ctx.beginPath();
let canvas = ctx.canvas;
const bufferSize = analyser.frequencyBinCount;
let triggerIndex = align
@@ -38,10 +57,17 @@ export function drawTimeScope(
export function drawFrequencyScope(
analyser,
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {},
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {},
) {
const dataArray = getAnalyzerData('frequency');
const ctx = getDrawContext();
if (!analyser) {
ctx.beginPath();
let y = pos * canvas.height;
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
return;
}
const dataArray = getAnalyzerData('frequency', id);
const canvas = ctx.canvas;
ctx.fillStyle = color;
@@ -60,8 +86,7 @@ export function drawFrequencyScope(
}
}
function clearScreen(smear = 0, smearRGB = `0,0,0`) {
const ctx = getDrawContext();
function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
if (!smear) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
} else {
@@ -83,10 +108,14 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`) {
* s("sawtooth").fscope()
*/
Pattern.prototype.fscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawFrequencyScope(analyser, config);
});
let id = config.id ?? 1;
return this.analyze(id).draw(
() => {
clearScreen(config.smear, '0,0,0', config.ctx);
analysers[id] && drawFrequencyScope(analysers[id], config);
},
{ id },
);
};
/**
@@ -103,11 +132,18 @@ Pattern.prototype.fscope = function (config = {}) {
* @example
* s("sawtooth").scope()
*/
let latestColor = {};
Pattern.prototype.tscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawTimeScope(analyser, config);
});
let id = config.id ?? 1;
return this.analyze(id).draw(
(haps) => {
config.color = haps[0]?.value?.color || getTheme().foreground;
latestColor[id] = config.color;
clearScreen(config.smear, '0,0,0', config.ctx);
drawTimeScope(analysers[id], config);
},
{ id },
);
};
Pattern.prototype.scope = Pattern.prototype.tscope;
+4 -3
View File
@@ -12,12 +12,13 @@ setLogger(logger);
const hap2value = (hap) => {
hap.ensureObjectValue();
return { ...hap.value, velocity: hap.context.velocity };
return hap.value;
};
// TODO: bind logger
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration);
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioOutputTrigger);
+699 -193
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
# samples folder
1. copy any samples to this folder
2. run `npx @strudel/sampler` from this folder
3. add `samples('local:')` to your code
File diff suppressed because it is too large Load Diff

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