Compare commits

...

330 Commits

Author SHA1 Message Date
Felix Roos d649d3e940 fix: add flag to reduce dom weight 2023-08-11 01:20:31 +02:00
Felix Roos 5101bb298e add link to reference home 2023-08-11 01:13:30 +02:00
Felix Roos f2c8b76d75 hide superdirt controls 2023-08-11 01:09:14 +02:00
Felix Roos 07c9515de1 fix: ignore empty names 2023-08-11 01:01:54 +02:00
Felix Roos 62c121da42 fix: pages of synonyms 2023-08-11 00:57:07 +02:00
Felix Roos 37a2ee5973 fix: filter out internals
+ use name instead of longname for linking
2023-08-11 00:48:29 +02:00
Felix Roos 60deb461b5 fix: remove duplicate 2023-08-11 00:48:00 +02:00
Felix Roos 6381a8deca basic reference with one page per function 2023-08-11 00:38:24 +02:00
Felix Roos 2483bf812f Merge pull request #660 from tidalcycles/fix-multiple-triggers
fix: always run previous trigger
2023-08-11 00:06:21 +02:00
Felix Roos 7a999de602 fix: always run previous trigger
+ make dominantTrigger more dominant
2023-08-11 00:04:09 +02:00
Felix Roos 1929c27225 Merge pull request #659 from tidalcycles/fix-console-welcome
fix: welcome message for latestCode
2023-08-10 23:54:58 +02:00
Felix Roos 0cf5c9c7d6 fix: welcome message for latestCode 2023-08-10 23:51:04 +02:00
Felix Roos c20b691683 Merge pull request #657 from daslyfe/main
[Bug Fix] Midi: Don't treat note 0 as false
2023-08-07 07:45:26 +02:00
Jade Rowland 5c2101de1a [Bug Fix] Midi: Don't treat note 0 as false 2023-07-29 13:34:02 -04:00
Felix Roos 2ec2e81511 Merge pull request #656 from daslyfe/main
[Bug Fix] Account for numeral notation when converting to midi
2023-07-29 09:06:17 +02:00
Jade Rowland ac3d8b832c [Bug Fix] Account for numeral notation when converting to midi 2023-07-29 01:29:05 -04:00
Felix Roos ad174957e8 Merge pull request #653 from tidalcycles/ireal-voicings
ireal voicings
2023-07-23 22:18:48 +02:00
Felix Roos 9b3fa83516 add common chord symbol aliases 2023-07-23 22:05:42 +02:00
Felix Roos 52b8d9d361 add octaves control for arpeggios 2023-07-23 21:18:34 +02:00
Felix Roos d231ded7a2 fix: ^7 voicings 2023-07-23 21:18:18 +02:00
Felix Roos 5f57951e3d fix: add empty string as major alias 2023-07-23 14:10:38 +02:00
Felix Roos b1a4788442 a few more voicings 2023-07-23 14:06:25 +02:00
Felix Roos 6377378a3d sort by topnote and then by bottom note
(easier to track diffs)
2023-07-23 13:31:47 +02:00
Felix Roos 45b91cdea5 fix: major chords 2023-07-23 09:57:34 +02:00
Felix Roos d5647c6760 sort voicings by top note 2023-07-23 09:51:21 +02:00
Felix Roos f419559bde Merge remote-tracking branch 'origin/main' into ireal-voicings 2023-07-22 16:22:49 +02:00
Felix Roos 4cfaa39419 fix: flip subtraction 2023-07-22 13:54:43 +02:00
Felix Roos d6b1762adf Merge pull request #652 from tidalcycles/understand-pitch
Understand pitch
2023-07-22 09:30:21 +02:00
Felix Roos 45c3293385 format 2023-07-22 09:28:51 +02:00
Felix Roos 92d574605e small fixes 2023-07-22 09:26:34 +02:00
Felix Roos cec7b90631 finish pitch page 2023-07-22 09:19:13 +02:00
Felix Roos aa3cb35f09 better exponent display 2023-07-21 18:42:47 +02:00
Felix Roos 7373138e3d improve pitch page 2023-07-21 17:46:45 +02:00
Felix Roos 2d8176f213 begin pitch page 2023-07-21 14:40:47 +02:00
Felix Roos 8589d72aee fix: console error 2023-07-21 14:40:40 +02:00
Felix Roos 626b50087a Merge branch 'main' into docs 2023-07-21 10:02:02 +02:00
Felix Roos 1e62554dfb add links 2023-07-20 23:43:53 +02:00
Felix Roos bcf800482b fix: chord typo + export only voicings 2023-07-20 23:43:08 +02:00
Felix Roos 08f94d2122 add ireal voicings 2023-07-20 22:53:02 +02:00
Felix Roos 559cfd11db Merge pull request #651 from tidalcycles/update-vitest
update vitest
2023-07-17 23:42:59 +02:00
Felix Roos ce820c2314 update vitest 2023-07-17 23:37:51 +02:00
Felix Roos 8583ed0e5c Merge pull request #647 from tidalcycles/tonleiter
stateless voicings + tonleiter lib
2023-07-17 23:34:33 +02:00
Felix Roos f2c16a06a1 fix: note2oct 2023-07-17 23:10:13 +02:00
Felix Roos 584d469a73 Merge pull request #650 from bwagner/tonleiter
FIXES: TODO in rotateChroma
2023-07-17 17:57:16 +02:00
Bernhard Wagner 4298e582fb FIXES: note2pc TODOs 2023-07-14 14:38:26 +02:00
Bernhard Wagner a3fdfbbc16 FIXES: weird gain difference 2023-07-14 14:04:35 +02:00
Bernhard Wagner b8929a0d18 FIXES: prettier formatting 2023-07-14 13:56:36 +02:00
Bernhard Wagner 0fa5832b63 FIXES: TODO in rotateChroma 2023-07-14 13:45:27 +02:00
Felix Roos bbb29ebd7c fix: duck mode with n
+ detect chord obj in rootNotes
2023-07-13 12:52:57 +02:00
Felix Roos 0fd9ac6da3 feat: support strings in voicing function 2023-07-13 12:13:00 +02:00
Felix Roos e53098b179 fix: tests 2023-07-13 11:58:24 +02:00
Felix Roos 78dcab2778 fix: control interference 2023-07-13 11:24:57 +02:00
Felix Roos e05f74cef1 docs + rename under -> duck, quak quak 2023-07-13 10:49:08 +02:00
Felix Roos 3d8724c90d snapshot 2023-07-13 01:11:31 +02:00
Felix Roos ffeda19ce5 snapshots + fix default anchor for lefthand 2023-07-13 01:09:53 +02:00
Felix Roos 70fe8f14e3 refactor: voicings -> voicing in tunes 2023-07-13 01:09:29 +02:00
Felix Roos a639d9f49f fix: remove log 2023-07-13 00:25:24 +02:00
Felix Roos e5251422fe revert changing existing addVoicings
+ add registerVoicings for the new logic
2023-07-13 00:20:28 +02:00
Felix Roos d8f80c058d can now set mode and anchor on voicing dictionary 2023-07-12 23:47:13 +02:00
Felix Roos 4cf3655c08 voicing modes: below, under, above 2023-07-12 23:32:27 +02:00
Felix Roos 58fc786462 noteToMidi: adjustable default octave
+ pull out static objects
2023-07-12 23:32:02 +02:00
Felix Roos 81b142704c change call signature of renderVoicing
+ pull logic in from voicings function
2023-07-12 22:55:34 +02:00
Felix Roos b0cbeda796 fix: support existing voicing dicts in new logic
+ rename voiceBelow to renderVoicing
2023-07-12 22:38:44 +02:00
Felix Roos 0b3a8a5f65 - make dictionary a control
- standalone voicing function
- simplify voicing control names
2023-07-11 22:49:34 +02:00
Felix Roos d7d8d2a3e6 Merge pull request #645 from tidalcycles/slice
slice: list mode
2023-07-10 19:07:45 +02:00
Felix Roos d5c0309885 feat: support n for voicing scales
+ simplify voicing logic
2023-07-09 22:11:40 +02:00
Felix Roos 36fa9d81fb fix: pianoroll isActive still used whole.end 2023-07-09 18:51:26 +02:00
Felix Roos 35f5a5d11a feat: add voiceOffset (2) 2023-07-09 16:53:41 +02:00
Felix Roos d1fe50e271 feat: add voiceOffset 2023-07-09 16:53:29 +02:00
Felix Roos 2d08b31f9a fix: tests 2023-07-08 16:14:25 +02:00
Felix Roos 27643f4a48 doc: slice 2023-07-08 16:12:21 +02:00
Felix Roos 110e179e9e wip: new voicing function 2023-07-08 12:41:17 +02:00
Felix Roos fe6d99d4f1 slice array notation 2023-07-07 21:48:19 +02:00
Felix Roos 06fbcbe24b docs: add slice effect 2023-07-07 21:48:03 +02:00
Felix Roos f40b57ee48 hotfix: remove log 2023-07-06 09:59:20 +02:00
Felix Roos 9438c69bed comment 2023-07-06 09:58:57 +02:00
Felix Roos 70d6f3000a move tonleiter 2023-07-06 09:58:48 +02:00
Felix Roos d47fd0cf18 Merge pull request #639 from tidalcycles/delete-old-packages
Delete old packages
2023-07-04 23:50:30 +02:00
Felix Roos 40117fbe54 package-lock 2023-07-04 23:47:37 +02:00
Felix Roos 7466d8a1de delete webdirt 2023-07-04 23:44:36 +02:00
Felix Roos ce09443e48 delete tone 2023-07-04 23:43:49 +02:00
Felix Roos 1718dbe94e delete eval package 2023-07-04 23:41:29 +02:00
Felix Roos 69894db206 Merge pull request #634 from tidalcycles/highlight-ids
Adaptive Highlighting
2023-07-04 23:38:41 +02:00
Felix Roos 6e4c873248 remove tests from soon removed package 2023-07-04 22:17:44 +02:00
Felix Roos 64693ffd26 fix: remove test flag 2023-07-04 22:16:02 +02:00
Felix Roos 66f8ca72c1 refactor: remove old location methods
docs: update repl chapter to reflect new transpiler behavior
2023-07-04 22:13:40 +02:00
Felix Roos c82f7bd8fe Merge pull request #636 from mindofmatthew/highlight-ids
More work on highlight IDs
2023-07-04 21:55:57 +02:00
Felix Roos 34176ab5f8 refactor:
- dedupe flash / highlighting logic
- codemirror logic now lives only in codemirror package
- remove old highlighting logic
- use codemirror package in react package
- cleanup CodeMirror6.jsx
- pull setMiniLocations into useHighlighting
- migrate MiniRepl, nano-repl + Repl to new highlighting
2023-07-04 21:49:39 +02:00
Felix Roos 7f12ce9b45 fix: format 2023-07-04 18:47:44 +02:00
Felix Roos 9e1b9f7f5c Merge remote-tracking branch 'upstream/main' into highlight-ids 2023-07-04 18:46:05 +02:00
Felix Roos 23e949de1c Merge pull request #637 from tidalcycles/snapshots-sort-haps-by-part
snapshot tests: sort haps by part
2023-07-04 18:44:47 +02:00
Felix Roos 00bd60a855 snapshot tests: sort haps by part 2023-07-04 18:43:26 +02:00
Felix Roos dacd9afac0 comment out opacity logic for now 2023-07-04 18:34:22 +02:00
Felix Roos 3fc5bb31d0 fix: do not recreate haps mapping on update 2023-07-04 18:22:22 +02:00
Felix Roos 78770888a5 remove mock positions 2023-07-04 18:16:58 +02:00
Matthew Kaney f5b092acf2 Split up highlighting logic and add progress flash 2023-07-03 17:06:39 -04:00
Matthew Kaney ba9562f000 Remove highlights when code stops 2023-07-03 10:52:06 -04:00
Matthew Kaney aded178ab7 Pass mininotation locations into highlight state 2023-07-03 03:55:49 -04:00
Felix Roos 0b5d905120 fix: adaptive highlighting
- transpiler now uses m function with globalOffset
- patternifyAST now accepts global offset
- patternifyAST now calls .withLoc with global leaf location
- .withLoc replaces .withLocation + .withMiniLocation
- simple locs (offsets) are now used everywhere
- some tests fail, seems some haps have reordered...
- wip: Repl still uses hardcoded updateMiniLocations
- todo: find way to call updateMiniLocations dynamically
2023-07-03 05:15:32 +02:00
Felix Roos 08abec8fd5 fix: init phase 2023-07-02 18:37:56 +02:00
Felix Roos 63c23736ad wip: adaptive highlighting 2023-07-02 18:33:44 +02:00
Felix Roos f7bd373ce6 update package-lock 2023-07-02 14:17:15 +02:00
Felix Roos 8e717d2ea1 - transpiler now always returns an object
- emit transpiler metadata from evaluate / afterEval
- currently logging miniLocations from Repl.jsx
2023-07-02 14:15:54 +02:00
Felix Roos 5f271ed127 add emitMiniLocations flag to transpiler 2023-07-02 13:42:41 +02:00
Felix Roos d686b65dbe add 2 more tests 2023-07-01 23:55:41 +02:00
Felix Roos c7e882e001 mini: leaf location retrieval
- add onEnter callback for patternifyAST
- add leaf node location helpers
- add tests
2023-07-01 23:52:31 +02:00
Felix Roos f07859996e Merge pull request #631 from tidalcycles/fix-canvas-resize
fix: update canvas size on window resize
2023-06-30 22:47:39 +02:00
Felix Roos fbc73bc78a Merge pull request #630 from tidalcycles/out-of-range-error
fix: out of range error
2023-06-30 22:45:41 +02:00
Felix Roos bd8ad1ed1b fix: out of range error due to rerender 2023-06-30 22:40:02 +02:00
Felix Roos dc2ff83fa5 do not flash when empty 2023-06-30 22:38:14 +02:00
Felix Roos 13545d147b optimize rendering 2023-06-30 22:37:55 +02:00
Felix Roos 1b85aa713b fix: update canvas size on window resize 2023-06-30 16:03:56 +02:00
Felix Roos 90a58858ca Publish
- @strudel/codemirror@0.8.4
 - @strudel.cycles/core@0.8.2
 - @strudel.cycles/csound@0.8.0
 - @strudel.cycles/eval@0.8.0
 - @strudel.cycles/midi@0.8.0
 - @strudel.cycles/mini@0.8.2
 - @strudel.cycles/osc@0.8.0
 - @strudel.cycles/react@0.8.0
 - @strudel.cycles/serial@0.8.0
 - @strudel.cycles/soundfonts@0.8.2
 - @strudel.cycles/tonal@0.8.2
 - @strudel.cycles/tone@0.8.0
 - @strudel.cycles/transpiler@0.8.2
 - @strudel/web@0.8.3
 - @strudel.cycles/webaudio@0.8.2
 - @strudel.cycles/webdirt@0.8.0
 - @strudel.cycles/xen@0.8.0
2023-06-30 08:17:19 +02:00
Felix Roos 8149b85a55 Merge pull request #627 from tidalcycles/fix-clock-drift
fix: midi clock drift
2023-06-30 08:14:40 +02:00
Felix Roos e79b6c514a Merge pull request #621 from tidalcycles/tauri-fs
desktop: play samples from disk
2023-06-29 22:02:27 +02:00
Felix Roos aaa894d74b Merge pull request #620 from tidalcycles/loopAt
cps dependent functions
2023-06-29 21:59:42 +02:00
Felix Roos b55721f16f fix: midi clock drift 2023-06-29 21:58:15 +02:00
Felix Roos b39d948d71 fix: build 2023-06-29 12:32:59 +02:00
Felix Roos 0fee279e3d rename fay to fit 2023-06-29 12:05:31 +02:00
Felix Roos c7d8a9cb82 fix: playFile import 2023-06-29 12:05:09 +02:00
Felix Roos b50075f102 feat: can now load disk samples via samples
+ encapsulate file logic from FilesTab
2023-06-29 10:47:20 +02:00
Felix Roos fb3ca9f960 feat: register fn for sample prefix handling 2023-06-29 10:46:32 +02:00
Felix Roos 058b971870 can now play samples from file system
- ui still wip
2023-06-29 02:02:00 +02:00
Felix Roos 610ea414c1 expose tauri fs api 2023-06-29 02:01:22 +02:00
Felix Roos cc6997baec - encapsulate sample map handling
- add resolveUrl param
2023-06-29 02:00:59 +02:00
Felix Roos d126c28b85 fix: loopAt with cps 1 for fallback 2023-06-28 22:25:49 +02:00
Felix Roos c1649b9dc0 - fix: loopAt for non default cps value
- add fay to repitch samples to fit hap duration
- add setcpm function
2023-06-28 22:13:05 +02:00
Felix Roos b0a8d12112 Merge pull request #520 from paikwiki/patch-1
Fix typo on packages.mdx
2023-06-26 22:45:04 +02:00
Felix Roos b5160d38cb another dummy commit 2023-06-26 22:43:14 +02:00
Felix Roos 360e9b5936 dummy commit 2023-06-26 22:41:47 +02:00
Felix Roos 85a6cd828e Merge pull request #606 from tidalcycles/setting-patterning
patterning ui settings
2023-06-26 22:35:02 +02:00
Felix Roos e698f19a64 Merge pull request #614 from tidalcycles/spirals
add spiral viz
2023-06-26 22:32:46 +02:00
Felix Roos 83c1b580dd Merge pull request #613 from vasilymilovidov/tauri
tauri test
2023-06-26 22:17:46 +02:00
vvm 9273e1f8ed fix tauri icons 2023-06-26 13:13:22 +04:00
vvm 5287825279 tauri workflow fixes 2023-06-26 11:27:21 +04:00
vvm 5567959afd add tauri github workflow 2023-06-26 01:58:16 +04:00
Felix Roos eaab36b051 add spiral viz 2023-06-25 14:23:18 +02:00
vvm 9a4f3af580 add README 2023-06-24 02:34:02 +04:00
vvm b83664dfce add build optimizations 2023-06-24 02:32:57 +04:00
vvm 349f7ae965 init 2023-06-23 21:14:55 +04:00
vvm 6f41716ab5 init 2023-06-23 21:06:10 +04:00
Felix Roos abeb62dea8 Merge pull request #612 from tidalcycles/fix-links
fix: doc links
2023-06-23 09:59:43 +02:00
Felix Roos 7cfed80443 Merge pull request #598 from tidalcycles/clip-support-floats
clip now works like legato in tidal
2023-06-23 09:55:18 +02:00
Felix Roos 36aa7d9b25 Merge pull request #607 from tidalcycles/doc-fixes
tonal fixes
2023-06-18 10:36:17 +02:00
Felix Roos 2a449ad3c0 rename tonal api -> tonal functions 2023-06-18 10:32:51 +02:00
Felix Roos a71c5957cc fix: better fix 2023-06-16 14:23:39 +02:00
Felix Roos eef6c7d55e quick fix: typo 2023-06-16 14:20:49 +02:00
Felix Roos 3872322da1 allow patterning theme + font settings 2023-06-16 13:45:13 +02:00
Felix Roos f1e850a62a fix: doc links 2023-06-16 09:20:29 +02:00
Felix Roos 8d9278419d fix: flatten scale lists 2023-06-15 20:19:31 +02:00
Felix Roos 0862827a51 Merge pull request #581 from roipoussiere/line_wrap
editor: enable line wrapping
2023-06-15 12:51:40 +02:00
Roipoussiere f37a220a05 add checkbox in settings to enable line wrapping 2023-06-15 11:52:29 +02:00
Felix Roos aa51c2b6dd Merge remote-tracking branch 'upstream/main' into line_wrap 2023-06-15 10:59:52 +02:00
Felix Roos 92da70a7ae Merge remote-tracking branch 'origin/main' into clip-support-floats 2023-06-15 10:46:10 +02:00
Felix Roos a55862fc25 Merge pull request #602 from tidalcycles/ratio-function
add ratio function
2023-06-15 10:22:46 +02:00
Felix Roos 09b14a5702 add ratio function 2023-06-14 23:19:04 +02:00
Felix Roos 260b831815 refactor: legato -> clip 2023-06-14 00:22:55 +02:00
Felix Roos 665cf6ac65 Merge pull request #588 from roipoussiere/enable_autocomplete
enable auto-completion
2023-06-12 23:24:28 +02:00
Felix Roos 5e90021698 update snapshot 2023-06-12 23:12:40 +02:00
Felix Roos 16aeb7e12c docs: update clip entry 2023-06-12 23:11:07 +02:00
Felix Roos b4cbec173b only accept undefined for clip 2023-06-12 22:58:25 +02:00
Felix Roos 8c9e06c329 - clip now works like legato in tidal
- supports floats
- hap.duration now respects clip value
- hap.endClipped is now end*clip
- visualizations show clipped length
- clip(0) will now be silence
2023-06-12 22:39:32 +02:00
Felix Roos e4a35fdd72 Merge pull request #597 from tidalcycles/cursor-fix
improve cursor
2023-06-11 22:04:17 +02:00
Felix Roos 370d82e66c + disable cursor blink
+ make cursor thicker
2023-06-11 20:21:11 +02:00
Felix Roos 876e851517 Merge pull request #570 from dariacotocu/branch-daria
Solmization added
2023-06-11 20:00:45 +02:00
Felix Roos 190fe37a9f move solmization logic from midi2note to sol2note 2023-06-11 19:55:24 +02:00
Felix Roos 623661f8d2 Merge remote-tracking branch 'upstream/main' into branch-daria 2023-06-11 19:54:55 +02:00
Felix Roos f31bc3f8c5 Merge pull request #591 from tidalcycles/fix-fast0
fix: division by zero
2023-06-11 19:45:00 +02:00
Felix Roos 12fc48544c Merge pull request #596 from tidalcycles/fix-option-dot
Fix option dot
2023-06-11 19:44:40 +02:00
Felix Roos 3b4e44a9fd fix: main repl option-dot on mac 2023-06-11 13:55:11 +02:00
Felix Roos 7850fb727e fix: allow f for flat notes like tidal 2023-06-11 13:23:45 +02:00
Felix Roos 9b3122837e fix: division by zero 2023-06-11 13:15:52 +02:00
Felix Roos d4c59cc73b fix:typo + remove notes 2023-06-11 11:21:19 +02:00
Felix Roos fe36bf54b2 Merge commit '4d8308a763a632cc4dd93ed2a2ab129eb957af72' 2023-06-09 21:39:14 +02:00
Felix Roos 4d8308a763 fix: link goes to wrong language 2023-06-09 21:33:32 +02:00
Felix Roos 11f26c1e2c Merge pull request #587 from tidalcycles/workshop-new
New Workshop
2023-06-09 21:02:11 +02:00
Felix Roos e10104d401 bring de improvements to en workshop 2023-06-09 20:59:40 +02:00
Roipoussiere 5e09c3c318 add autocompletion extension from useMemo 2023-06-09 18:28:16 +02:00
Felix Roos 576b88221d Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-09 18:00:16 +02:00
Felix Roos 7dc2a0091b more detail 2023-06-09 17:59:08 +02:00
Felix Roos 89a4a85589 more friendly heading 2023-06-09 17:58:05 +02:00
Felix Roos a3baf07a5c Merge pull request #580 from roipoussiere/music_metadata
Music metadata
2023-06-09 17:31:58 +02:00
Roipoussiere 8e6909478c autocompletion: lint 2023-06-09 16:42:31 +02:00
Roipoussiere 2d2d0cabb3 Add option in settings to enable autocompletion 2023-06-09 16:38:48 +02:00
Roipoussiere 6f04a1b8fa autocomplete: add vanilla js completion 2023-06-09 14:52:07 +02:00
Roipoussiere a9ad3a56ad autocomplete: avoid tabs overlapping 2023-06-09 14:30:20 +02:00
Roipoussiere 3069b14e9c enable autocompletion 2023-06-09 13:41:40 +02:00
Felix Roos ae868c6372 hide gutter in mini repl 2023-06-09 00:46:04 +02:00
Felix Roos c287ff554c optimize sounds chapter after teach test 2023-06-09 00:26:40 +02:00
Felix Roos 3ccbd35fad add label support to pianoroll 2023-06-09 00:25:54 +02:00
Felix Roos 51cc69208f fix: repl link to docs was old 2023-06-09 00:25:14 +02:00
Felix Roos 1fec2f7a46 fix: build 2023-06-08 21:31:32 +02:00
Felix Roos ffffdd2d7d fix: format 2023-06-08 20:55:26 +02:00
Felix Roos f8c3e3d5aa add missing sounds by hand 2023-06-08 20:43:45 +02:00
Felix Roos eccce6fa84 remove dirt samples
+ load a small selection for workshop instead
2023-06-08 17:48:54 +02:00
Felix Roos 44be6cee2c Merge pull request #585 from srenatus/typo
learn/tonal: fix typo in "scaleTran[s]pose"
2023-06-08 09:33:34 +02:00
Stephan Renatus 799afd511a learn/tonal: fix typo in "scaleTran[s]pose" 2023-06-08 08:17:38 +02:00
Felix Roos 37c4541468 translate link 2023-06-07 21:50:26 +02:00
Felix Roos 119a66acd0 - translate getting-started
- fix de links
- link english docs / german workshop
2023-06-07 21:49:57 +02:00
Felix Roos 105193a452 replace tutorial with workshop
+ move parts of tutorial to more
+ move more before Pattern Functions
2023-06-07 21:22:01 +02:00
Felix Roos 02d793e9e0 Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-07 20:19:34 +02:00
Felix Roos 90647a8ba8 Merge pull request #582 from roipoussiere/line_numbers
repl: add option to display line numbers
2023-06-07 20:19:11 +02:00
Felix Roos 06950a6bd8 translate rest of workshop to german 2023-06-07 16:50:54 +02:00
Roipoussiere 0c2147d9c5 repl: add option to display line numbers 2023-06-07 15:34:57 +02:00
Roipoussiere 51e8174689 metadata: add quotes syntax for titles 2023-06-07 12:26:23 +02:00
Roipoussiere 185318a70c metadata: fix typos 2023-06-07 11:02:38 +02:00
Felix Roos a88759ba94 translate notes chapter 2023-06-06 22:43:51 +02:00
Roipoussiere 96ac054392 metadata: improve doc 2023-06-06 21:34:53 +02:00
Roipoussiere bd7f4dd73f metadata: add tests 2023-06-06 21:34:53 +02:00
Roipoussiere 2e22152089 metadata: allow several values and one-liners 2023-06-06 21:34:53 +02:00
Roipoussiere 98a89aea11 lint: metadata parsing 2023-06-06 21:34:53 +02:00
Roipoussiere 372e33c066 doc: add section about metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 52641db590 load titles from metadata in examples page 2023-06-06 21:34:53 +02:00
Roipoussiere f9ad5f0d01 add tunes metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 3db1c7a521 swatch: look for song titles in code metadata 2023-06-06 21:34:53 +02:00
Roipoussiere b6d1f96930 editor: enable line wrapping 2023-06-06 13:29:55 +02:00
Felix Roos 61a9b01062 started german translation of workshop 2023-06-05 23:48:04 +02:00
Felix Roos f0faf5b42e Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-05 22:48:50 +02:00
Felix Roos 11733d5e05 Merge pull request #563 from tidalcycles/vanilla-highlighting
Vanilla JS Refactoring
2023-06-05 21:35:49 +02:00
Felix Roos c4a38d9008 + pattern effects chapter
+ recap page
+ only load mini repls when visible
2023-05-30 06:16:02 +02:00
Felix Roos 0edd7e14b7 add compound adsr + ds controls 2023-05-29 12:39:36 +02:00
Felix Roos 8b7bb7b6ae add adsr section to effects chapter 2023-05-29 12:39:17 +02:00
Felix Roos 39d5955e58 add function recap to sounds chapter 2023-05-29 12:39:05 +02:00
Felix Roos 536327f403 effects chapter mostly finished 2023-05-29 02:18:36 +02:00
Felix Roos 8679dc63be pianoroll: also reflect gain in transparency 2023-05-29 02:18:27 +02:00
Felix Roos 1e9979ae18 start fleshing out effects chapter 2023-05-28 12:42:24 +02:00
Felix Roos d2dffe3186 MiniRepl: consume font settings 2023-05-28 12:42:15 +02:00
Felix Roos 9971867e2f clamp function 2023-05-28 12:41:53 +02:00
Felix Roos 8c93e578a0 clamp delayfeedback 2023-05-28 12:41:46 +02:00
Felix Roos 1ba5d2e1ca finish notes chapter 2023-05-27 21:52:44 +02:00
Felix Roos ed792fc0d4 continue notes chapter 2023-05-27 16:29:05 +02:00
Felix Roos 4e575c44b3 begin first notes page 2023-05-27 13:31:18 +02:00
Felix Roos fc06181217 - add claviature flag to minirepl
- bring back option+dot on macos
- consume more editor settings in minirepl
2023-05-27 13:30:57 +02:00
Felix Roos 0d6fcf78d8 hide mini repl headers + improve workshop 2023-05-26 16:05:53 +02:00
Felix Roos 82225f0b81 started workshop pages 2023-05-26 14:12:53 +02:00
Felix Roos 2177e13b49 Merge branch 'vanilla-highlighting' into workshop 2023-05-25 21:38:34 +02:00
Daria Cotocu b63f4eb503 Update solmization.test.js 2023-05-24 18:58:11 +01:00
Daria Cotocu cd7bc09f9c Update solmization.test.js 2023-05-24 18:43:24 +01:00
Daria Cotocu ccf775e976 Format code 2023-05-24 17:57:05 +01:00
Daria Cotocu 811346e83d Solmization added 2023-05-24 17:24:34 +01:00
Felix Roos d3240a75d0 fix: dupe style.css to Repl.css
for some reason they were missing after update
+ darken line backgrounds
+ change preview port to keep workbox out of dev server
2023-05-12 11:56:50 +02:00
Felix Roos d93e396ce5 fix: nano-repl 2023-05-12 10:21:05 +02:00
Felix Roos d74cc234c4 fix: thinner outline for highlights with .color 2023-05-12 10:06:55 +02:00
Felix Roos 0fda83d8de bump soundfonts 2023-05-12 10:00:55 +02:00
Felix Roos a1ec8545fb format 2023-05-10 13:03:06 +02:00
Felix Roos 087f574355 update web readme + add more html examples 2023-05-10 12:49:45 +02:00
Felix Roos 1de784b817 add headless-serverless-buildless example 2023-05-10 12:20:27 +02:00
Felix Roos db79c05d9a bump web pkg dependencies 2023-05-10 12:11:51 +02:00
Felix Roos 1fdb1f0da6 bump webaudio package 2023-05-10 12:09:20 +02:00
Felix Roos e2bfaa6233 remove soundfonts from web package for now 2023-05-10 12:06:16 +02:00
Felix Roos 6089849d35 rename init to initStrudel + add to window 2023-05-10 11:53:26 +02:00
Felix Roos 6e02bf59e9 Revert "refactor: remove old draw logic"
This reverts commit 95719654f3.
2023-05-10 11:44:55 +02:00
Felix Roos 12228c56d9 improve api for web package 2023-05-07 22:36:26 +02:00
Felix Roos 3b631cb6af add @strudel/web umbrella package + example 2023-05-07 11:56:35 +02:00
Felix Roos b27a58df7e bump more packages to 0.8.0 2023-05-05 23:58:47 +02:00
Felix Roos 308f139193 bump codemirror again to get new core version 2023-05-05 23:52:55 +02:00
Felix Roos 692fc4e11d revert rename core package 2023-05-05 23:52:04 +02:00
Felix Roos b52f65c4d7 rename core package + bump to 0.8.0 2023-05-05 23:50:06 +02:00
Felix Roos 299275e368 lockfile 2023-05-05 23:47:15 +02:00
Felix Roos bb71c519db bump codemirror package 2023-05-05 23:46:42 +02:00
Felix Roos 9ec7109dc8 + simplify vanilla setup drastically
+ move repl + drawer boilerplate inside StrudelMirror
2023-05-05 23:43:07 +02:00
Felix Roos 5b67fccb1b vanilla-repl-cm6: draw first frame 2023-05-05 16:17:22 +02:00
Felix Roos 9807c4b7a1 move Framer + Drawer to core
... from vite-vanilla-repl-cm6
2023-05-05 16:06:11 +02:00
Felix Roos 9039600f6d bump codemirror to 0.8.1 2023-05-05 15:31:19 +02:00
Felix Roos b5b0156e08 fix: codemirror reeval 2023-05-05 15:31:04 +02:00
Felix Roos 352e647d37 add build script for codemirror package 2023-05-05 15:15:45 +02:00
Felix Roos e6b66f2525 lockfile 2023-05-05 15:14:46 +02:00
Felix Roos f5075906e2 add codemirror package
+ use it in vite-vanilla-repl-cm6
2023-05-05 15:13:17 +02:00
Felix Roos a6f57bced8 prettier 2023-05-05 12:00:13 +02:00
Felix Roos d35bf9591c vanilla-repl-cm6: better theme 2023-05-05 11:54:09 +02:00
Felix Roos d96f3f9dd4 vanilla-repl-cm6: style + codesplitting 2023-05-05 11:09:44 +02:00
Felix Roos 1f4c2f8c5a draw scheduler in vanilla js 2023-05-05 10:52:21 +02:00
Felix Roos 95719654f3 refactor: remove old draw logic
+ pianoroll now uses .onPaint
2023-05-05 09:52:07 +02:00
Felix Roos 4b921c47f5 vanilla-repl-cm6: add flash effect 2023-05-05 09:11:40 +02:00
Felix Roos f069f53fae vanilla-repl-cm6: add keybindings 2023-05-05 09:06:27 +02:00
Felix Roos 1494cc38fc add vite-vanilla-repl-cm6 2023-05-05 08:55:43 +02:00
Felix Roos a035412956 Merge pull request #559 from tidalcycles/patchday
Patchday
2023-05-05 08:34:36 +02:00
Felix Roos 08662ea831 Merge pull request #557 from tidalcycles/some-voicings
add basic triads and guidetone voicings
2023-05-02 21:48:32 +02:00
Felix Roos d97103268b add basic triads and guidetone voicings 2023-05-02 21:40:20 +02:00
Felix Roos a959e2e34d update remaining packages 2023-04-30 08:30:29 +02:00
Felix Roos 602bfbdd79 update a bunch of minor versions 2023-04-30 08:20:51 +02:00
Felix Roos 0785bdd37a update lerna 2023-04-30 08:04:56 +02:00
Felix Roos 8b62b6e1b1 update codemirror + tailwindcss 2023-04-30 08:02:06 +02:00
Felix Roos d1c8ebd15b remove c8 2023-04-30 07:55:23 +02:00
Felix Roos 13088e4001 - remove gh-pages
- update peggy
- rebuild parser
2023-04-30 07:48:08 +02:00
Felix Roos 5c6fa1c82b update vite to v4 2023-04-29 08:10:12 +02:00
Felix Roos fd81867e7b fix: controls import broke on ssr 2023-04-29 07:45:21 +02:00
Felix Roos 78293e64dd lockfile 2023-04-29 07:45:09 +02:00
Felix Roos 930fc10aec bump some minor versions 2023-04-29 07:45:04 +02:00
Felix Roos 0b2a4fda44 update supabase-js 2023-04-29 07:24:03 +02:00
Felix Roos efdce6d4cd update astro 2023-04-29 06:55:34 +02:00
Felix Roos c0b0ea4493 remove preact 2023-04-29 06:25:19 +02:00
Felix Roos 0fcec0a0ca upgrade jsdoc 2023-04-28 13:05:50 +02:00
Felix Roos c0701d8a35 Merge remote-tracking branch 'origin/main' into patchday 2023-04-28 12:50:20 +02:00
Felix Roos ae51092044 Merge pull request #556 from tidalcycles/fix-prebake
fix: make soundfonts import dynamic
2023-04-28 12:46:56 +02:00
Felix Roos b7fcccb8db fix: make soundfonts import dynamic
- it broke the whole docs page
2023-04-28 12:42:07 +02:00
Felix Roos b21a52f5f3 update @vite-pwa/astro 2023-04-28 12:23:10 +02:00
Felix Roos 2bdcfe13e6 update @vitest/ui 2023-04-28 12:20:35 +02:00
Felix Roos 12a97ec429 update nanostores 2023-04-28 12:18:45 +02:00
Felix Roos 99298ce9f2 update vite-plugin-pwa 2023-04-28 12:14:07 +02:00
Felix Roos a0ed25d373 use vitests@^0.28.0 in packages 2023-04-28 12:12:05 +02:00
Felix Roos dc6c29c668 update vitest to ^0.28.0 2023-04-28 12:08:48 +02:00
Felix Roos 3a85f28b5c install with pnpm 8.3.1 2023-04-28 10:33:47 +02:00
Felix Roos 95c44b1ce2 Merge pull request #553 from tidalcycles/highlighting-colors
fix: colorable highlighting
2023-04-22 15:43:22 +02:00
Felix Roos b88e0ecb78 fix: colorable highlighting 2023-04-22 15:25:00 +02:00
Felix Roos c26f6b72d9 Merge pull request #550 from tidalcycles/fix-soundfont-loading
fix: load soundfonts in prebake
2023-04-06 00:11:02 +02:00
Felix Roos 6cd3cc96a3 fix: do not prebake in tests
+ pull piano function to separate file for tests
2023-04-06 00:08:23 +02:00
Felix Roos c138cf0b76 fix: load soundfonts in prebake 2023-04-06 00:00:49 +02:00
Felix Roos 8edb086178 Merge pull request #548 from tidalcycles/stop-pause
fix: reset time on stop
2023-03-29 22:23:07 +02:00
Felix Roos 0f74b5a4b1 Merge pull request #547 from tidalcycles/fix-mini-whitespace
fix: allow whitespace at the end of a mini pattern
2023-03-29 22:16:37 +02:00
Felix Roos 6de2feff05 fix: reset time on stop 2023-03-29 22:05:13 +02:00
Felix Roos a9811ee955 fix: allow whitespace at the end of a mini pattern
- fixes #546
2023-03-29 21:57:55 +02:00
Felix Roos 98b6c11a82 Merge pull request #544 from tidalcycles/fonts
add firacode font
2023-03-24 22:10:55 +01:00
Felix Roos 3df2929d12 dummy commit (github cert error :-/) 2023-03-24 22:06:40 +01:00
Felix Roos 30d1f423cb fix: remove loading state on eval error 2023-03-24 21:50:50 +01:00
Felix Roos e0c5566f35 add 2 FiraCode fonts 2023-03-24 21:23:37 +01:00
Felix Roos c5a2ae47d7 Merge pull request #542 from tidalcycles/loading-animation
feat: add loader bar to animate loading state
2023-03-24 12:54:20 +01:00
Felix Roos 17654b885b feat: add loader bar to animate loading state 2023-03-24 12:41:00 +01:00
Felix Roos acaf0ca0af Merge pull request #539 from tidalcycles/setcps-reset-only-on-shuffle
do not reset cps before eval #517
2023-03-23 22:39:26 +01:00
Felix Roos 6a201be1fd Merge remote-tracking branch 'origin/main' into setcps-reset-only-on-shuffle 2023-03-23 22:35:57 +01:00
Felix Roos 8617b9164e do not reset cps before eval #517 2023-03-23 22:34:25 +01:00
Felix Roos 991e5f1b3c Merge pull request #538 from tidalcycles/improve-loading
improve initial loading + wait before eval
2023-03-23 22:27:31 +01:00
Felix Roos 55c533c947 improve initial loading + wait before eval 2023-03-23 21:56:20 +01:00
Felix Roos d17543d5d9 Merge pull request #537 from tidalcycles/fix-keypress-period
fix period key for dvorak + remove duplicated code
2023-03-23 21:40:19 +01:00
Felix Roos 73169563c4 fix period key for dvorak + remove duplicated code 2023-03-23 21:37:38 +01:00
Felix Roos d351cb9f7f Merge pull request #535 from tidalcycles/update-lerna
Update lerna
2023-03-23 11:44:56 +01:00
Felix Roos 251be60630 update package publishing guide 2023-03-23 11:39:33 +01:00
Felix Roos 18d7222804 Publish
- @strudel.cycles/core@0.7.2
 - @strudel.cycles/csound@0.7.1
 - @strudel.cycles/eval@0.7.1
 - @strudel.cycles/midi@0.7.1
 - @strudel.cycles/mini@0.7.2
 - @strudel.cycles/osc@0.7.1
 - @strudel.cycles/react@0.7.1
 - @strudel.cycles/serial@0.7.1
 - @strudel.cycles/soundfonts@0.7.1
 - @strudel.cycles/tonal@0.7.1
 - @strudel.cycles/tone@0.7.1
 - @strudel.cycles/transpiler@0.7.1
 - @strudel.cycles/webaudio@0.7.1
 - @strudel.cycles/webdirt@0.7.1
 - @strudel.cycles/xen@0.7.1
2023-03-23 11:34:07 +01:00
Felix Roos ff1c37d361 bump core + mini to 0.7.1 2023-03-23 11:26:23 +01:00
paikwiki 74ad521fc9 Fix typo on packages.mdx
I found a broken link.

Because it omits protocol, page move to wrong page(https://strudel.tidalcycles.org/github.com/tidalcycles/strudel)

I add protocol(https://) on that link.
2023-03-14 13:01:40 +09:00
211 changed files with 24461 additions and 14403 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
+5 -1
View File
@@ -39,4 +39,8 @@ server/samples/old
repl/stats.html
coverage
public/icons/apple-splash-*
dev-dist
dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
src-tauri/target
+9 -2
View File
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
```sh
npm login
npx lerna publish
# this will increment all the versions in package.json files of non private packages to selected versions
npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
+2 -2
View File
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
- [`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
-3
View File
@@ -2,7 +2,6 @@
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/eval/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
@@ -10,8 +9,6 @@ export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/tone/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/webdirt/index.mjs';
export * from './packages/xen/index.mjs';
+11 -13
View File
@@ -18,7 +18,6 @@
"build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet",
@@ -53,22 +52,21 @@
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
"dependency-tree": "^9.0.0"
},
"devDependencies": {
"@vitest/ui": "^0.25.7",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"eslint": "^8.28.0",
"@tauri-apps/cli": "^1.4.0",
"@vitest/ui": "^0.28.0",
"canvas": "^2.11.2",
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^6.5.1",
"prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1"
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.33.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
+113
View File
@@ -0,0 +1,113 @@
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightExtension,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
onDraw?.(haps, time, currentFrame);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor.view, time, haps);
}
}
+35
View File
@@ -0,0 +1,35 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setFlash = StateEffect.define();
export const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
+126
View File
@@ -0,0 +1,126 @@
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations) => {
view.dispatch({ effects: setMiniLocations.of(locations) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
};
const miniLocations = StateField.define({
create() {
return Decoration.none;
},
update(locations, tr) {
if (tr.docChanged) {
locations = locations.map(tr.changes);
}
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
// this is called on eval, with the mini locations obtained from the transpiler
// codemirror will automatically remap the marks when the document is edited
// create a mark for each mini location, adding the range to the spec to find it later
const marks = e.value
.filter(([from]) => from < tr.newDoc.length)
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
.map(
(range) =>
Decoration.mark({
id: range.join(':'),
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
attributes: { style: `background-color: #00CA2880` },
}).range(...range), // -> Decoration
);
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
}
}
return locations;
},
});
const visibleMiniLocations = StateField.define({
create() {
return { atTime: 0, haps: new Map() };
},
update(visible, tr) {
for (let e of tr.effects) {
if (e.is(showMiniLocations)) {
// this is called every frame to show the locations that are currently active
// we can NOT create new marks because the context.locations haven't changed since eval time
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
haps.set(id, hap);
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
return visible;
},
});
// // Derive the set of decorations from the miniLocations and visibleLocations
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
const iterator = state.field(miniLocations).iter();
const { haps } = state.field(visibleMiniLocations);
const builder = new RangeSetBuilder();
while (iterator.value) {
const {
from,
to,
value: {
spec: { id },
},
} = iterator;
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
let channels = getComputedStyle(swatch)
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
.slice(1)
.map((c) => parseFloat(c || 1));
document.body.removeChild(swatch);
// Get percentage of event
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
channels[3] *= percent;
*/
builder.add(
from,
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style: `outline: solid 2px ${color}` },
}),
);
}
iterator.next();
}
return builder.finish();
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
+3
View File
@@ -0,0 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
@@ -1,9 +1,8 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.7.0",
"description": "WebDirt integration for Strudel",
"name": "@strudel/codemirror",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
@@ -12,6 +11,7 @@
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
@@ -24,18 +24,24 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"WebDirt": "github:dktr0/WebDirt"
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3"
}
}
+139
View File
@@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
+2 -1
View File
@@ -1,4 +1,5 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
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;
let clearColor = '#22222210';
+28 -6
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, sequence } from './pattern.mjs';
import { Pattern, register, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs';
const controls = {};
@@ -222,6 +222,7 @@ const generic_params = [
*
* @name legato
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @noAutocomplete
* @example
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
@@ -508,7 +509,7 @@ const generic_params = [
* @superDirtOnly
*/
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
@@ -572,6 +573,15 @@ const generic_params = [
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'],
['voice'], // TODO: synth param
// voicings // https://github.com/tidalcycles/strudel/issues/506
['chord'], // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
['dictionary', 'dict'], // which dictionary to use for the voicings
['anchor'], // the top note to align the voicing to, defaults to c5
['offset'], // how the voicing is offset from the anchored position
['octaves'], // how many octaves are voicing steps spread apart, defaults to 1
[['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Sets the level of reverb.
*
@@ -749,13 +759,14 @@ const generic_params = [
['val'],
['cps'],
/**
* If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111).
* For now, if you're coming from tidal, just think clip = legato.
*
* @name clip
* @param {number | Pattern} active 1 or 0
* @param {number | Pattern} factor >= 0
* @example
* note("c a f e ~").s("piano").clip(1)
* note("c a f e").s("piano").clip("<.5 1 2>")
*
*/
['clip'],
@@ -810,4 +821,15 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls;
+1
View File
@@ -74,6 +74,7 @@ export class Cyclist {
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
}
setPattern(pat, autostart = false) {
+104 -2
View File
@@ -1,6 +1,6 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/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/>.
*/
@@ -13,8 +13,16 @@ export const getDrawContext = (id = 'test-canvas') => {
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}, 200);
});
}
return canvas.getContext('2d');
};
@@ -65,3 +73,97 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint };
return this;
};
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
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)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}
+7 -3
View File
@@ -1,6 +1,6 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.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/>.
*/
@@ -37,8 +37,12 @@ function safeEval(str, options = {}) {
}
export const evaluate = async (code, transpiler) => {
let meta = {};
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
// transform syntactically correct js code to semantically usable code
const transpiled = transpiler(code);
code = transpiled.output;
meta = transpiled;
}
// if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler };
@@ -48,5 +52,5 @@ export const evaluate = async (code, transpiler) => {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
return { mode: 'javascript', pattern: evaluated, meta };
};
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,8 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -0,0 +1,39 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());
@@ -0,0 +1,23 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -0,0 +1,31 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}
@@ -0,0 +1,112 @@
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.tidalcycles.org/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
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
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.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;
@@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
"vite": "^4.3.3"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
+5 -1
View File
@@ -32,7 +32,11 @@ export class Hap {
}
get duration() {
return this.whole.end.sub(this.whole.begin);
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
}
get endClipped() {
return this.whole.begin.add(this.duration);
}
wholeOrPart() {
+2
View File
@@ -18,11 +18,13 @@ export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
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';
export { default as gist } from './gist.js';
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.7.0",
"version": "0.8.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.33.0"
}
}
+65 -45
View File
@@ -472,15 +472,15 @@ export class Pattern {
/**
* Returns a new pattern with the given location information added to the
* context of every hap.
* @param {Number} start
* @param {Number} end
* @param {Number} start start offset
* @param {Number} end end offset
* @returns Pattern
* @noAutocomplete
*/
withLocation(start, end) {
withLoc(start, end) {
const location = {
start: { line: start[0], column: start[1], offset: start[2] },
end: { line: end[0], column: end[1], offset: end[2] },
start,
end,
};
return this.withContext((context) => {
const locations = (context.locations || []).concat([location]);
@@ -488,32 +488,6 @@ export class Pattern {
});
}
withMiniLocation(start, end) {
const offset = {
start: { line: start[0], column: start[1], offset: start[2] },
end: { line: end[0], column: end[1], offset: end[2] },
};
return this.withContext((context) => {
let locations = context.locations || [];
locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset,
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset,
},
};
});
return { ...context, locations };
});
}
/**
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
@@ -809,13 +783,14 @@ export class Pattern {
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (!dominant && hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
// run previously set trigger, if it exists
hap.context.onTrigger?.(...args);
onTrigger(...args);
},
// we need this to know later if the default trigger should still fire
dominantTrigger: dominant,
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
// when using multiple triggers, you cannot flip this flag to false again!
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
dominantTrigger: hap.context.dominantTrigger || dominant,
}),
);
}
@@ -1582,6 +1557,24 @@ export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
});
/**
* Allows dividing numbers via list notation using ":".
* Returns a new pattern with just numbers.
* @name ratio
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
if (!Array.isArray(v)) {
return v;
}
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
}),
);
//////////////////////////////////////////////////////////////////////
// Structural and temporal transformations
@@ -1677,6 +1670,9 @@ export const ply = register('ply', function (factor, pat) {
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor));
@@ -1703,6 +1699,9 @@ export const hurry = register('hurry', function (r, pat) {
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
return silence;
}
return pat._fast(Fraction(1).div(factor));
});
@@ -2029,7 +2028,7 @@ export const jux = register('jux', function (func, pat) {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().legato(.2)
* .scale('C minor').note().clip(.2)
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2179,6 +2178,7 @@ export const velocity = register('velocity', function (velocity, pat) {
/**
*
* Multiplies the hap duration with the given factor.
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
* @name legato
* @memberof Pattern
* @example
@@ -2241,17 +2241,21 @@ const _loopAt = function (factor, pat, cps = 1) {
.slow(factor);
};
/*
/**
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
* @name slice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
const slice = register(
export const slice = register(
'slice',
function (npat, ipat, opat) {
return npat.innerBind((n) =>
@@ -2259,9 +2263,9 @@ const slice = register(
opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o };
// Remember we must stay pure and avoid editing the object directly
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
return pure({ ...toAdd, ...o });
const begin = Array.isArray(n) ? n[i] : i / n;
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
return pure({ begin, end, _slices: n, ...o });
}),
),
);
@@ -2279,7 +2283,7 @@ const slice = register(
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
const splice = register(
export const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
@@ -2296,10 +2300,26 @@ const splice = register(
false, // turns off auto-patternification
);
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
});
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
),
);
/**
* Makes the sample fit the given number of cycles and cps value, by
* changing the speed. Please note that at some point cps will be
@@ -2313,6 +2333,6 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
*/
// TODO - global cps clock
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
+39 -8
View File
@@ -1,6 +1,6 @@
/*
pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/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/>.
*/
@@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
@@ -82,12 +83,12 @@ Pattern.prototype.pianoroll = function ({
ctx.fillRect(0, 0, w, h);
}
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.whole.end > t;
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -181,6 +190,7 @@ export function pianoroll({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = false,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -236,11 +246,11 @@ export function pianoroll({
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const isActive = event.whole.begin <= time && event.endClipped > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -267,6 +277,14 @@ export function pianoroll({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -284,7 +302,7 @@ export function pianoroll({
return this;
}
function getOptions(drawTime, options = {}) {
export function getDrawOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
@@ -293,5 +311,18 @@ function getOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
+49 -31
View File
@@ -3,6 +3,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
export function repl({
interval,
@@ -18,47 +19,26 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
beforeEval?.({ code });
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
// problem: when the code does contain a setCps after an awaited promise,
// the cps will be 1 until the promise resolves
// example:
/*
await new Promise(resolve => setTimeout(resolve,1000))
setCps(.5)
note("c a f e")
*/
// to make sure the setCps inside the code is called immediately,
// it has to be placed first
let { pattern } = await _evaluate(code, transpiler);
await beforeEval?.({ code });
let { pattern, meta } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern });
setPattern(pattern, autostart);
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
@@ -70,9 +50,47 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
return { scheduler, evaluate, start, stop, pause, setCps };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+1 -8
View File
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
*
@@ -191,13 +191,6 @@ export const chooseInWith = (pat, xs) => {
*/
export const choose = (...xs) => chooseWith(rand, xs);
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose = function (...xs) {
return chooseWith(this, xs);
};
+118
View File
@@ -0,0 +1,118 @@
import { Pattern } from './index.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
const radians = ((angle - 90) * Math.PI) / 180;
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
}
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
// draw spiral / segment of spiral
function spiralSegment(options) {
let {
ctx,
from = 0,
to = 3,
margin = 50,
cx = 100,
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
cap = 'round',
stretch = 1,
fromOpacity = 1,
toOpacity = 1,
} = options;
from *= stretch;
to *= stretch;
rotate *= stretch;
ctx.lineWidth = thickness;
ctx.lineCap = cap;
ctx.strokeStyle = color;
ctx.globalAlpha = fromOpacity;
ctx.beginPath();
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
ctx.moveTo(sx, sy);
const increment = 1 / 60;
let angle = from;
while (angle <= to) {
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
//ctx.lineWidth = angle*thickness;
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
ctx.lineTo(x, y);
angle += increment;
}
ctx.stroke();
}
Pattern.prototype.spiral = function (options = {}) {
const {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
} = 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,
});
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
+40
View File
@@ -0,0 +1,40 @@
/*test for issue 302 support alternative solmization types */
import { sol2note } from '../util.mjs';
import { test } from 'vitest';
import assert from 'assert';
test('solmization - letters', () => {
const result = sol2note(60, 'letters');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - solfeggio', () => {
const result = sol2note(60, 'solfeggio');
const expected = 'Do4';
assert.equal(result, expected);
});
test('solmization - indian', () => {
const result = sol2note(60, 'indian');
const expected = 'Sa4';
assert.equal(result, expected);
});
test('solmization - german', () => {
const result = sol2note(60, 'german');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - byzantine', () => {
const result = sol2note(60, 'byzantine');
const expected = 'Ni4';
assert.equal(result, expected);
});
test('solmization - japanese', () => {
const result = sol2note(60, 'japanese');
const expected = 'I4';
assert.equal(result, expected);
});
+1 -1
View File
@@ -1,6 +1,6 @@
/*
ui.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.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/>.
*/
+70 -8
View File
@@ -6,26 +6,29 @@ This program is free software: you can redistribute it and/or modify it under th
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
// turns the given note into its midi number representation
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
@@ -67,13 +70,14 @@ export const getFreq = (noteOrMidi) => {
return midiToFreq(noteToMidi(noteOrMidi));
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @deprecated only used in workshop (first-notes)
* @noAutocomplete
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
const pc = pcs[n % 12];
return pc + oct;
};
@@ -212,3 +216,61 @@ export const splitAt = function (index, value) {
};
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
const indian = [
'Sa',
'Re',
'Ga',
'Ma',
'Pa',
'Dha',
'Ni',
]; /*indian musical notes, seems like they do not use flats or sharps*/
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
const byzantine = [
'Ni',
'Pab',
'Pa',
'Voub',
'Vou',
'Ga',
'Dib',
'Di',
'Keb',
'Ke',
'Zob',
'Zo',
]; /*byzantine musical notes*/
const japanese = [
'I',
'Ro',
'Ha',
'Ni',
'Ho',
'He',
'To',
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
export const sol2note = (n, notation = 'letters') => {
const pc =
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.7.0",
"version": "0.8.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,11 +32,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.5",
"@csound/browser": "6.18.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
-3
View File
@@ -1,3 +0,0 @@
shift-parser
shift-reducer
!shift-traverser
-50
View File
@@ -1,50 +0,0 @@
# @strudel.cycles/eval
This package contains the strudel code transformer and evaluator.
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
## Deprecation Note
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
## Install
```sh
npm i @strudel.cycles/eval --save
```
## Example
```js
import { evalScope } from '@strudel.cycles/core';
import { evaluate } from '@strudel.cycles/eval';
evalScope(
import('@strudel.cycles/core'),
// import other strudel packages here
); // add strudel to eval scope
async function run(code) {
const { pattern } = await evaluate(code);
const events = pattern.firstCycle();
console.log(events.map((e) => e.show()).join('\n'));
}
run('sequence([a3, [b3, c4]])');
```
yields:
```js
(0/1 -> 1/2, 0/1 -> 1/2, a3)
(1/2 -> 3/4, 1/2 -> 3/4, b3)
(3/4 -> 1/1, 3/4 -> 1/1, c4)
```
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
## Dev Notes
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
-12
View File
@@ -1,12 +0,0 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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 { evaluate as _evaluate } from '@strudel.cycles/core';
import shapeshifter from './shapeshifter.mjs';
export const evaluate = async (code) => {
return _evaluate(code, shapeshifter);
};
-1
View File
@@ -1 +0,0 @@
export * from './evaluate.mjs';
-296
View File
@@ -1,296 +0,0 @@
/*
shapeshifter.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.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 { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
import { parseScriptWithLocation } from 'shift-parser';
import traverser from './shift-traverser/index.js';
const { replace } = traverser;
import {
LiteralStringExpression,
IdentifierExpression,
CallExpression,
StaticMemberExpression,
ReturnStatement,
ArrayExpression,
LiteralNumericExpression,
} from 'shift-ast';
import shiftCodegen from 'shift-codegen';
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export const minifyStrings = true;
export const wrappedAsync = false; // this is now handled by core evaluate by default
export const shouldAddReturn = true;
export default (_code) => {
const { code, addReturn } = wrapAsync(_code);
const ast = parseScriptWithLocation(disguiseImports(code));
const artificialNodes = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) {
parents.push(parent);
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
if (isSynthetic) {
return node;
}
// replace template string `xxx` with mini(`xxx`)
if (minifyStrings && isBackTickString(node)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// allows to use top level strings, which are normally directives... but we don't need directives
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
return { ...node, directives: [], statements: [wrapped] };
}
// replace double quote string "xxx" with mini('xxx')
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append',
};
if (
node.type === 'BinaryExpression' &&
operators[node.operator] &&
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
canBeOverloaded(node.left)
) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') {
arg = wrapFunction('reify', node.left);
}
return new CallExpression({
callee: new StaticMemberExpression({
property: operators[node.operator],
object: wrapFunction('reify', arg),
}),
arguments: [node.right],
});
}
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
const literal = node.arguments[0];
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) {
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
}
return new LiteralStringExpression({ value });
}
if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' });
}
}
if (
addLocations &&
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
isMarkable
) {
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
return reifyWithLocation(node, node, ast.locations, artificialNodes);
}
if (addMiniLocations) {
return addMiniNotationLocations(node, ast.locations, artificialNodes);
}
return node;
},
leave() {
parents.pop();
},
});
// add return to last statement (because it's wrapped in an async function artificially)
if (shouldAddReturn) {
addReturn(shifted);
}
const generated = undisguiseImports(codegen(shifted));
return generated;
};
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
// there shouldn't be any side-effects from this as this change does not affect
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
function disguiseImports(code) {
return code.replaceAll('import', '_mport'); // Must be the same length!
}
// Rename the renamed import statements back to "import"
function undisguiseImports(code) {
return code.replaceAll('_mport', 'import');
}
function wrapAsync(code) {
// wrap code in async to make await work on top level => this will create 1 line offset to locations
// this is why line offset is -1 in getLocationObject calls below
if (wrappedAsync) {
code = `(async () => {
${code}
})()`;
}
const addReturn = (ast) => {
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
body.statements = body.statements
.slice(0, -1)
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
};
return {
code,
addReturn,
};
}
function addMiniNotationLocations(node, locations, artificialNodes) {
const miniFunctions = ['mini', 'm'];
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
const str = node.arguments[0];
return minifyWithLocation(str, str, locations, artificialNodes);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
// 'c3'.mini or 'c3'.m
return minifyWithLocation(node.object, node, locations, artificialNodes);
}
return node;
}
function wrapFunction(name, ...args) {
return new CallExpression({
callee: new IdentifierExpression({ name }),
arguments: args,
});
}
function isBackTickString(node) {
return node.type === 'TemplateExpression' && node.elements.length === 1;
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'LiteralStringExpression') {
return false;
}
const loc = locations.get(node);
const snippet = code.slice(loc.start.offset, loc.end.offset);
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
}
// returns true if the given parents belong to a pattern argument node
// this is used to check if a node should receive a location for highlighting
function isPatternArg(parents) {
if (!parents.length) {
return false;
}
const ancestors = parents.slice(0, -1);
const parent = parents[parents.length - 1];
if (isPatternFactory(parent)) {
return true;
}
if (parent?.type === 'ArrayExpression') {
return isPatternArg(ancestors);
}
return false;
}
function hasModifierCall(parent) {
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
// need all prototypes of Pattern
return parent?.type === 'StaticMemberExpression';
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
}
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
function isPatternFactory(node) {
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
}
function canBeOverloaded(node) {
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapFunction('reify', literalNode),
property: 'withLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const wrapped = wrapFunction('mini', literalNode);
if (!addMiniLocations) {
artificialNodes.push(wrapped);
return wrapped;
}
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapped,
property: 'withMiniLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
function getLocationArguments(node, locations) {
const loc = locations.get(node);
const lineOffset = wrappedAsync ? -1 : 0;
return [
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.start.column }),
new LiteralNumericExpression({ value: loc.start.offset }),
],
}),
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.end.column }),
new LiteralNumericExpression({ value: loc.end.offset }),
],
}),
];
}
-68
View File
@@ -1,68 +0,0 @@
/*
index.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import _Spec from 'shift-spec';
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
// import { version } from '../package.json'
// Loading uncached estraverse for changing estraverse.Syntax.
import _estraverse from 'estraverse';
const estraverse = _estraverse.cloneEnvironment();
// Adjust estraverse members.
Object.keys(estraverse.Syntax)
.filter((key) => key !== 'Property')
.forEach((key) => {
delete estraverse.Syntax[key];
delete estraverse.VisitorKeys[key];
});
Object.assign(
estraverse.Syntax,
Object.keys(Spec).reduce((result, key) => {
result[key] = key;
return result;
}, {}),
);
Object.assign(
estraverse.VisitorKeys,
Object.keys(Spec).reduce((result, key) => {
result[key] = Spec[key].fields.map((field) => field.name);
return result;
}, {}),
);
// estraverse.version = version;
export default estraverse;
/* vim: set sw=4 ts=4 et tw=80 : */
-32
View File
@@ -1,32 +0,0 @@
/*
evaluate.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.test.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 { expect, describe, it } from 'vitest';
import { evaluate } from '../evaluate.mjs';
import { mini } from '@strudel.cycles/mini';
import * as strudel from '@strudel.cycles/core';
const { fastcat, evalScope } = strudel;
describe('evaluate', async () => {
await evalScope({ mini }, strudel);
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
it('Should evaluate strudel functions', async () => {
expect(await ev('pure("c3")')).toEqual(['c3']);
expect(await ev('cat("c3")')).toEqual(['c3']);
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
});
it('Scope should be extendable', async () => {
await evalScope({ myFunction: (...x) => fastcat(...x) });
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
});
it('Should evaluate simple double quoted mini notation', async () => {
expect(await ev('"c3"')).toEqual(['c3']);
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
});
});
-25
View File
@@ -1,25 +0,0 @@
/*
shapeshifter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.test.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 { describe, it, expect } from 'vitest';
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
describe('shapeshifter', () => {
it('Should shift simple double quote string', () => {
if (wrappedAsync) {
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
} else {
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
}
});
if (wrappedAsync) {
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
);
});
}
});
+3 -3
View File
@@ -105,7 +105,7 @@ Pattern.prototype.midi = function (output) {
hap.ensureObjectValue();
// calculate time
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
time = time * 1000 + timingOffset;
// destructure value
@@ -113,8 +113,8 @@ Pattern.prototype.midi = function (output) {
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = noteToMidi(note);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.7.0",
"version": "0.8.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -31,9 +31,9 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+22 -12
View File
@@ -1,4 +1,4 @@
// Generated by Peggy 2.0.1.
// Generated by Peggy 3.0.2.
//
// https://peggyjs.org/
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
}
}
var s = this.location.start;
var loc = this.location.source + ":" + s.line + ":" + s.column;
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
? this.location.source.offset(s)
: s;
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
if (src) {
var e = this.location.end;
var filler = peg$padEnd("", s.line.toString().length, ' ');
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n"
+ filler + " |\n"
+ s.line + " | " + line + "\n"
+ offset_s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^");
} else {
@@ -422,11 +425,11 @@ function peg$parse(input, options) {
}
}
function peg$computeLocation(startPos, endPos) {
function peg$computeLocation(startPos, endPos, offset) {
var startPosDetails = peg$computePosDetails(startPos);
var endPosDetails = peg$computePosDetails(endPos);
return {
var res = {
source: peg$source,
start: {
offset: startPos,
@@ -439,6 +442,11 @@ function peg$parse(input, options) {
column: endPosDetails.column
}
};
if (offset && peg$source && (typeof peg$source.offset === "function")) {
res.start = peg$source.offset(res.start);
res.end = peg$source.offset(res.end);
}
return res;
}
function peg$fail(expected) {
@@ -1511,18 +1519,20 @@ function peg$parse(input, options) {
}
function peg$parsemini() {
var s0, s1, s2, s3, s4;
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsews();
s2 = peg$parsequote();
if (s2 !== peg$FAILED) {
s3 = peg$parsestack_or_choose();
if (s3 !== peg$FAILED) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
s3 = peg$parsews();
s4 = peg$parsestack_or_choose();
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
+1 -1
View File
@@ -182,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
mini = ws quote ws sc:stack_or_choose ws quote
{ return sc; }
// ------------------ operators ---------------------------
+73 -53
View File
@@ -9,7 +9,7 @@ import * as strudel from '@strudel.cycles/core';
const randOffset = 0.0003;
const applyOptions = (parent, code) => (pat, i) => {
const applyOptions = (parent, enter) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
@@ -23,18 +23,14 @@ const applyOptions = (parent, code) => (pat, i) => {
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(
patternifyAST(op.arguments_.pulse, code),
patternifyAST(op.arguments_.step, code),
patternifyAST(op.arguments_.rotation, code),
);
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
} else {
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
}
break;
}
@@ -45,7 +41,7 @@ const applyOptions = (parent, code) => (pat, i) => {
break;
}
case 'tail': {
const friend = patternifyAST(op.arguments_.element, code);
const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
@@ -72,11 +68,14 @@ function resolveReplications(ast) {
);
}
export function patternifyAST(ast, code) {
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
resolveReplications(ast);
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
if (alignment === 'stack') {
return strudel.stack(...children);
@@ -84,7 +83,7 @@ export function patternifyAST(ast, code) {
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
? 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))));
@@ -111,7 +110,7 @@ export function patternifyAST(ast, code) {
return pat;
}
case 'element': {
return patternifyAST(ast.source_, code);
return enter(ast.source_);
}
case 'atom': {
if (ast.source_ === '~') {
@@ -121,66 +120,82 @@ export function patternifyAST(ast, code) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
// make sure whitespaces are not part of the highlight:
const actual = code?.split('').slice(start.offset, end.offset).join('');
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return strudel
.pure(value)
.withLocation(
[start.line, start.column + offsetStart, start.offset + offsetStart],
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
);
if (offset === -1) {
// skip location handling (used when getting leaves to avoid confusion)
return strudel.pure(value);
}
const [from, to] = getLeafLocation(code, ast, offset);
return strudel.pure(value).withLoc(from, to);
}
case 'stretch':
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
/* case 'scale':
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
const intervals = Scale.get(scale).intervals;
const pattern = patternifyAST(ast.source_);
tonic = tonic || 'C4';
// console.log('scale', ast, pattern, tonic, scale);
console.log('tonic', tonic);
return pattern.fmap((step: any) => {
step = Number(step);
if (isNaN(step)) {
console.warn(`scale step "${step}" not a number`);
return step;
}
const octaves = Math.floor(step / intervals.length);
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
return Note.transpose(tonic, interval || '1P');
}); */
/* case 'struct':
// TODO:
return strudel.silence; */
return enter(ast.source_).slow(enter(ast.arguments_.amount));
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return strudel.silence;
}
}
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
// value is expected without quotes!
const { start, end } = leaf.location_;
const actual = code?.split('').slice(start.offset, end.offset).join('');
// make sure whitespaces are not part of the highlight
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
};
// takes quoted mini string, returns ast
export const mini2ast = (code) => krill.parse(code);
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code) => {
const ast = mini2ast(code);
let leaves = [];
patternifyAST(
ast,
code,
(node) => {
if (node.type_ === 'atom') {
leaves.push(node);
}
},
-1,
);
return leaves;
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, offset = 0) => {
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
};
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const code = `"${str}"`;
const ast = krill.parse(code);
const ast = mini2ast(code);
return patternifyAST(ast, code);
});
return strudel.sequence(...pats);
};
// turns str mini string (without quotes) into pattern
// offset is the position of the mini string in the JS code
// each leaf node will get .withLoc added
// this function is used by the transpiler for double quoted strings
export const m = (str, offset) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code, null, offset);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = krill.parse(string);
// console.log('ast', ast);
const ast = mini2ast(string);
return patternifyAST(ast, string);
};
@@ -190,3 +205,8 @@ export function minify(thing) {
}
return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.7.0",
"version": "0.8.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,8 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"peggy": "^2.0.1",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.33.0"
}
}
+34 -1
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { mini } from '../mini.mjs';
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
@@ -185,3 +185,36 @@ describe('mini', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
});
describe('getLeafLocation', () => {
it('gets location of leaf nodes', () => {
const code = '"bd sd"';
const ast = mini2ast(code);
const bd = ast.source_[0].source_;
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
const sd = ast.source_[1].source_;
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
});
});
describe('getLeafLocations', () => {
it('gets locations of leaf nodes', () => {
expect(getLeafLocations('"bd sd"')).toEqual([
[1, 3], // bd columns
[4, 6], // sd columns
]);
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
[1, 3], // bd columns
[7, 9], // sd columns
[10, 12], // cp columns
[4, 5], // "2" columns
]);
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
[1, 3], // bd columns
[5, 6], // "2" columns
[7, 8], // "3" columns
]);
});
});
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.7.0",
"version": "0.8.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
@@ -40,7 +40,7 @@
"osc-js": "^2.4.0"
},
"devDependencies": {
"pkg": "^5.7.0",
"vite": "^3.2.2"
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
@@ -15,17 +15,18 @@
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+29 -16
View File
@@ -1,22 +1,34 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import {
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick();
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
async function init() {
// TODO: only import stuff when play is pressed?
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
@@ -31,7 +43,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@@ -40,7 +52,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@@ -49,7 +61,7 @@ stack(
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@@ -70,9 +82,10 @@ function App() {
code,
defaultOutput: webaudioOutput,
getTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
useHighlighting({
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
@@ -103,7 +116,7 @@ function App() {
}
}
},
[scheduler, evaluate, view],
[scheduler, evaluate, view, code],
),
);
return (
+17 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.7.0",
"version": "0.8.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -32,18 +32,19 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.3",
"@lezer/highlight": "^1.1.3",
"@replit/codemirror-emacs": "^6.0.0",
"@replit/codemirror-vim": "^6.0.6",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"@strudel/codemirror": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
@@ -51,14 +52,14 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+48 -196
View File
@@ -1,109 +1,38 @@
import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import strudelTheme from '../themes/strudel-theme';
import './style.css';
import { useCallback } from 'react';
import { autocompletion } from '@codemirror/autocomplete';
//import { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { EditorView } from '@codemirror/view';
import { emacs } from '@replit/codemirror-emacs';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200);
};
export const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
//const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
const staticExtensions = [
javascript(),
highlightField,
import { vim } from '@replit/codemirror-vim';
import _CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo } from 'react';
import strudelTheme from '../themes/strudel-theme';
import { strudelAutocomplete } from './Autocomplete';
import {
highlightExtension,
flashField,
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
// autocompletion({ override: [strudelAutocomplete] }),
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
];
flash,
highlightMiniLocations,
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
export { flash, highlightMiniLocations, updateMiniLocations };
const staticExtensions = [javascript(), flashField, highlightExtension];
export default function CodeMirror({
value,
onChange,
onViewChanged,
onSelectionChange,
onDocChange,
theme,
keybindings,
isLineNumbersDisplayed,
isAutoCompletionEnabled,
isLineWrappingEnabled,
fontSize = 18,
fontFamily = 'monospace',
options,
editorDidMount,
}) {
const handleOnChange = useCallback(
(value) => {
@@ -111,30 +40,52 @@ export default function CodeMirror({
},
[onChange],
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
},
[onViewChanged],
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.docChanged && onDocChange) {
onDocChange?.(viewUpdate);
}
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
},
[onSelectionChange],
);
const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = {
vim,
emacs,
};
if (bindings[keybindings]) {
return [...staticExtensions, bindings[keybindings]()];
_extensions.push(bindings[keybindings]());
}
return staticExtensions;
}, [keybindings]);
if (isAutoCompletionEnabled) {
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
return _extensions;
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
return (
<div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror
@@ -144,107 +95,8 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={basicSetup}
/>
</div>
);
}
let parenMark;
export const markParens = (editor, data) => {
const v = editor.getDoc().getValue();
const marked = getCurrentParenArea(v, data);
parenMark?.clear();
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
};
// returns { line, ch } from absolute character offset
export function offsetToPosition(offset, code) {
const lines = code.split('\n');
let line = 0;
let ch = 0;
for (let i = 0; i < offset; i++) {
if (ch === lines[line].length) {
line++;
ch = 0;
} else {
ch++;
}
}
return { line, ch };
}
// returns absolute character offset from { line, ch }
export function positionToOffset(position, code) {
const lines = code.split('\n');
if (position.line > lines.length) {
// throw new Error('positionToOffset: position.line > lines.length');
return 0;
}
let offset = 0;
for (let i = 0; i < position.line; i++) {
offset += lines[i].length + 1;
}
offset += position.ch;
return offset;
}
// given code and caret position, the functions returns the indices of the parens we are in
export function getCurrentParenArea(code, caretPosition) {
const caret = positionToOffset(caretPosition, code);
let open, i, begin, end;
// walk left
i = caret;
open = 0;
while (i > 0) {
if (code[i - 1] === '(') {
open--;
} else if (code[i - 1] === ')') {
open++;
}
if (open === -1) {
break;
}
i--;
}
begin = i;
// walk right
i = caret;
open = 0;
while (i < code.length) {
if (code[i] === '(') {
open--;
} else if (code[i] === ')') {
open++;
}
if (open === 1) {
break;
}
i++;
}
end = i;
return [begin, end].map((o) => offsetToPosition(o, code));
}
/*
export const markEvent = (editor) => (time, event) => {
const locs = event.context.locations;
if (!locs || !editor) {
return;
}
const col = event.context?.color || '#FFCA28';
// mark active event
const marks = locs.map(({ start, end }) =>
editor.getDoc().markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
//{ css: `background-color: ${col};border-radius:5px` },
),
);
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
setTimeout(() => {
marks.forEach((mark) => mark.clear());
// }, '+' + event.duration * 0.5);
}, event.duration * 1000);
}; */
+63 -50
View File
@@ -18,16 +18,24 @@ export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
onTrigger,
drawTime,
punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme,
keybindings,
isLineNumbersDisplayed,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard],
);
const {
code,
@@ -47,11 +55,23 @@ export function MiniRepl({
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
editPattern: (pat, id) => {
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime,
evalOnMount,
drawContext,
drawTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
const [view, setView] = useState();
@@ -65,7 +85,7 @@ export function MiniRepl({
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
@@ -82,7 +102,7 @@ export function MiniRepl({
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@@ -93,26 +113,6 @@ export function MiniRepl({
),
);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -121,7 +121,7 @@ export function MiniRepl({
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
return l.concat([e.detail]).slice(-8);
});
}
}, []),
@@ -129,33 +129,46 @@ export function MiniRepl({
return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
</div>
</div>
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
</div>
)}
<div className="overflow-auto relative">
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
{show && (
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{drawTime && (
{punchcard && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
+5 -1
View File
@@ -1,6 +1,6 @@
:root {
--background: #222;
--lineBackground: #22222250;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
@@ -24,3 +24,7 @@
.cm-theme-light {
width: 100%;
}
footer {
z-index: 0 !important;
}
+16 -6
View File
@@ -1,10 +1,18 @@
import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6';
import { useEffect, useRef, useState } from 'react';
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef(0);
const [miniLocations, setMiniLocations] = useState([]);
useEffect(() => {
if (view) {
updateMiniLocations(view, miniLocations);
}
}, [view, miniLocations]);
useEffect(() => {
if (view) {
if (pattern && active) {
@@ -17,12 +25,12 @@ function useHighlighting({ view, pattern, active, getTime }) {
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [round(begin), round(audioTime + 1 / 60)];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
highlightMiniLocations(view, begin, highlights.current);
} catch (err) {
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
highlightMiniLocations(view, 0, []);
}
frame = requestAnimationFrame(updateHighlights);
});
@@ -31,10 +39,12 @@ function useHighlighting({ view, pattern, active, getTime }) {
};
} else {
highlights.current = [];
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
highlightMiniLocations(view, 0, highlights.current);
}
}
}, [pattern, active, view]);
return { setMiniLocations };
}
export default useHighlighting;
+4 -3
View File
@@ -29,7 +29,8 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
@@ -46,9 +47,9 @@ function useStrudel({
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
beforeEval: async ({ code }) => {
setCode(code);
beforeEval?.();
await beforeEval?.();
},
afterEval: (res) => {
const { pattern: _pattern, code } = res;
+1 -1
View File
@@ -1,6 +1,6 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
export * from './components/MiniRepl'; // !SSR
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
-2
View File
@@ -22,8 +22,6 @@ export default defineConfig({
...Object.keys(peerDependencies),
...Object.keys(dependencies),
// TODO: find out which of below names are obsolete now
'@strudel.cycles/tone',
'@strudel.cycles/eval',
'@strudel.cycles/transpiler',
'acorn',
'@strudel.cycles/core',
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.7.0",
"version": "0.8.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
@@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
-2
View File
@@ -153,5 +153,3 @@ export function registerSoundfonts() {
);
});
}
registerSoundfonts();
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.7.0",
"version": "0.8.2",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"soundfont2": "^0.4.0"
},
"devDependencies": {
"node-fetch": "^3.2.6",
"vite": "^3.2.2"
"node-fetch": "^3.3.1",
"vite": "^4.3.3"
}
}
+2
View File
@@ -3,3 +3,5 @@ import './voicings.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
import './ireal.mjs';
+523
View File
@@ -0,0 +1,523 @@
// explore them here: https://codesandbox.io/s/voicing-explorer-ireal-47tkx5?file=/src/ireal.js:0-16036
// scraped via: https://codesandbox.io/s/ireal-midi-scraper-2-gjz2mr?file=/src/index.js
export const simple = {
2: ['1P 5P 8P 9M', '1P 5P 8P 9M 12P', '5P 8P 9M 12P'],
5: ['1P 5P 8P 12P', '5P 8P 12P 15P'],
6: ['1P 5P 6M 8P 10M', '1P 5P 8P 10M 13M', '3M 5P 8P 10M 13M', '5P 8P 10M 12P 13M'],
7: [
'1P 5P 7m 8P 10M',
'1P 7m 8P 10M 12P',
'3M 7m 8P 10M 12P',
'3M 7m 8P 10M 14m',
'3M 7m 10M 12P 15P',
'7m 10M 12P 14m 15P',
'7m 10M 12P 15P 17M',
],
9: [
'1P 5P 7m 9M 10M',
'1P 7m 9M 10M 12P',
'3M 7m 8P 9M 12P',
'7m 9M 10M 14m 15P',
'3M 7m 8P 12P 16M',
'7m 10M 12P 15P 16M',
],
11: ['1P 5P 7m 9M 11P', '5P 7m 8P 9M 11P', '7m 8P 9M 11P 12P', '7m 8P 11P 12P 16M'],
13: ['1P 6M 7m 9M 10M', '1P 7m 9M 10M 13M', '3M 7m 8P 9M 13M', '7m 8P 9M 10M 13M', '7m 9M 10M 13M 15P'],
69: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 8P 9M 13M', '5P 8P 9M 10M 13M'],
add9: ['1P 5P 8P 9M 10M', '1P 5P 9M 10M 12P', '3M 8P 9M 10M 12P', '3M 8P 9M 12P 15P', '5P 8P 9M 12P 17M'],
'+': [
'1P 3M 6m 8P 10M',
'1P 6m 8P 10M 13m',
'3M 6m 8P 10M 13m',
'3M 8P 10M 13m 15P',
'6m 8P 10M 13m 15P',
'6m 10M 13m 15P 17M',
],
o: ['1P 5d 8P 10m 12d', '3m 8P 10m 12d 15P', '5d 8P 10m 12d 15P'],
h: [
'3m 5d 7m 8P 10m',
'1P 5d 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
sus: ['1P 4P 5P 8P', '1P 4P 5P 8P 11P', '5P 8P 11P 12P', '5P 8P 11P 12P 15P'],
'^': ['1P 5P 8P 10M', '1P 5P 8P 10M 12P', '3M 5P 8P 10M 12P', '3M 8P 10M 12P 15P', '5P 8P 10M 12P 15P'],
'-': ['1P 3m 5P 8P 10m', '1P 5P 8P 10m 12P', '3m 5P 8P 10m 12P', '5P 8P 10m 12P 15P'],
'^7': ['1P 5P 7M 10M 12P', '1P 10M 12P 14M', '3M 8P 10M 12P 14M', '5P 8P 10M 12P 14M', '5P 8P 10M 14M 17M'],
'-7': [
'1P 3m 5P 7m 10m',
'1P 5P 7m 10m 12P',
'3m 7m 8P 10m 12P',
'3m 7m 8P 10m 14m',
'5P 7m 8P 10m 14m',
'7m 10m 12P 14m 15P',
'5P 8P 10m 14m 17m',
'7m 10m 12P 15P 17m',
],
'7sus': ['1P 5P 7m 8P 11P', '5P 8P 11P 12P 14m', '7m 8P 11P 12P 14m', '7m 11P 12P 14m 18P'],
h7: [
'3m 5d 7m 8P 10m',
'1P 5d 7m 10m 12d',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
o7: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
'^9': [
'1P 5P 7M 9M 10M',
'1P 7M 9M 10M 12P',
'3M 7M 8P 9M 12P',
'3M 7M 8P 12P 16M',
'5P 8P 10M 14M 16M',
'7M 8P 10M 12P 16M',
],
'^13': ['1P 6M 7M 9M 10M', '1P 7M 9M 10M 13M', '3M 7M 8P 9M 13M', '3M 7M 8P 13M 16M', '7M 8P 10M 13M 16M'],
'^7#11': ['1P 5P 7M 10M 12d', '3M 7M 8P 10M 12d', '1P 7M 10M 12d 14M', '3M 7M 8P 12d 14M', '5P 8P 10M 12d 14M'],
'^9#11': ['1P 3M 5d 7M 9M', '1P 7M 9M 10M 12d', '3M 7M 8P 9M 12d', '3M 8P 9M 12d 14M'],
'^7#5': ['1P 6m 7M 10M 13m', '3M 7M 8P 10M 13m', '6m 7M 8P 10M 13m'],
'-6': [
'1P 3m 5P 6M 8P',
'1P 5P 6M 8P 10m',
'3m 5P 6M 8P 10m',
'1P 5P 8P 10m 13M',
'3m 5P 8P 10m 13M',
'5P 8P 10m 12P 13M',
'5P 8P 10m 13M 15P',
],
'-69': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'3m 6M 9M 10m 12P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 9M 10m 13M',
'5P 8P 10m 13M 16M',
],
'-^7': ['1P 3m 5P 7M 10m', '1P 5P 7M 10m 12P', '3m 7M 8P 10m 12P', '5P 7M 8P 10m 14M', '5P 8P 10m 14M 17m'],
'-^9': ['1P 3m 5P 7M 9M', '1P 7M 9M 10m 12P', '3m 7M 8P 9M 12P', '5P 8P 9M 10m 14M'],
'-9': [
'1P 3m 5P 7m 9M',
'3m 5P 7m 8P 9M',
'3m 7m 8P 9M 12P',
'5P 8P 9M 10m 14m',
'3m 7m 9M 12P 15P',
'7m 10m 12P 15P 16M',
],
'-add9': ['1P 2M 3m 5P 8P', '1P 3m 5P 9M', '3m 5P 8P 9M 12P', '5P 8P 9M 10m 12P'],
'-11': [
'1P 3m 7m 9M 11P',
'3m 7m 8P 9M 11P',
'1P 4P 7m 10m 12P',
'5P 8P 11P 14m',
'3m 7m 9M 11P 15P',
'5P 8P 11P 14m 16M',
'7m 10m 12P 15P 18P',
],
'-7b5': [
'3m 5d 7m 8P 10m',
'1P 7m 10m 12d',
'1P 5d 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
h9: ['1P 7m 9M 10m 12d', '3m 7m 8P 9M 12d', '5d 8P 9M 10m 14m', '7m 10m 12d 15P 16M'],
'-b6': ['1P 5P 6m 8P 10m', '1P 5P 8P 10m 13m', '3m 5P 8P 10m 13m', '5P 8P 10m 13m', '5P 8P 10m 13m 15P'],
'-#5': ['1P 6m 8P 10m 13m', '3m 6m 8P 10m 13m', '6m 8P 10m 13m 15P'],
'7b9': ['1P 3M 7m 9m 10M', '3M 7m 8P 9m 10M', '3M 7m 8P 9m 14m', '7m 9m 10M 14m 15P'],
'7#9': ['1P 3M 7m 10m', '3M 7m 8P 10m 14m', '7m 10m 10M 14m 15P'],
'7#11': ['1P 3M 7m 10M 12d', '3M 7m 8P 10M 12d', '7m 10M 12d 14m 15P'],
'7b5': ['1P 3M 7m 10M 12d', '3M 7m 8P 10M 12d', '7m 10M 12d 14m 15P'],
'7#5': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P'],
'9#11': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9b5': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9#5': ['1P 7m 9M 10M 13m', '3M 7m 9M 10M 13m', '3M 7m 9M 13m 14m', '7m 10M 13m 14m 16M', '7m 10M 13m 16M 17M'],
'7b13': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P'],
'7#9#5': ['1P 3M 7m 10m 13m', '3M 7m 10m 13m 15P', '7m 10M 13m 15P 17m'],
'7#9b5': ['1P 3M 7m 10m 12d', '3M 7m 10m 12d 15P', '7m 10M 12d 15P 17m'],
'7#9#11': ['1P 3M 7m 10m 12d', '3M 7m 10m 12d 15P', '7m 10M 12d 15P 17m'],
'7b9#11': ['1P 7m 9m 10M 12d', '3M 7m 8P 9m 12d', '7m 8P 10M 12d 16m'],
'7b9b5': ['1P 7m 9m 10M 12d', '3M 7m 8P 9m 12d', '7m 8P 10M 12d 16m'],
'7b9#5': ['1P 7m 9m 10M 13m', '3M 7m 8P 9m 13m', '7m 9m 10M 13m 15P'],
'7b9#9': ['1P 3M 7m 9m 10m', '3M 7m 8P 9m 10m', '7m 8P 10M 16m 17m'],
'7b9b13': ['1P 7m 9m 10M 13m', '3M 7m 8P 9m 13m', '7m 9m 10M 13m 15P'],
'7alt': [
'3M 7m 8P 9m 12d',
'1P 7m 10m 10M 13m',
'3M 7m 8P 10m 13m',
'3M 7m 9m 12d 15P',
'3M 7m 10m 13m 15P',
'7m 10M 12d 15P 17m',
'7m 10M 13m 15P 17m',
],
'13#11': ['1P 6M 7m 10M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'13b9': ['1P 3M 6M 7m 9m', '1P 6M 7m 9m 10M', '3M 7m 9m 10M 13M', '3M 7m 10M 13M 16m', '7m 10M 13M 16m 17M'],
'13#9': ['1P 3M 6M 7m 10m', '3M 7m 8P 10m 13M', '7m 10M 13M 14m 17m'],
'7b9sus': ['1P 5P 7m 9m 11P', '5P 7m 8P 9m 11P', '7m 8P 11P 14m 16m'],
'7susadd3': ['1P 4P 5P 7m 10M', '5P 8P 10M 11P 14m', '7m 11P 12P 15P 17M'],
'9sus': ['1P 5P 7m 9M 11P', '5P 7m 8P 9M 11P', '7m 8P 9M 11P 12P', '7m 8P 11P 12P 16M'],
'13sus': ['1P 4P 6M 7m 9M', '1P 7m 9M 11P 13M', '5P 7m 9M 11P 13M', '7m 9M 11P 13M 15P'],
'7b13sus': ['1P 5P 7m 11P 13m', '5P 7m 8P 11P 13m', '7m 11P 13m 14m 15P'],
};
export const complex = {
2: ['1P 5P 6M 8P 9M', '1P 5P 8P 9M 12P', '5P 8P 9M 12P 13M', '5P 8P 9M 12P 15P'],
5: ['1P 5P 8P 12P', '1P 5P 8P 9M 12P', '5P 8P 12P 15P', '5P 8P 12P 15P 16M'],
6: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 9M 10M 13M', '5P 8P 9M 10M 13M', '3M 6M 9M 12P 15P'],
7: [
'1P 5P 7m 8P 10M',
'1P 7m 8P 10M 12P',
'3M 7m 8P 10M 12P',
'3M 7m 8P 10M 14m',
'3M 7m 10M 12P 15P',
'7m 10M 12P 14m 15P',
'7m 10M 12P 15P 17M',
'7m 10M 14m 17M 19P',
],
9: [
'1P 6M 7m 9M 10M',
'3M 7m 9M 10M 12P',
'1P 7m 9M 10M 13M',
'3M 7m 9M 10M 13M',
'3M 7m 9M 12P 15P',
'7m 10M 12P 13M 16M',
'7m 10M 13M 16M 17M',
'7m 10M 13M 16M 19P',
],
11: [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
13: [
'3M 7m 9M 10M 13M',
'3M 7m 9M 13M 15P',
'3M 7m 10M 13M 16M',
'7m 10M 12P 13M 16M',
'7m 10M 13M 16M 17M',
'7m 10M 13M 16M 19P',
],
69: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 9M 10M 13M', '5P 8P 9M 10M 13M', '3M 6M 9M 12P 15P'],
add9: [
'1P 5P 8P 9M 10M',
'1P 5P 9M 10M 12P',
'3M 8P 9M 10M 12P',
'3M 8P 9M 12P 15P',
'5P 8P 9M 10M 15P',
'5P 8P 9M 12P 17M',
],
'+': [
'1P 6m 8P 9M 10M',
'1P 6m 8P 10M 13m',
'3M 8P 9M 10M 13m',
'3M 8P 10M 13m 15P',
'6m 10M 13m 15P 16M',
'6m 10M 13m 15P 17M',
],
o: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
h: [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
sus: [
'1P 4P 5P 8P 9M',
'1P 4P 5P 8P 11P',
'1P 5P 8P 9M 11P',
'5P 8P 9M 11P 12P',
'5P 8P 11P 12P 13M',
'5P 8P 11P 13M 15P',
],
'^': [
'1P 3M 5P 6M 9M',
'1P 5P 8P 10M 12P',
'3M 5P 9M 10M 12P',
'1P 5P 8P 10M 13M',
'3M 8P 10M 13M 15P',
'5P 9M 10M 12P 15P',
],
'-': [
'1P 3m 5P 8P 10m',
'1P 3m 5P 9M 11P',
'3m 5P 8P 9M 11P',
'5P 8P 9M 10m 11P',
'1P 5P 9M 10m 12P',
'3m 5P 8P 10m 12P',
'5P 8P 10m 12P 15P',
],
'^7': [
'1P 6M 7M 9M 10M',
'3M 7M 9M 10M 12P',
'1P 7M 9M 10M 13M',
'3M 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'-7': [
'1P 3m 5P 7m 9M',
'1P 3m 5P 7m 10m',
'1P 5P 7m 10m 11P',
'3m 7m 8P 10m 11P',
'1P 5P 7m 10m 12P',
'3m 7m 9M 10m 12P',
'3m 7m 8P 10m 14m',
'5P 7m 9M 10m 14m',
'7m 10m 11P 14m 15P',
'7m 10m 12P 15P 16M',
'5P 8P 11P 14m 17m',
'7m 10m 12P 15P 17m',
],
'7sus': [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
h7: [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
o7: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
'^9': [
'1P 6M 7M 9M 10M',
'1P 7M 9M 10M 13M',
'3M 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 8P 9M 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'^13': [
'1P 6M 7M 9M 10M',
'1P 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 9M 10M 13M',
'3M 7M 8P 9M 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'^7#11': [
'1P 3M 5d 7M 9M',
'1P 7M 9M 10M 12d',
'3M 7M 9M 10M 12d',
'3M 7M 9M 12d 13M',
'3M 7M 10M 12d 14M',
'7M 10M 12d 13M 14M',
'7M 10M 12d 13M 16M',
'7M 10M 12d 14M 17M',
],
'^9#11': [
'1P 3M 5d 7M 9M',
'1P 7M 9M 10M 12d',
'3M 7M 9M 10M 12d',
'3M 7M 9M 12d 13M',
'3M 7M 9M 12d 14M',
'7M 10M 12d 14M 16M',
'7M 10M 12d 13M 16M',
],
'^7#5': ['1P 6m 7M 10M 13m', '3M 7M 9M 10M 13m', '3M 7M 10M 13m 14M', '7M 10M 13m 14M 16M', '7M 10M 13m 14M 17M'],
'-6': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'1P 5P 6M 10m 11P',
'3m 5P 6M 8P 11P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 10m 11P 13M',
'5P 8P 10m 13M 16M',
],
'-69': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'3m 6M 9M 10m 12P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 9M 10m 13M',
'5P 8P 10m 13M 16M',
],
'-^7': [
'1P 3m 5P 7M 9M',
'1P 5P 7M 10m 11P',
'3m 7M 9M 10m 11P',
'3m 7M 9M 10m 12P',
'3m 7M 9M 12P 14M',
'7M 10m 11P 12P 14M',
'7M 10m 12P 14M 16M',
],
'-^9': [
'1P 3m 5P 7M 9M',
'1P 5P 7M 10m 11P',
'3m 7M 9M 10m 11P',
'3m 7M 9M 10m 12P',
'3m 7M 9M 12P 14M',
'7M 10m 11P 12P 14M',
'7M 10m 12P 14M 16M',
],
'-9': [
'1P 3m 5P 7m 9M',
'1P 3m 7m 9M 11P',
'3m 7m 9M 10m 11P',
'3m 7m 9M 10m 12P',
'3m 7m 9M 10m 14m',
'3m 7m 9M 12P 15P',
'7m 10m 11P 14m 16M',
'7m 10m 12P 16M 18P',
],
'-add9': ['1P 2M 3m 5P 8P', '1P 3m 5P 9M', '3m 5P 8P 9M 12P', '5P 8P 9M 10m 12P'],
'-11': [
'3m 5P 7m 9M 11P',
'7m 9M 10m 11P',
'1P 4P 7m 10m 12P',
'3m 7m 9M 11P 12P',
'7m 9M 10m 11P 12P',
'3m 7m 9M 11P 14m',
'4P 10m 12P 14m',
'5P 8P 11P 14m',
'5P 8P 11P 14m 16M',
'7m 10m 12P 16M 18P',
'7m 10m 11P 16M 21m',
],
'-7b5': [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
h9: [
'3m 5d 7m 9M 11P',
'1P 7m 9M 10m 12d',
'3m 7m 9M 12d 14m',
'5d 8P 9M 10m 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 16M',
],
'-b6': ['1P 3m 5P 6m 8P', '3m 5P 8P 11P 13m', '5P 8P 10m 11P 13m'],
'-#5': ['1P 6m 8P 10m 13m', '3m 6m 8P 11P 13m', '6m 8P 10m 13m 15P'],
'7b9': ['1P 3M 7m 9m 10M', '3M 7m 8P 9m 10M', '3M 7m 8P 9m 14m', '7m 9m 10M 14m 15P'],
'7#9': ['1P 3M 7m 10m', '3M 7m 10m 10M 12P', '3M 7m 10m 12P 14m', '7m 10M 12P 14m 17m'],
'7#11': ['1P 3M 7m 9M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'7b5': ['1P 3M 7m 9M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'7#5': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P', '7m 10M 13m 14m 17M'],
'9#11': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9b5': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9#5': ['1P 7m 9M 10M 13m', '3M 7m 9M 10M 13m', '3M 7m 9M 13m 14m', '7m 10M 13m 14m 16M', '7m 10M 13m 16M 17M'],
'7b13': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P', '7m 10M 13m 14m 17M'],
'7#9#5': ['3M 7m 10m 10M 13m', '3M 7m 10m 13m 14m', '7m 10M 13m 14m 17m'],
'7#9b5': ['3M 7m 10m 10M 12d', '3M 7m 10m 12d 14m', '7m 10M 12d 14m 17m'],
'7#9#11': ['3M 7m 10m 10M 12d', '3M 7m 10m 12d 14m', '7m 10M 12d 14m 17m'],
'7b9#11': ['3M 7m 9m 10M 12d', '3M 7m 9m 12d 14m', '7m 8P 10M 12d 16m', '7m 10M 12d 14m 16m'],
'7b9b5': ['3M 7m 9m 10M 12d', '3M 7m 9m 12d 14m', '7m 8P 10M 12d 16m', '7m 10M 12d 14m 16m'],
'7b9#5': ['1P 7m 9m 10M 13m', '3M 7m 9m 10M 13m', '3M 7m 10M 13m 16m', '7m 10M 13m 14m 16m', '7m 10M 13m 16m 17M'],
'7b9#9': ['1P 3M 7m 9m 10m', '3M 7m 10m 13m 16m', '7m 10M 13m 16m 17m'],
'7b9b13': ['1P 7m 9m 10M 13m', '3M 7m 9m 10M 13m', '3M 7m 10M 13m 16m', '7m 10M 13m 14m 16m', '7m 10M 13m 16m 17M'],
'7alt': [
'3M 7m 8P 10m 13m',
'3M 7m 9m 12d 13m',
'3M 7m 9m 10m 13m',
'3M 7m 10m 13m 14m',
'3M 7m 9m 12d 14m',
'3M 7m 10m 13m 15P',
'3M 7m 10m 13m 16m',
'7m 10M 12d 14m 16m',
'7m 10M 12d 13m 16m',
'7m 10M 13m 15P 17m',
'7m 10M 13m 16m 17m',
'7m 10M 13m 16m 19d',
],
'13#11': ['3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'13b9': ['3M 7m 9m 10M 13M', '3M 7m 10M 13M 16m', '7m 10M 13M 16m 17M'],
'13#9': ['3M 7m 10m 10M 13M', '7m 10M 13M 14m 17m'],
'7b9sus': ['1P 5P 7m 9m 11P', '5P 7m 8P 9m 11P', '7m 8P 11P 14m 16m'],
'7susadd3': ['1P 4P 5P 7m 10M', '5P 8P 10M 11P 14m', '7m 11P 12P 15P 17M'],
'9sus': [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
'13sus': [
'1P 4P 6M 7m 9M',
'1P 7m 9M 11P 13M',
'4P 7m 9M 11P 13M',
'7m 9M 11P 13M 15P',
'7m 11P 13M 14m 16M',
'7m 11P 13M 16M 18P',
],
'7b13sus': ['1P 5P 7m 11P 13m', '5P 7m 8P 11P 13m', '7m 11P 13m 14m 15P'],
};
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.7.0",
"version": "0.8.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.33.0"
}
}
+152
View File
@@ -0,0 +1,152 @@
/*
tonleiter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonleiter.test.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 { describe, test, expect } from 'vitest';
import {
Step,
Note,
transpose,
pc2chroma,
rotateChroma,
chroma2pc,
tokenizeChord,
note2pc,
note2oct,
midi2note,
renderVoicing,
scaleStep,
} from '../tonleiter.mjs';
describe('tonleiter', () => {
test('Step ', () => {
expect(Step.tokenize('#11')).toEqual(['#', 11]);
expect(Step.tokenize('b13')).toEqual(['b', 13]);
expect(Step.tokenize('bb6')).toEqual(['bb', 6]);
expect(Step.tokenize('b3')).toEqual(['b', 3]);
expect(Step.tokenize('3')).toEqual(['', 3]);
expect(Step.tokenize('10')).toEqual(['', 10]);
// expect(Step.tokenize('asdasd')).toThrow();
expect(Step.accidentals('b3')).toEqual(-1);
expect(Step.accidentals('#11')).toEqual(1);
});
test('Note', () => {
expect(Note.tokenize('C##')).toEqual(['C', '##']);
expect(Note.tokenize('Bb')).toEqual(['B', 'b']);
expect(Note.accidentals('C#')).toEqual(1);
expect(Note.accidentals('C##')).toEqual(2);
expect(Note.accidentals('Eb')).toEqual(-1);
expect(Note.accidentals('Bbb')).toEqual(-2);
});
test('transpose', () => {
expect(transpose('F#', '3')).toEqual('A#');
expect(transpose('C', '3')).toEqual('E');
expect(transpose('D', '3')).toEqual('F#');
expect(transpose('E', '3')).toEqual('G#');
expect(transpose('Eb', '3')).toEqual('G');
expect(transpose('Ebb', '3')).toEqual('Gb');
});
test('pc2chroma', () => {
expect(pc2chroma('C')).toBe(0);
expect(pc2chroma('C#')).toBe(1);
expect(pc2chroma('C##')).toBe(2);
expect(pc2chroma('D')).toBe(2);
expect(pc2chroma('Db')).toBe(1);
expect(pc2chroma('Dbb')).toBe(0);
expect(pc2chroma('bb')).toBe(10);
expect(pc2chroma('f')).toBe(5);
expect(pc2chroma('c')).toBe(0);
});
test('rotateChroma', () => {
expect(rotateChroma(0, 1)).toBe(1);
expect(rotateChroma(0, -1)).toBe(11);
expect(rotateChroma(11, 1)).toBe(0);
expect(rotateChroma(11, 13)).toBe(0);
});
test('chroma2pc', () => {
expect(chroma2pc(0)).toBe('C');
expect(chroma2pc(1)).toBe('Db');
expect(chroma2pc(1, true)).toBe('C#');
expect(chroma2pc(2)).toBe('D');
expect(chroma2pc(3)).toBe('Eb');
});
test('tokenizeChord', () => {
expect(tokenizeChord('Cm7')).toEqual(['C', 'm7', undefined]);
expect(tokenizeChord('C#m7')).toEqual(['C#', 'm7', undefined]);
expect(tokenizeChord('Bb^7')).toEqual(['Bb', '^7', undefined]);
expect(tokenizeChord('Bb^7/F')).toEqual(['Bb', '^7', 'F']);
});
test('note2pc', () => {
expect(note2pc('C5')).toBe('C');
expect(note2pc('C52')).toBe('C');
expect(note2pc('Bb3')).toBe('Bb');
expect(note2pc('F')).toBe('F');
});
test('note2oct', () => {
expect(note2oct('C5')).toBe(5);
expect(note2oct('Bb3')).toBe(3);
expect(note2oct('C7')).toBe(7);
expect(note2oct('C10')).toBe(10);
});
test('midi2note', () => {
expect(midi2note(60)).toBe('C4');
expect(midi2note(61)).toBe('Db4');
expect(midi2note(61, true)).toBe('C#4');
});
test('scaleStep', () => {
expect(scaleStep([60, 63, 67], 0)).toBe(60);
expect(scaleStep([60, 63, 67], 1)).toBe(63);
expect(scaleStep([60, 63, 67], 2)).toBe(67);
expect(scaleStep([60, 63, 67], 3)).toBe(72);
expect(scaleStep([60, 63, 67], 4)).toBe(75);
expect(scaleStep([60, 63, 67], -1)).toBe(55);
expect(scaleStep([60, 63, 67], -2)).toBe(51);
expect(scaleStep([60, 63, 67], -3)).toBe(48);
expect(scaleStep([60, 63, 67], -4)).toBe(43);
});
test('renderVoicing', () => {
const dictionary = {
m7: [
'3 7 10 14', // b3 5 b7 9
'10 14 15 19', // b7 9 b3 5
],
};
expect(renderVoicing({ chord: 'Em7', anchor: 'Bb4', dictionary, mode: 'below' })).toEqual([
'G3',
'B3',
'D4',
'Gb4',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'D5', dictionary, mode: 'below' })).toEqual([
'Eb4',
'G4',
'Bb4',
'D5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'G5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 0 })).toEqual([70]); // Bb4
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 1 })).toEqual([74]); // D5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 4 })).toEqual([82]); // Bb5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', offset: 1 })).toEqual([
'Eb5',
'G5',
'Bb5',
'D6',
]);
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
// TODO: test with offset
});
});
+7 -7
View File
@@ -127,25 +127,25 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
*
* The root note defaults to octave 3, if no octave number is given.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2".scale("C2:major").note()
* n("0 2 4 6 4 2").scale("C:major")
* @example
* "0 2 4 6 4 2"
* .scale("C2:<major minor>")
* .note()
* n("[0,7] 4 [2,7] 4")
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* n(rand.range(0,12).segment(8).round())
* .scale("C:ritusen")
* .s("folkharp")
*/
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.join(' ');
scale = scale.flat().join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
+190
View File
@@ -0,0 +1,190 @@
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core';
import { Interval } from '@tonaljs/tonal';
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
const pcs = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
const sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const accs = { b: -1, '#': 1 };
export const pc2chroma = (pc) => {
const [letter, ...rest] = pc.split('');
return pcs.indexOf(letter.toLowerCase()) + rest.reduce((sum, sign) => sum + accs[sign], 0);
};
export const rotateChroma = (chroma, steps) => (chroma + (steps % 12) + 12) % 12;
export const chroma2pc = (chroma, sharp = false) => {
return (sharp ? sharps : flats)[chroma];
};
export function tokenizeChord(chord) {
const match = (chord || '').match(/^([A-G][b#]*)([^/]*)[/]?([A-G][b#]*)?$/);
if (!match) {
// console.warn('could not tokenize chord', chord);
return [];
}
return match.slice(1);
}
export const note2pc = (note) => note.match(/^[A-G][#b]?/i)[0];
export const note2oct = (note) => tokenizeNote(note)[2];
export const note2chroma = (note) => {
return pc2chroma(note2pc(note));
};
// TODO: test
export const midi2chroma = (midi) => midi % 12;
// TODO: test and use in voicing function
export const pitch2chroma = (x, defaultOctave) => {
if (isNoteWithOctave(x)) {
return note2chroma(x);
}
if (isNote(x)) {
//pc
return pc2chroma(x, defaultOctave);
}
if (typeof x === 'number') {
// expect midi
return midi2chroma(x);
}
};
export const step2semitones = (x) => {
let num = Number(x);
if (!isNaN(num)) {
return num;
}
return Interval.semitones(x);
};
export const x2midi = (x) => {
if (typeof x === 'number') {
return x;
}
if (typeof x === 'string') {
return noteToMidi(x);
}
};
// duplicate: util.mjs (does not support sharp flag)
export const midi2note = (midi, sharp = false) => {
const oct = Math.floor(midi / 12) - 1;
const pc = (sharp ? sharps : flats)[midi % 12];
return pc + oct;
};
export function scaleStep(notes, offset, octaves = 1) {
notes = notes.map((note) => (typeof note === 'string' ? noteToMidi(note) : note));
const octOffset = Math.floor(offset / notes.length) * octaves * 12;
offset = _mod(offset, notes.length);
return notes[offset] + octOffset;
}
// different ways to resolve the note to compare the anchor to (see renderVoicing)
let modeTarget = {
below: (v) => v.slice(-1)[0],
duck: (v) => v.slice(-1)[0],
above: (v) => v[0],
};
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
const [root, symbol] = tokenizeChord(chord);
const rootChroma = pc2chroma(root);
anchor = anchor?.note || anchor;
const anchorChroma = pitch2chroma(anchor);
const voicings = dictionary[symbol].map((voicing) =>
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
);
let minDistance, bestIndex;
// calculate distances up from voicing top notes
let chromaDiffs = voicings.map((v, i) => {
const targetStep = modeTarget[mode](v);
const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
if (minDistance === undefined || diff < minDistance) {
minDistance = diff;
bestIndex = i;
}
return diff;
});
const octDiff = Math.ceil(offset / voicings.length) * 12;
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
const voicing = voicings[indexWithOffset];
const targetStep = modeTarget[mode](voicing);
const anchorMidi = noteToMidi(anchor, 4) - chromaDiffs[indexWithOffset] + octDiff;
const voicingMidi = voicing.map((v) => anchorMidi - targetStep + v);
let notes = voicingMidi.map((n) => midi2note(n));
if (mode === 'duck') {
notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor));
}
if (n !== undefined) {
return [scaleStep(notes, n, octaves)];
}
return notes;
}
// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs
const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7];
const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B'];
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
export const accidentalOffset = (accidentals) => {
return accidentals.split('#').length - accidentals.split('b').length;
};
const accidentalString = (offset) => {
if (offset < 0) {
return 'b'.repeat(-offset);
}
if (offset > 0) {
return '#'.repeat(offset);
}
return '';
};
export const Step = {
tokenize(step) {
const matches = step.match(/^([#b]*)([1-9][0-9]*)$/);
if (!matches) {
throw new Error(`Step.tokenize: not a valid step: ${step}`);
}
const [accidentals, stepNumber] = matches.slice(1);
return [accidentals, parseInt(stepNumber)];
},
accidentals(step) {
return accidentalOffset(Step.tokenize(step)[0]);
},
};
export const Note = {
// TODO: support octave numbers
tokenize(note) {
return [note[0], note.slice(1)];
},
accidentals(note) {
return accidentalOffset(this.tokenize(note)[1]);
},
};
// TODO: support octave numbers
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
const noteLetter = Note.tokenize(note)[0]; // E
const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2"
const targetNote = noteLetters[(noteIndex + stepNumber - 1) % 8]; // G "G is a third above E"
const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C"
const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C"
const indexOffset = targetIndex - rootIndex; // 3 (E to G is normally a 3 semitones)
const stepIndex = steps.indexOf(stepNumber); // 4 ("3" is normally 4 semitones)
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")
+147 -4
View File
@@ -5,11 +5,79 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { stack, register } from '@strudel.cycles/core';
import { renderVoicing } from './tonleiter.mjs';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
import { complex, simple } from './ireal.mjs';
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
const guidetones = {
m7: ['3m 7m', '7m 10m'],
m9: ['3m 7m', '7m 10m'],
7: ['3M 7m', '7m 10M'],
'^7': ['3M 7M', '7M 10M'],
'^9': ['3M 7M', '7M 10M'],
69: ['3M 6M'],
6: ['3M 6M', '6M 10M'],
m7b5: ['3m 7m', '7m 10m'],
'7b9': ['3M 7m', '7m 10M'],
'7b13': ['3M 7m', '7m 10M'],
o7: ['3m 6M', '6M 10m'],
'7#11': ['3M 7m', '7m 10M'],
'7#9': ['3M 7m', '7m 10M'],
mM7: ['3m 7M', '7M 10m'],
m6: ['3m 6M', '6M 10m'],
};
const triads = {
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
const defaultDictionary = {
// triads
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
// sevenths chords
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
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' },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
@@ -38,6 +106,11 @@ export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
Object.assign(voicingRegistry, { [name]: { dictionary, range } });
};
// new call signature
export const registerVoicings = (name, dictionary, options = {}) => {
Object.assign(voicingRegistry, { [name]: { dictionary, ...options } });
};
const getVoicing = (chord, dictionaryName, lastVoicing) => {
const { dictionary, range } = voicingRegistry[dictionaryName];
return dictionaryVoicing({
@@ -50,6 +123,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
};
/**
* DEPRECATED: still works, but it is recommended you use .voicing instead (without s).
* Turns chord symbols into voicings, using the smoothest voice leading possible.
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
@@ -86,7 +160,76 @@ export const voicings = register('voicings', function (dictionary, pat) {
*/
export const rootNotes = register('rootNotes', function (octave, pat) {
return pat.fmap((value) => {
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
return root + octave;
const chord = value.chord || value;
const root = chord.match(/^([a-gA-G][b#]?).*$/)[1];
const note = root + octave;
return value.chord ? { note } : note;
});
});
/**
* Turns chord symbols into voicings. You can use the following control params:
*
* - `chord`: Note, followed by chord symbol, e.g. C Am G7 Bb^7
* - `dict`: voicing dictionary to use, falls back to default dictionary
* - `anchor`: the note that is used to align the chord
* - `mode`: how the voicing is aligned to the anchor
* - `below`: top note <= anchor
* - `duck`: top note <= anchor, anchor excluded
* - `above`: bottom note >= anchor
* - `offset`: whole number that shifts the voicing up or down to the next voicing
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
*
* All of the above controls are optional, except `chord`.
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
* @example
* voicing("<C Am F G>")
* @example
* n("0 1 2 3 4 5 6 7").chord("<C Am F G>").voicing()
*/
export const voicing = register('voicing', function (pat) {
return 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;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves });
return stack(...notes)
.note()
.set(rest); // rest does not include voicing controls anymore!
})
.outerJoin();
});
export function voicingAlias(symbol, alias, setOrSets) {
setOrSets = !Array.isArray(setOrSets) ? [setOrSets] : setOrSets;
setOrSets.forEach((set) => {
set[alias] = set[symbol];
});
}
// no symbol = major chord
voicingAlias('^', '', [simple, complex]);
Object.keys(simple).forEach((symbol) => {
// add aliases for "-" === "m"
if (symbol.includes('-')) {
let alias = symbol.replace('-', 'm');
voicingAlias(symbol, alias, [complex, simple]);
}
// add aliases for "^" === "M"
if (symbol.includes('^')) {
let alias = symbol.replace('^', 'M');
voicingAlias(symbol, alias, [complex, simple]);
}
});
registerVoicings('ireal', simple);
registerVoicings('ireal-ext', complex);
-34
View File
@@ -1,34 +0,0 @@
# @strudel.cycles/tone
This package adds Tone.js functions to strudel Patterns.
## Deprecation Note
This package will not be developed further. Consider using `@strudel.cycles/webaudio` as a replacement.
## Install
```sh
npm i @strudel.cycles/tone --save
```
## Example
The following example will create a pattern and play it back with tone.js:
```js
import { sequence, stack, State, TimeSpan } from '@strudel.cycles/core';
import { Tone, polysynth, osc, out } from '@strudel.cycles/tone';
const pattern = sequence('c3', ['eb3', stack('g3', 'bb3')]).tone(polysynth().set(osc('sawtooth4')).chain(out()));
document.getElementById('play').addEventListener('click', async () => {
await Tone.start();
Tone.getTransport().stop();
const events = pattern.query(new State(new TimeSpan(0, 4))).filter((e) => e.whole.begin.equals(e.part.begin));
events.forEach((event) =>
Tone.getTransport().schedule((time) => event.context.onTrigger(time, event), event.whole.begin.valueOf()),
);
Tone.getTransport().start('+0.1');
});
```
-1
View File
@@ -1 +0,0 @@
export * from './tone.mjs';
-40
View File
@@ -1,40 +0,0 @@
{
"name": "@strudel.cycles/tone",
"version": "0.7.0",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"tone": "^14.7.77"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
}
}
-18
View File
@@ -1,18 +0,0 @@
/*
tone.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/test/tone.test.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 '../tone.mjs';
import { pure } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
describe('tone', () => {
it('Should have working tone function', () => {
// const s = synth().chain(out()); // TODO: mock audio context?
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
const s = {};
expect(pure('c3').tone(s).firstCycleValues).toEqual(['c3']);
});
});
-106
View File
@@ -1,106 +0,0 @@
/*
tone.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/tone.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 { register } from '@strudel.cycles/core';
import * as _Tone from 'tone';
// import Tone from here, to make sure to get the same AudioContext
export const Tone = _Tone;
const {
Filter,
Gain,
Synth,
PolySynth,
MembraneSynth,
MetalSynth,
MonoSynth,
AMSynth,
DuoSynth,
FMSynth,
NoiseSynth,
PluckSynth,
Sampler,
getDestination,
Players,
} = Tone;
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
export const getDefaultSynth = () => {
const s = new PolySynth().chain(new Gain(0.5), getDestination());
s.set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
return s;
};
// what about
// https://www.charlie-roberts.com/gibberish/playground/
// with this function, you can play the pattern with any tone synth
export const tone = register('tone', function (instrument, pat) {
return pat.onTrigger((time, hap) => {
let note;
let velocity = hap.context?.velocity ?? 0.75;
if (instrument instanceof PluckSynth) {
note = getPlayableNoteValue(hap);
instrument.triggerAttack(note, time);
} else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Sampler) {
note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
} else if (instrument instanceof Players) {
if (!instrument.has(hap.value)) {
throw new Error(`name "${hap.value}" not defined for players`);
}
const player = instrument.player(hap.value);
// velocity ?
player.start(time);
player.stop(time + hap.duration.valueOf());
} else {
note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
}
});
});
// synth helpers
export const amsynth = (options) => new AMSynth(options);
export const duosynth = (options) => new DuoSynth(options);
export const fmsynth = (options) => new FMSynth(options);
export const membrane = (options) => new MembraneSynth(options);
export const metal = (options) => new MetalSynth(options);
export const monosynth = (options) => new MonoSynth(options);
export const noise = (options) => new NoiseSynth(options);
export const pluck = (options) => new PluckSynth(options);
export const polysynth = (options) => new PolySynth(options);
export const sampler = (options, baseUrl) =>
new Promise((resolve) => {
const s = new Sampler(options, () => resolve(s), baseUrl);
});
export const players = (options, baseUrl = '') => {
options = !baseUrl
? options
: Object.fromEntries(Object.entries(options).map(([key, value]) => [key, baseUrl + value]));
return new Promise((resolve) => {
const s = new Players(options, () => resolve(s));
});
};
export const synth = (options) => new Synth(options);
// effect helpers
export const vol = (v) => new Gain(v);
export const lowpass = (v) => new Filter(v, 'lowpass');
export const highpass = (v) => new Filter(v, 'highpass');
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
export const osc = (type) => ({ oscillator: { type } });
export const out = () => getDestination();
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.7.0",
"version": "0.8.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
@@ -31,12 +31,13 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"acorn": "^8.8.1",
"escodegen": "^2.0.0",
"estree-walker": "^3.0.1"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.8"
"vite": "^4.3.3",
"vitest": "^0.33.0"
}
}
+14 -8
View File
@@ -11,22 +11,20 @@ const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
describe('transpiler', () => {
it('wraps double quote string with mini and adds location', () => {
expect(transpiler('"c3"', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
expect(transpiler('stack("c3","bd sd")', simple)).toEqual(
"stack(mini('c3').withMiniLocation(6, 10), mini('bd sd').withMiniLocation(11, 18));",
);
expect(transpiler('"c3"', simple).output).toEqual("m('c3', 0);");
expect(transpiler('stack("c3","bd sd")', simple).output).toEqual("stack(m('c3', 6), m('bd sd', 11));");
});
it('wraps backtick string with mini and adds location', () => {
expect(transpiler('`c3`', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
expect(transpiler('`c3`', simple).output).toEqual("m('c3', 0);");
});
it('replaces note variables with note strings', () => {
expect(transpiler('seq(c3, d3)', simple)).toEqual("seq('c3', 'd3');");
expect(transpiler('seq(c3, d3)', simple).output).toEqual("seq('c3', 'd3');");
});
it('keeps tagged template literal as is', () => {
expect(transpiler('xxx`c3`', simple)).toEqual('xxx`c3`;');
expect(transpiler('xxx`c3`', simple).output).toEqual('xxx`c3`;');
});
it('supports top level await', () => {
expect(transpiler("await samples('xxx');", simple)).toEqual("await samples('xxx');");
expect(transpiler("await samples('xxx');", simple).output).toEqual("await samples('xxx');");
});
/* it('parses dynamic imports', () => {
expect(
@@ -36,4 +34,12 @@ describe('transpiler', () => {
}),
).toEqual("const {default: foo} = await import('https://bar.com/foo.js');");
}); */
it('collections locations', () => {
const { miniLocations } = transpiler(`s("bd", "hh oh")`, { ...simple, emitMiniLocations: true });
expect(miniLocations).toEqual([
[3, 5],
[9, 11],
[12, 14],
]);
});
});
+30 -63
View File
@@ -2,9 +2,10 @@ import escodegen from 'escodegen';
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import { isNoteWithOctave } from '@strudel.cycles/core';
import { getLeafLocations } from '@strudel.cycles/mini';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
let ast = parse(input, {
ecmaVersion: 2022,
@@ -12,18 +13,27 @@ export function transpiler(input, options = {}) {
locations: true,
});
let miniLocations = [];
const collectMiniLocations = (value, node) => {
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
miniLocations = miniLocations.concat(leafLocs);
};
walk(ast, {
enter(node, parent, prop, index) {
enter(node, parent /* , prop, index */) {
if (isBackTickString(node, parent)) {
const { quasis, start, end } = node;
const { quasis } = node;
const { raw } = quasis[0].value;
this.skip();
return this.replace(miniWithLocation(raw, node, simpleLocs));
emitMiniLocations && collectMiniLocations(raw, node);
return this.replace(miniWithLocation(raw, node));
}
if (isStringWithDoubleQuotes(node)) {
const { value, start, end } = node;
const { value } = node;
this.skip();
return this.replace(miniWithLocation(value, node, simpleLocs));
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
@@ -47,11 +57,14 @@ export function transpiler(input, options = {}) {
argument: expression,
};
}
const output = escodegen.generate(ast);
let output = escodegen.generate(ast);
if (wrapAsync) {
return `(async ()=>{${output}})()`;
output = `(async ()=>{${output}})()`;
}
return output;
if (!emitMiniLocations) {
return { output };
}
return { output, miniLocations };
}
function isStringWithDoubleQuotes(node, locations, code) {
@@ -66,64 +79,18 @@ function isBackTickString(node, parent) {
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
}
function miniWithLocation(value, node, simpleLocs) {
let locs;
const { start: fromOffset, end: toOffset } = node;
if (simpleLocs) {
locs = [
{
type: 'Literal',
value: fromOffset,
},
{
type: 'Literal',
value: toOffset,
},
];
} else {
const {
loc: {
start: { line: fromLine, column: fromColumn },
end: { line: toLine, column: toColumn },
},
} = node;
locs = [
{
type: 'ArrayExpression',
elements: [fromLine, fromColumn, fromOffset].map((value) => ({
type: 'Literal',
value,
})),
},
{
type: 'ArrayExpression',
elements: [toLine, toColumn, toOffset].map((value) => ({
type: 'Literal',
value,
})),
},
];
}
// with location
function miniWithLocation(value, node) {
const { start: fromOffset } = node;
return {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'mini',
},
arguments: [{ type: 'Literal', value }],
optional: false,
},
property: {
type: 'Identifier',
name: 'withMiniLocation',
},
type: 'Identifier',
name: 'm',
},
arguments: locs,
arguments: [
{ type: 'Literal', value },
{ type: 'Literal', value: fromOffset },
],
optional: false,
};
}
+82
View File
@@ -0,0 +1,82 @@
# @strudel/web
This package provides an easy to use bundle of multiple strudel packages for the web.
## Usage
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<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();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').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.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Via npm
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
```js
import { initStrudel } from '@strudel/web';
```
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
### Loading samples
By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click',
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
### Double vs Single Quotes
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
In `@strudel/web`, it does not matter which types of quotes you're using.
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!
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<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();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<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';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<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();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
<body>
<div id="app"></div>
<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 '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>
@@ -0,0 +1,18 @@
{
"name": "repl-example",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/web": "workspace:*"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<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({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
@@ -1,21 +1,17 @@
{
"name": "@strudel.cycles/eval",
"version": "0.7.0",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"name": "@strudel/web",
"version": "0.8.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"type": "module",
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
@@ -28,6 +24,9 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
@@ -35,16 +34,12 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"estraverse": "^5.3.0",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
"shift-parser": "^8.0.0",
"shift-spec": "^2019.0.0",
"shift-traverser": "^1.0.0"
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"@strudel.cycles/mini": "workspace:*",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3"
}
}
@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
+66
View File
@@ -0,0 +1,66 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
//export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
// import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds() /* , registerSoundfonts() */]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
window.initStrudel = initStrudel;
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.7.0",
"version": "0.8.2",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,9 +35,9 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"nanostores": "^0.7.4"
"nanostores": "^0.8.1"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+65 -33
View File
@@ -21,7 +21,7 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
@@ -45,6 +45,9 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
transpose = -midiDiff(closest); // semitones to repitch
sampleUrl = bank[closest][n % bank[closest].length];
}
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
let buffer = await loadBuffer(sampleUrl, ac, s, n);
if (speed < 0) {
// should this be cached?
@@ -91,6 +94,46 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
return Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
fn(key, value);
});
};
// allows adding a custom url prefix handler
// for example, it is used by the desktop app to load samples starting with '~/music'
let resourcePrefixHandlers = {};
export function registerSamplesPrefix(prefix, resolve) {
resourcePrefixHandlers[prefix] = resolve;
}
// finds a prefix handler for the given url (if any)
function getSamplesPrefixHandler(url) {
const handler = Object.entries(resourcePrefixHandlers).find(([key]) => url.startsWith(key));
if (handler) {
return handler[1];
}
return;
}
/**
* Loads a collection of samples to use with `s`
* @example
@@ -107,6 +150,11 @@ export const getLoadedBuffer = (url) => {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
@@ -130,39 +178,23 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
});
}
const { prebake, tag } = options;
Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl,
prebake,
tag,
});
});
processSampleMap(
sampleMap,
(key, value) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl,
prebake,
tag,
}),
baseUrl,
);
};
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank) {
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const {
s,
freq,
@@ -170,7 +202,7 @@ export async function onTriggerSample(t, value, onended, bank) {
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = 0, // if 1, samples will be cut off when the hap ends
clip = undefined, // if 1, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
@@ -188,7 +220,7 @@ export async function onTriggerSample(t, value, onended, bank) {
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -232,7 +264,7 @@ export async function onTriggerSample(t, value, onended, bank) {
out.disconnect();
onended();
};
const stop = (endTime, playWholeBuffer = !clip) => {
const stop = (endTime, playWholeBuffer = clip === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration;
+18
View File
@@ -85,7 +85,12 @@ export async function initAudioOnFirstClick() {
}
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
@@ -243,3 +248,16 @@ Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
-7
View File
@@ -1,7 +0,0 @@
# @strudel.cycles/webdirt
This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel!
## Deprecation Note
This package will not be developed further. Consider using `@strudel.cycles/webaudio` as a replacement.
-1
View File
@@ -1 +0,0 @@
export * from './webdirt.mjs';

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