diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 2de6670c..e6636a72 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -1,6 +1,9 @@ name: Build and Deploy to beta (warm.strudel.cc) -on: [workflow_dispatch] +on: + push: + branches: + - main # Allow one concurrent deployment concurrency: diff --git a/CHANGELOG.md b/CHANGELOG.md index a13e379d..19a502f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,62 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted... +## january 2026 + +- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851) +- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883) +- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889) +- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880) +- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by 1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833) +- 2026-01-11T11:37:37+01:00 fix: add trem to top level by froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887) +- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888) +- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836) +- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886) +- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885) +- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884) +- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810) +- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878) +- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877) +- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786) +- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826) +- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874) +- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872) +- 2026-01-04T02:01:07+01:00 Make stretch modulatable by glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870) +- 2026-01-02T18:44:53+01:00 Make pan modulatable by glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865) +- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850) +- 2026-01-01T21:39:05+01:00 fix: missing punctuation by eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860) +- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864) +- 2026-01-01T19:52:15+01:00 Feat: FX Chains by glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861) + +## december 2025 + +- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859) +- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858) +- 2025-12-28T14:20:03+01:00 dough repl fixes by froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855) +- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839) +- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817) +- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816) +- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818) +- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815) +- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832) +- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835) +- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831) +- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830) +- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827) +- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809) +- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781) +- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800) +- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820) +- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805) +- **2025-12-01 strudel.cc deployed** + ## november 2025 +- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796) +- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791) +- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787) +- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795) +- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794) - 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783) - 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754) - 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784) @@ -52,6 +106,7 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly - 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624) - 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630) - 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701) +- **2025-10-27 @strudel/core@1.2.5** - 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697) - 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685) - 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7f65b5b..b8a941fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,20 +4,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi ## Move to codeberg -We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. - -To update your local clone, you can run this command: - -``` -git remote set-url origin git@codeberg.org:uzu/strudel.git -``` - +Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github. ## Communication Channels -To get in touch with the contributors, either +To get in touch with the community, either -- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel +- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel) - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question @@ -36,22 +29,35 @@ Use one of the Communication Channels listed above and drop us a line or two! ## Share Music If you made some music with strudel, you can give back some love and share what you've done! -Your creation could also be part of the random selection in the REPL if you want. Use one of the Communication Channels listed above. +(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse) ## Improve the Docs -If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), -you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) +If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place. ## Propose a Feature -If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. -Maybe you even want to help with the implementation of that feature! +If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature! + +## Contribute a feature + +Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. + +At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion. + +### AI/LLM policy + +Strudel is a project handmade by humans, with thought and nuance. + +If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself. + +There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels. ## Report a Bug If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues). + Please check that it has not been reported before. ## Fix a Bug @@ -118,8 +124,7 @@ There are also eslint extensions / plugins for most editors. ## Running all CI Checks -When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests. -You can run the same check with `pnpm check` +When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`. ## Package Workflow diff --git a/README.md b/README.md index 779769d5..b1208e7b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # strudel Live coding patterns on the web -https://strudel.cc/ + - Try it here: - Docs: +- Source: https://codeberg.org/uzu/strudel/ + * Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**. - Technical Blog Post: - 1 Year of Strudel Blog Post: - 2 Years of Strudel Blog Post: + ## Running Locally After cloning the project, you can run the REPL locally: diff --git a/examples/buildless/web-component-no-iframe.html b/examples/buildless/web-component-no-iframe.html index 8afa85cb..ae4fb700 100644 --- a/examples/buildless/web-component-no-iframe.html +++ b/examples/buildless/web-component-no-iframe.html @@ -1,4 +1,5 @@ - + + - + diff --git a/website/src/components/HeadCommonNext.astro b/website/src/components/HeadCommonNext.astro index 9f323a7a..1f878bac 100644 --- a/website/src/components/HeadCommonNext.astro +++ b/website/src/components/HeadCommonNext.astro @@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL - + diff --git a/website/src/config.ts b/website/src/config.ts index f335b272..eb39d295 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -76,6 +76,7 @@ export const SIDEBAR: Sidebar = { { text: 'MIDI & OSC', link: 'learn/input-output' }, ], More: [ + { text: 'FAQ', link: 'learn/faq' }, { text: 'Recipes', link: 'recipes/recipes' }, { text: 'Mini-Notation', link: 'learn/mini-notation' }, { text: 'Visual Feedback', link: 'learn/visual-feedback' }, diff --git a/website/src/pages/dough/index.astro b/website/src/pages/dough/index.astro new file mode 100644 index 00000000..9909fc6d --- /dev/null +++ b/website/src/pages/dough/index.astro @@ -0,0 +1,15 @@ +--- +import HeadCommon from '../../components/HeadCommon.astro'; +import Dough from '../../components/Dough/Dough.astro'; +--- + + + + + Strudel Dough REPL + + + + + + diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx new file mode 100644 index 00000000..e43839dc --- /dev/null +++ b/website/src/pages/learn/faq.mdx @@ -0,0 +1,331 @@ +--- +title: Frequently Asked Questions +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Frequently Asked Questions + +This page contains frequently asked questions, with answers. Usually, the topic is explained in more detail in a section which is linked in the answer. + +## Is Strudel/Tidal free? + +Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. However if you can, please make a one-off or regular donation to our [opencollective fund](https://opencollective.com/tidalcycles), that supports the software and cultural development of Strudel and other Uzu languages. + +While there is no charge there are some caveats, e.g.: + +- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. +- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. + +## How to record or export audio? + +Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel: + +- Use the 'export' tab to render and download as an audio file. +- capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. +- use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. +- capture the audio/video stream using a capture tool such as [OBS](https://obsproject.com/fr), which is designed for live streaming, but also works very well for recording. +- don't record anything and code it again in front of your friends. + +## Can I use strudel with my IDE? + +Yes you can. There are experimental modes, made by community members, for several IDEs such as: + +- VS Code: [Strudel VS](https://marketplace.visualstudio.com/items?itemName=cmillsdev.strudelvs): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel), which is defunct. +- nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim) + +## How can I use my own samples? + +There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: + +- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded. +- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed. +- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples) + +## Can I use Strudel with AI/LLM tools? + +You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. +However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, +including by people with very shady motives. Many in the community are very wary of people training models +on their tunes that they've poured their love into. So please keep discussion and questions around AI and +LLMs to channels dedicated to the topic and be fully respectful of other people's work. + +Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those +answers for you, as generally they will be timewasting nonsense. + +Human questions are always welcome! + +## Where can I download loads of patterns to train my LLM? + +You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools) + +## How to run offline? + +Strudel works offline just fine! There are multiple techniques for this, see [this explanation](learn/pwa/#using-strudel-offline). + +## How to change tempo? How do I translate BPM to cpm? + +Strudel works in cycles, rather than beats, but if you assume a certain number of beats per cycle, you can convert between them. + +For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` +where BPM is your beats per minute. + +If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or +two bars into one cycle), adjust accordingly. + +## Where can I see all the functions? + +If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "reference" which lists all the functions of strudel. + +## Where can I see all the samples and synths? + +If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "sounds" which lists all the drum machines, samples and synths currently loaded. + +## How do I use this exactly like a DAW? + +Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards +sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and +transforming patterns in ways that can be hard to predict. + +If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations +executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these +operations. For example in Strudel, the 'arrange' and 'pick' methods are useful for sequencing patterns over time (see question on these later in this document). + +You might still find that the typical DAW workflow is not really adapted to live coding because, despite +both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding +to the medium of code, which might mean leaving more place to serendipity and writing code that you don't predict the output of. + +## Why doesn't everyone just use a DAW? + +There is no easy answer to this question. Here are some thoughts: + +- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. + +- Live coding has developed over decades as a distinct creative practice. For example, live coding artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. + +- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model, no user tracking or hidden features. We need open tools in the arts! - Live coders don't all shy away from using DAWs. Many use them all of the time, especially when it makes their life easier for... live coding! + +- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. + +- There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. + +## How can I interface Strudel with my favorite music software? What can I do with it? + +Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for communicating musical information. + +Other music software (or hardware!) can then listen to these messages and process them according to its capabilities. + +A simple example would be to send livecoded audio to a DAW like Ardour on different tracks and then use it to mix them. + +You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score. + +You could also send MIDI to your hardware synths, if you like their sound. + +## How do I use this in my closed source webgame or other software? + +You don't. You need to license your game to a free/open source license fulfill the [AGPLv3 license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) Strudel is distributed under. + +## How to play different patterns simultaneously? + +Using the $ operator, several patterns can be played at once: + + + +See also [stack](intro/#combining-patterns) + +## Is it possible to mute a pattern? + +With an additional underscore, a pattern can be muted. + + + +See also [hush](/learn/conditional-modifiers/#hush) + +## How can I arrange in Strudel using `mask`? + +With mini-notation, using the `<>` and `!` operators, you can try something like + +``` +.mask("<0!24 1!40>") +``` + +It mutes a pattern for 24 cycles and plays it for 40. You would gain 64 cycles total, a multiple of 2/4/8 commonly used in western music. + +If each cycle is a bar, as a starting point, you could write a mask like that for any pattern: + +``` +.mask("<0!16 0!16 0!16 0!16 0!16 0!10>") +``` + +It mutes it throughout. + +For arranging, you could add the same mask to each part and replace some zeroes with ones in your different masks to make parts play. + +If you use `.mask()` on different patterns mess up your counting, then patterns do not align anymore. +On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel. +You can make things quite lively and more organic with a little (controlled) interference, according to your own taste. +And you are free to arrange in cycles like 3, 6 or 9 too. + +To modify everything at once, you could try all and when, for example: + +``` +all(x=>x.when("<0!7 1>", x=>x.lpf(saw.range(200, 2000)))) +``` + +This would lowpass filter sweep everything every 8 cycles. + +## How can I arrange in Strudel using `arrange` or `pick`? + +Take [Pachelbel's Canon in D](https://en.wikipedia.org/wiki/Pachelbel%27s_Canon#Analysis) as an example which has 4 voices (one cello and 3 violins) which have repeating patterns, as seen in the link above. + +The following snipped defines the patterns as constants which can then be used for the different voices. `arrange` takes multiple arguments, which are each a number of cycles and a pattern which is played for the number of cycles, wrapped in `[]` If the pattern is shorter than the number, it is repeated. + +") + .color("grey").sound("gm_tremolo_strings:3") + const violin_p1 = note( + "<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>") + .color("blue") + const violin_p2 = note( + "<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>") + .color("green") + const violin_p3 = note( + "<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>") + .color("purple") + const violin_p4 = note( + "<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>") + .color("red") + +cello$: arrange( + [2, silence], + [18,cello]) + violin1$: arrange( +[4,silence], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4] +).sound("gm_tremolo_strings:0") +violin2$: arrange( + [6,silence], [2,violin_p1], + [2,violin_p2], [2,violin_p3], + [2,violin_p4], [2,violin_p1], + [2,violin_p2], [2,violin_p3] + ).sound("gm_tremolo_strings:1") + violin3$: arrange( +[8,silence], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4], +[2,violin_p1], [2,violin_p2] +).sound("gm_tremolo_strings:2") + + all(x => x.release(.2)) + +`} /> + +Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles. + +`pick` has better highlighting than `arrange`: + +") + .color("grey").sound("gm_tremolo_strings:3") + const violin_p1 = note( + "<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>") + .color("blue") + const violin_p2 = note( + "<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>") + .color("green") + const violin_p3 = note( + "<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>") + .color("purple") + const violin_p4 = note( + "<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>") + .color("red") + +const violins = [violin_p1, violin_p2, violin_p3, violin_p4] + +cello$: "<~@2 0@18>".pick([cello]) +violin1$: "<~@4 0@2 1@2 2@2 3@2 0@2 1@2 2@2 3@2>".pick(violins) +.sound("gm_tremolo_strings:0") +violin2$: "<~@6 0@2 1@2 2@2 3@2 0@2 1@2 2@2>".pick(violins) +.sound("gm_tremolo_strings:1") +violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins) +.sound("gm_tremolo_strings:2") +all(x => x.release(.2)) +`} /> + +The `pick` method also works with jsons which have named elements, which makes it easier to read, see the [here](/learn/conditional-modifiers/#pick). `pickRestart` restarts the pattern upon picking it which can make a difference if the duration of the pick indexes doesn't line up with the patterns which are picked - which is not the case here. + +Try adding `.punchcard()` after the `release(.2)` for a visualization. + +## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work? + +Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. + +They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts) + +You can find the instructions how to use that script in the readme.md there. + +## Is there difference between `n` and `note`? + +They are not aliases of each other, in contrast to `s` and `sound`. + +The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`). + +On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . + +The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. + +") + .scale("A:minor:pentatonic") + .s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`} +/> + +Note that `n` is not the only way that functions use indices, some take numbered patterns instead. + +## Is there a cheat sheet for all symbols? + +Yes! + +``` +' marks start and end of strings, is different from " +" marks start and end of single line patterns in mini notation, is different from ' +` marks start and end of patterns with line breaks in mini notation, is different from ' +[] used for patterns in mini notation, each item in it has the same length +<> used for patterns, alternates between items each cycle +{} historically used for polyrhythmic patterns. {a b c}%4 is the same as *4. +@3 elongates the item by a factor of 3 (other numbers work too, even non-integer, but for numbers between 0 and 1 you need a leading zero like this: @0.5) +@ after an item: elongates the item once (multiple @ work too c @ @ is the same as c@3) +_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3), see below for a different usage. +. this divides equal parts of a pattern and is called a foot. Can be used instead of [] like this: "1 6 7 8 . 2 . 3 . 4" is the same as "[1 6 7 8] 2 3 4" +- silence +~ also silence +x not silence (for the use in struct, any non-silence symbol works there) +b decrease by one semitone, i.e. flat, works for steps of scales, note names (but not midi numbers) and chord names +s increase by one semitone, i.e. sharp, works for steps of scales, note names (but not midi numbers) but not chord names +# increase by one semitone, i.e. sharp works for steps of scales, note names (but not midi numbers) and chord names +# also used in mondo notation +*3 play the sample or pattern at thrice the speed, fast(3) +!3 play the sample or pattern three times +/2 play the sample or pattern at half speed, slow(2) +? play the pattern sometimes +| once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles() +, play all items separated by it at the same time, i.e. stack() +: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects. +$: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names +_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. +``` diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 61db16f0..c9425792 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -11,9 +11,9 @@ import { JsDoc } from '../../docs/JsDoc'; You can optionally add some music metadata in your Strudel code, by using tags in code comments: ```js -// @title Hey Hoo -// @by Sam Tagada -// @license CC BY-NC-SA +// @title My Cool Song +// @by John Doe +// @license CC-BY-SA-4.0 ``` Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. @@ -24,22 +24,22 @@ You can also use comment blocks: ```js /* -@title Hey Hoo -@by Sam Tagada -@license CC BY-NC-SA +@title My Cool Song +@by John Doe +@license CC-BY-SA-4.0 */ ``` Or define multiple tags in one line: ```js -// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA +// @title My Cool Song @by John Doe @license CC-BY-SA-4.0 ``` The `title` tag has an alternative syntax using quotes (must be defined at the very begining): ```js -// "Hey Hoo" @by Sam Tagada +// "My Cool Song" @by John Doe ``` ## Tags list @@ -48,20 +48,22 @@ Available tags are: - `@title`: music title - `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe `) -- `@license`: music license(s), e.g. CC BY-NC-SA. Unsure? [Choose a creative commons license here](https://creativecommons.org/choose/) +- `@license`: music license(s), separated by comma. Each license should be specified by using the correct identifier in the [https://spdx.org/licenses/](SPDX License List). Example: CC-BY-SA-4.0. Unsure? [Choose a Creative Commons license here](https://creativecommons.org/choose/). - `@details`: some additional information about the music -- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.) -- `@genre`: music genre(s) (pop, jazz, etc) +- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.) +- `@genre`: music genre(s) (pop, jazz, etc.) - `@album`: music album name +Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values + ## Multiple values Some of them accepts several values, using the comma or new line separator, or duplicating the tag: ```js /* -@by Sam Tagada - Jimmy +@by John Doe + Jane Doe @genre pop, jazz @url https://example.com @url https://example.org @@ -72,11 +74,11 @@ You can also add optional prefixes and use tags where you want: ```js /* -song @by Sam Tagada -samples @by Jimmy +song @by John Doe +samples @by Jane Doe */ ... -note("a3 c#4 e4 a4") // @by Sandy +note("a3 c#4 e4 a4") // @by Sandy Sue ``` ## Multiline diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index 02ead743..8b09f12f 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -142,6 +142,8 @@ The "~" represents a rest, and will create silence between other events: +Alternatively, "-" can be used instead of "~". It means the same thing. + ## Parallel / polyphony Using commas, we can play chords. diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f74a4fb8..d8a22cf5 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -41,7 +41,7 @@ note("c3 [e3 g3]*2") is transpiled to: ```strudel -note(m('c3 [e3 g3]', 5)) +note(m('c3 [e3 g3]*2', 5)) ``` Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later. diff --git a/website/src/pages/technical-manual/sounds.mdx b/website/src/pages/technical-manual/sounds.mdx index 11078713..73ae0579 100644 --- a/website/src/pages/technical-manual/sounds.mdx +++ b/website/src/pages/technical-manual/sounds.mdx @@ -13,7 +13,7 @@ Let's take a closer look about how sounds are implemented in the webaudio output All sounds are registered in the sound map, using the the `registerSound` function: -```ts +```js function registerSound( name: string, // The name of the sound that should be given to `s`, e.g. `mysaw` // The function called by the scheduler to trigger the sound: @@ -35,34 +35,36 @@ When `registerSound` is called, it registers `{ onTrigger, data }` under the giv This might be a bit abstract, so here is a minimal example: -```js -registerSound( - 'mysaw', - (time, value, onended) => { - let { freq } = value; // destructure control params - const ctx = getAudioContext(); - // create oscillator - const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) }); - o.start(time); - // add gain node to level down osc - const g = new GainNode(ctx, { gain: 0.3 }); - // connect osc to gain - const node = o.connect(g); - // this function can be called from outside to stop the sound - const stop = (time) => o.stop(time); - // ended will be fired when stop has been fired - o.addEventListener('ended', () => { - o.disconnect(); - g.disconnect(); - onended(); - }); - return { node, stop }; - }, - { type: 'synth' }, -); -// use the sound -freq(220, 440, 330).s('mysaw'); -``` + { + let { freq } = value; // destructure control params + const ctx = getAudioContext(); + // create oscillator + const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) }); + o.start(time); + // add gain node to level down osc + const g = new GainNode(ctx, { gain: 0.3 }); + // connect osc to gain + const node = o.connect(g); + // this function can be called from outside to stop the sound + const stop = (time) => o.stop(time); + // ended will be fired when stop has been fired + o.addEventListener('ended', () => { + o.disconnect(); + g.disconnect(); + onended(); + }); + return { node, stop }; + }, + { type: 'synth' }, + ); + // use the sound + freq("220 440 330").s('mysaw');`} +/> You can actually use this code in the [REPL](https://strudel.cc/) and it'll work. After evaluating the code, you should see `mysaw` in listed in the sounds tab. diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx index fa53aa55..6ec41d4a 100644 --- a/website/src/pages/technical-manual/vim.mdx +++ b/website/src/pages/technical-manual/vim.mdx @@ -35,3 +35,24 @@ Troubleshooting - If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. - For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. + +## Adding custom keybindings + +To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script) + +Example: + +```javascript +// Map 'jk' to Escape in normal mode +Vim.map('jk', '', 'insert'); +// Map 'U' to :w (Evaluate the current code) +Vim.map('U', ':w', 'normal'); +// Map 'Q' to :q — Stop/pause playback +Vim.map('Q', ':q', 'normal'); +// Map 'J' to find next '$' (jump to next label) +Vim.map('J', '/\\$', 'normal'); +// Map 'K' to find previous '$' (jump to previous label) +Vim.map('K', '?\\$', 'normal'); +``` + +For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 1d659972..84deb055 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -377,4 +377,4 @@ insect [crow metal] - -, punchcard /> -Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes) +Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes). diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs new file mode 100644 index 00000000..2876388a --- /dev/null +++ b/website/src/repl/audiograph.mjs @@ -0,0 +1,651 @@ +/* +audiograph.mjs - show a svg view of the web audio API graph built during a playback +Copyright (C) 2025 Strudel contributors - see +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 . +*/ + +// main entry point is `debugAudiograph` +import { logger } from '@strudel/core'; +import { getAudioContext, getSuperdoughAudioController, webaudioOutput } from '@strudel/webaudio'; + +let mermaid = null; +let svgPanZoom = null; + +let running = false; +let hap_count = 0; + +let cache = new Map(); +const initCache = JSON.stringify({ + connect: [], + where: [], + disconnectAll: 0, + disconnectOne: 0, + hasStop: false, + stopCount: 0, + ac: null, + creation: null, +}); + +let toggleOrig; + +function stackTrace() { + var err = new Error(); + const stacktrace = err.stack; + const lines = stacktrace.split('\n'); + let lineIndex = lines.findIndex((line) => line !== 'Error' && !line.includes('audiograph.mjs')); + if (lines[lineIndex].includes('gainNode')) lineIndex++; + if (lines[lineIndex].includes('getWorklet')) lineIndex++; + const line = lines[lineIndex].replace(/\s*at\s/, '').replace('http', '@'); + let match; + match = line.match(/([^@]*)@.*packages(\/[^:]+:\d+:\d+)/); + if (match) { + return match[1].replace(/[^.:/a-zA-Z0-9]/g, '') + '@' + match[2].replace(/[^.:/a-zA-Z0-9]/g, ''); + } + return '@'; +} + +// This captures all AudioNodes lazily +// when an `.audioid` property is called +// no solution was found to hook +// AudioNode's constructor directly +let audioid = 0; +const lazyRegister = (o) => { + Object.defineProperty(o.prototype, 'audioid', { + get: function () { + if (!this._audioid) { + this._audioid = ++audioid; + const s = JSON.parse(initCache); + s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; + // special case for subclassed AudioNodes + // they are implemented in superdough but hard to get a reference on here + // they are not AudioScheduledSourceNodes anyway + if (['FeedbackDelayNode', 'VowelNode'].indexOf(s.type) === -1) { + s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; + } + s.ac = this.context?.constructor.name || 'AudioParam'; + s.creation = s.creation || stackTrace(); + cache.set(this._audioid, s); + } + return this._audioid; + }, + enumerable: false, + configurable: true, + }); +}; + +// extend a specific AudioNode's constructor +// necessary when creation is done direclty by +// calling the constructor +// eg: new GainNode(...) +const audioNodeHook = (node) => { + const name = node.prototype.constructor.name; + const PatchedNode = class AudiographNode extends node { + constructor(...args) { + super(...args); + // trigger the lazy register + this._audioid = this.audioid; + } + }; + PatchedNode._parentClassName = name; + window[name] = PatchedNode; +}; + +const drawMessage = async function (message) { + const element = document.querySelector('.strudel-mermaid'); + let gd = ''; + gd += '---\n'; + gd += 'config:\n'; + gd += ' flowchart:\n'; + gd += ' wrappingWidth: 600\n'; + gd += '---\n'; + gd += 'flowchart LR\n'; + gd += 'id[' + message.replaceAll(' ', ' ') + ']\n'; + + let { svg } = await mermaid.render('strudelSvgId', gd); + svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%'); + svg = svg.replaceAll('&nbsp;', ' '); + element.innerHTML = svg; +}; + +const drawDiagram = async function () { + const element = document.querySelector('.strudel-mermaid'); + let code = window.strudelMirror.code; + code = code.replace(/^await debugAudiograph.*\n?/gm, ''); + code = '// date: ' + new Date().toISOString() + '\n\n' + code; + code = '// host: ' + document.location.hostname + '\n' + code; + const codeLines = code.split(/(?:\n|\r\n?)/); + const maxLineLength = codeLines.reduce((memo, line) => Math.max(memo, line.length), 0); + + // https://mermaid.js.org/syntax/flowchart.html + let gd = ''; + gd += '---\n'; + gd += 'config:\n'; + gd += ' flowchart:\n'; + gd += ' wrappingWidth: ' + 14 * maxLineLength + '\n'; + gd += '---\n'; + gd += 'flowchart TB\n'; + gd += '\tsubgraph AG[STRUDEL AUDIOGRAPH]\n'; + + // seed graph builder with all + // unconnected nodes + let lookup = []; + cache.forEach((v, k) => { + if (v.connect.length === 0) lookup.push(k); + }); + const relations = []; + let curRelations; + const zombieCount = 0; + const sourceLoc = (stack) => { + if (stack === '@') return stack; + return stack.replace('@', '\n').replace('/superdough/', '/'); + }; + const label = (s) => { + const source = s.creation ? '\n' + sourceLoc(s.creation) : ''; + let lb = '[' + '**' + s.type + '**' + source + ']'; + if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']'; + return lb; + }; + const isConnectLeak = (s) => { + return ( + ['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 && + s.disconnectAll === 0 && + s.connect.length > s.disconnectOne + ); + }; + const isStopLeak = (s) => { + return s.hasStop && s.stopCount === 0; + }; + do { + curRelations = relations.length; + lookup.slice().forEach((n) => { + cache.forEach((v, k) => { + if (v.connect.indexOf(n) !== -1) { + if (lookup.indexOf(k) === -1) lookup.push(k); + gd += v.connect + .map((i) => { + if (lookup.indexOf(i) === -1) lookup.push(i); + if (relations.indexOf(k + '-' + i) === -1) { + relations.push(k + '-' + i); + return ( + '\t\tnode' + + k + + label(v) + + ' -- ' + + sourceLoc(v.where[0]) + + ' --> node' + + i + + label(cache.get(i)) + + '\n' + ); + } + }) + .join(''); + } + if (k === n) { + gd += v.connect + .map((i) => { + if (lookup.indexOf(i) === -1) lookup.push(i); + if (relations.indexOf(k + '-' + i) === -1) { + relations.push(k + '-' + i); + return ( + '\t\tnode' + + k + + label(v) + + ' -- ' + + sourceLoc(v.where[0]) + + ' --> node' + + i + + label(cache.get(i)) + + '\n' + ); + } + }) + .join(''); + } + }); + }); + } while (relations.length > curRelations /*&& lookup.length < 100*/); + // add orphan nodes + const inRelation = '-' + relations.join('-') + '-'; + cache.forEach((v, k) => { + if (!inRelation.includes('-' + k + '-')) { + gd += '\t\tnode' + k + label(v) + '\n'; + } + }); + + const codePlaceholder = 'm'.repeat(maxLineLength); + gd += '\tsubgraph LEGEND\n'; + gd += '\t\tlegend1[in AudioContext]\n'; + gd += '\t\tlegend2[[in OfflineAudioContext]]\n'; + gd += '\t\tlegend3[not disconnected]\n'; + gd += '\t\tlegend4[AudioParam]\n'; + gd += '\t\tlegend5[AudioDestinationNode]\n'; + gd += '\t\tlegend6[not stopped]\n'; + gd += '\tend\n'; + gd += '\tsubgraph CODE[Strudel Code]\n'; + // we use a codePlaceholder to + // - avoid problems with special chars + // - stop mermaid to split lines on space with multiple tspans + // - force mermaid to prepare a sufficiently sized zone + gd += '\ncode[' + (codePlaceholder + '
').repeat(codeLines.length) + ']\n'; + gd += '\tend\n'; + gd += '\tend\n'; + gd += '\tclassDef audioparam fill:#6f6;\n'; + gd += '\tclassDef destination fill:#99f;\n'; + gd += '\tclassDef connectleak fill:#f96,stroke:#f00,stroke-width:2px;\n'; + gd += '\tclassDef stopleak fill:#f55,stroke:#f00,stroke-width:2px;\n'; + gd += '\tclass legend3 connectleak;\n'; + gd += '\tclass legend4 audioparam;\n'; + gd += '\tclass legend5 destination;\n'; + gd += '\tclass legend6 stopleak;\n'; + cache.forEach((v, k) => { + if (isConnectLeak(v)) { + gd += '\tclass node' + k + ' connectleak;\n'; + } else if (isStopLeak(v)) { + gd += '\tclass node' + k + ' stopleak;\n'; + } + if (v.type === 'AudioParam') { + gd += '\tclass node' + k + ' audioparam;\n'; + } + if (v.type === 'AudioDestinationNode') { + gd += '\tclass node' + k + ' destination;\n'; + } + }); + let { svg } = await mermaid.render('strudelSvgId', gd); + + // put real code in code zone + let idx = 0; + const escapeHtml = (unsafe) => { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + svg = svg.replaceAll(codePlaceholder, () => escapeHtml(codeLines[idx++])); + + // improve sizing on web page + svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%'); + element.innerHTML = svg; + + // align the code lines + let svgText = document.querySelector('[id^=flowchart-code] text'); + svgText.setAttributeNS(null, 'style', 'text-anchor: start;'); + const svgElement = document.querySelector('svg'); + const svgLabel = document.querySelector('svg [id^=flowchart-code] .label'); + const transformList = svgLabel.transform.baseVal; + const svgTransform = svgElement.createSVGTransform(); + const tspans = Array.from(document.querySelectorAll('[id^=flowchart-code] tspan.text-inner-tspan')); + let tspansMaxLength = tspans.reduce((memo, tspan) => Math.max(memo, tspan.getComputedTextLength()), 0); + svgTransform.setTranslate(-tspansMaxLength / 2, 0); + transformList.appendItem(svgTransform); + + let doPan = false; + let eventsHandler; + let panZoom; + let mousepos; + + eventsHandler = { + haltEventListeners: ['mousedown', 'mousemove', 'mouseup'], + mouseDownHandler: function (ev) { + if (event.target.className == '[object SVGAnimatedString]') { + doPan = true; + mousepos = { + x: ev.clientX, + y: ev.clientY, + }; + } + }, + mouseMoveHandler: function (ev) { + if (doPan) { + panZoom.panBy({ + x: ev.clientX - mousepos.x, + y: ev.clientY - mousepos.y, + }); + mousepos = { + x: ev.clientX, + y: ev.clientY, + }; + window.getSelection().removeAllRanges(); + } + }, + mouseUpHandler: function (ev) { + doPan = false; + }, + init: function (options) { + options.svgElement.addEventListener('mousedown', this.mouseDownHandler, false); + options.svgElement.addEventListener('mousemove', this.mouseMoveHandler, false); + options.svgElement.addEventListener('mouseup', this.mouseUpHandler, false); + }, + destroy: function (options) { + options.svgElement.removeEventListener('mousedown', this.mouseDownHandler, false); + options.svgElement.removeEventListener('mousemove', this.mouseMoveHandler, false); + options.svgElement.removeEventListener('mouseup', this.mouseUpHandler, false); + }, + }; + panZoom = svgPanZoom('#strudelSvgId', { + zoomEnabled: true, + controlIconsEnabled: true, + fit: 1, + center: 1, + zoomScaleSensitivity: 0.4, + customEventsHandler: eventsHandler, + }); +}; + +const svgExport = async () => { + const a = document.createElement('a'); + document.body.appendChild(a); + a.style = 'display: none'; + + const selector = '.strudel-mermaid'; + const bbox = document.querySelector('svg g').getBBox(); + let transform, style; + // clean pan-zoom viewport + const pzViewport = document.querySelector('.svg-pan-zoom_viewport'); + if (pzViewport) { + transform = pzViewport.transform; + style = pzViewport.style; + pzViewport.setAttribute('transform', ''); + pzViewport.style = ''; + } + + const spzMin = await fetch('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/dist/svg-pan-zoom.min.js').then((res) => + res.text(), + ); + + const scriptContent = ''; + // prepare svg + const content = document + .querySelector(selector) + .innerHTML.replaceAll('
', '
') + // remove useless tags + .replace(/ { + if (k <= audioid) cache.delete(k); + }); +}; + +const postProcessing = async function () { + hap_count = 0; + await drawDiagram(); + + resetAudioOutput(audioid); +}; + +const defaultOptions = { + StopAfterHapCount: 10, + hapsBatch: 0, + maxEdges: 10000, + maxTextSize: 200000, + audioAPIBreathingRoomSec: 5, +}; + +// `StopAfterHapCount` : +// The player will auto-stop after hap count have +// been played. when StopAfterHapCount = 0, it will +// continue playing until 'stop' is clicked +// `audioAPIBreathingRoomSec` : +// how much time should we wait after 'stop' to let +// the audioAPI finish its tail of ondended calls +// `hapsBatch` : +// the AudioGraph will be displayed every hapsBatch haps +// when hapsBatch = 0, AudioGraph will only be displayed +// after and auto-stop or after 'stop' is clicked +// In hapsBatch mode you will probably see a trailing of +// non disconnected notes on the graph because the audio +// API may have some lag disconnecting them +// cf also audioAPIBreathingRoomSec +// `maxEdges` +// This is a mermaid.js config that forces a hard limit +// on the maximum number of Edges of a graph +// needs a reload to be taken into account +// `maxTextSize` +// This is a mermaid.js config that forces a hard limit +// on the maximum text size of a graph definition +// needs a reload to be taken into account + +export const debugAudiograph = async (argOptions = {}) => { + const options = Object.assign({}, defaultOptions, argOptions); + const { StopAfterHapCount, hapsBatch, maxEdges, maxTextSize, audioAPIBreathingRoomSec } = options; + const sm = window.strudelMirror; + const code = sm.code; + if (!code.match(/await\s+debugAudiograph/)) { + throw new Error('you need to call `await debugAudiograph()` for audiograph to work'); + } + const emptyOptions = /await\s+debugAudiograph\(\)/.exec(code); + if (emptyOptions) { + const cutCode = emptyOptions.index + emptyOptions[0].length - 1; + const codeOptions = JSON.stringify({ StopAfterHapCount: StopAfterHapCount }).replaceAll('"', ''); + sm.setCode(code.slice(0, cutCode) + codeOptions + code.slice(cutCode)); + } + + if (window.audiograph === undefined) { + const ag = (window.audiograph = {}); + + toggleOrig = sm.toggle; + + //////////////////////////////////////// + // step 1: web audio api instrumentation + //////////////////////////////////////// + + // path AudioNode & AudioParam + // to give them lazy ids + // this captures both `ac.createGain` + // and `new GainNode(..)` patterns + lazyRegister(AudioNode); + lazyRegister(AudioParam); + lazyRegister(PeriodicWave); + + const audioNodes = [ + AudioBufferSourceNode, + AudioWorkletNode, + AnalyserNode, + BiquadFilterNode, + ChannelMergerNode, + ChannelSplitterNode, + ConstantSourceNode, + ConvolverNode, + DelayNode, + DynamicsCompressorNode, + GainNode, + IIRFilterNode, + OscillatorNode, + PannerNode, + StereoPannerNode, + WaveShaperNode, + ]; + audioNodes.map((n) => { + if (n.prototype instanceof AudioScheduledSourceNode) { + const stopOrig = n.prototype.stop; + n.prototype.stop = function (...args) { + // stop called + const result = stopOrig.call(this, ...args); + const s = cache.get(this.audioid); + s.stopCount++; + return result; + }; + } + audioNodeHook(n); + }); + + // patch BaseAudioContext factory methods + // to capture the source reference + Object.getOwnPropertyNames(BaseAudioContext.prototype) + .filter((n) => n.startsWith('create') && ['createBuffer'].indexOf(n) === -1) + .map((name) => { + const orig = BaseAudioContext.prototype[name]; + BaseAudioContext.prototype[name] = function (...args) { + const result = orig.call(this, ...args); + const s = cache.get(result.audioid); + s.creation = stackTrace(); + return result; + }; + }); + + const connectOrig = AudioNode.prototype.connect; + AudioNode.prototype.connect = function (destination, ...args) { + const result = connectOrig.call(this, destination, ...args); + const s = cache.get(this.audioid); + s.connect.push(destination.audioid); + s.where.push(stackTrace()); + return result; + }; + + const disconnectOrig = AudioNode.prototype.disconnect; + AudioNode.prototype.disconnect = function (destination, ...args) { + const result = disconnectOrig.call(this, destination, ...args); + const s = cache.get(this.audioid); + if (s.connect.length) { + if (destination) { + s.disconnectOne++; + } else { + s.disconnectAll++; + } + } else { + logger('WEIRD: node ' + this.audioid + 'called disconnect before any call to connect !'); + //logger(new Error().stack); + console.log(cache); + } + return result; + }; + + // call reset 2 times to handle reload + 'play' + // the first reset's disconnect adds audioid tags on previous outputs + // that were not tagged (wrong cutoff) + resetAudioOutput(audioid); + // the second reset has the correct audioid cutoff + resetAudioOutput(audioid); + + //////////////////////////////////////// + // step 2: Load external modules + //////////////////////////////////////// + + const { default: mermaidModule } = await import( + 'https://cdn.jsdelivr.net/npm/mermaid@11.12.1/dist/mermaid.esm.mjs' + ); + mermaid = mermaidModule; + + mermaid.initialize({ + startOnLoad: false, + themeCSS: '.flowchart { height: 100%; }', + maxEdges: maxEdges, + maxTextSize: maxTextSize, + htmlLabels: false, + flowchart: { + htmlLabels: false, + }, + }); + + const { default: svgPanZoomModule } = await import('https://esm.sh/svg-pan-zoom'); + svgPanZoom = svgPanZoomModule; + + ////////////////////////////////////////// + // step 3: UI modifications + ////////////////////////////////////////// + + // add audiograph panel + if (!document.querySelector('.strudel-mermaid')) { + const mermaidDiv = document.createElement('div'); + mermaidDiv.className = 'strudel-mermaid'; + mermaidDiv.style = 'min-height: 600px; width: 60%'; + const referenceNode = document.querySelector('#code'); + referenceNode.parentNode.insertBefore(mermaidDiv, referenceNode.nextSibling); + } + + // add svg export button + if (!document.querySelector('button[title=svg]')) { + const exportButton = document.createElement('button'); + exportButton.innerHTML = 'ExportDiagram'; + exportButton.title = 'svg'; + exportButton.onclick = svgExport; + const updateButton = document.querySelector('button[title=update]'); + updateButton.parentNode.insertBefore(exportButton, updateButton); + } + } + + if (!running) { + running = true; + } + + if (hapsBatch === 0 || hap_count < hapsBatch) { + let msg = ''; + msg += 'Recording activity...'; + msg += '\npress stop to build diagram'; + if (StopAfterHapCount) { + msg += '\nwill stop automatically in ' + Math.max(StopAfterHapCount - hap_count, 0) + ' haps'; + } + await drawMessage(msg); + } + + sm.toggle = async () => { + running = false; + sm.toggle = toggleOrig; + // schedule `toggle` on the js main loop + // to avoid interfering with any on-flight onTick + // not doing this can lead to a phase > 0 which will + // break the next start + setTimeout(sm.toggle.bind(sm), 0); + await drawMessage('please wait ' + audioAPIBreathingRoomSec + ' seconds\n' + 'the audio API is finishing its work'); + setTimeout(postProcessing, audioAPIBreathingRoomSec * 1000); + }; + + /*global all*/ + all((pat) => + pat.onTrigger(async (hap, duration, cps, t) => { + hap_count++; + const key = Object.entries(hap.value) + .map((param) => param.join('/')) + .join('/'); + + // if we reached StopAfterHapCount, click 'stop' + if (StopAfterHapCount && hap_count > StopAfterHapCount) { + if (running) { + await sm.toggle(); + } + // stop sending haps to superdough(...) + return; + } + + await webaudioOutput(hap, t, hap.duration / cps, cps, t); + + if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram(); + }), + ); +}; diff --git a/website/src/repl/components/panel/ExportTab.jsx b/website/src/repl/components/panel/ExportTab.jsx new file mode 100644 index 00000000..cbb132e4 --- /dev/null +++ b/website/src/repl/components/panel/ExportTab.jsx @@ -0,0 +1,197 @@ +import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; +import cx from '@src/cx.mjs'; +import NumberInput from '@src/repl/components/NumberInput'; +import { useEffect, useState } from 'react'; +import { Textbox } from '../textbox/Textbox'; +import { getAudioContext } from '@strudel/webaudio'; +import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; + +function Checkbox({ label, value, onChange, disabled = false }) { + return ( + + ); +} + +function FormItem({ label, children, disabled }) { + return ( +
+ + {children} +
+ ); +} + +export default function ExportTab(Props) { + const { handleExport } = Props; + + const [downloadName, setDownloadName] = useState(''); + const [startCycle, setStartCycle] = useState(0); + const [endCycle, setEndCycle] = useState(1); + const [sampleRate, setSampleRate] = useState(48000); + const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); + const [maxPolyphony, setMaxPolyphony] = useState(1024); + const [exporting, setExporting] = useState(false); + const [progress, setProgress] = useState(0); + const [length, setLength] = useState(1); + + const refreshProgress = () => { + const audioContext = getAudioContext(); + if (audioContext instanceof OfflineAudioContext) { + setProgress(audioContext.currentTime); + setLength(audioContext.length / sampleRate); + setTimeout(refreshProgress, 100); + } + }; + + return ( + <> +
+ + { + setDownloadName(e.target.value); + }} + onChange={(v) => { + setDownloadName(v); + }} + disabled={exporting} + placeholder="Leave empty to use current date" + className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} + value={downloadName ?? ''} + /> + +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 0 : Math.max(0, v); + setStartCycle(v); + }} + onChange={(v) => { + v = parseInt(v); + setStartCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')} + value={startCycle ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v); + setEndCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')} + value={endCycle ?? ''} + /> + +
+
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 1 : Math.max(1, v); + setSampleRate(v); + }} + onChange={(v) => { + v = parseInt(v); + setSampleRate(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={sampleRate ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={maxPolyphony ?? ''} + /> + +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val); + }} + disabled={exporting} + value={multiChannelOrbits} + /> +
+ +
+ + + ); +} diff --git a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx index c997f6b6..64ce198b 100644 --- a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx +++ b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx @@ -1,6 +1,7 @@ import { errorLogger } from '@strudel/core'; import { useSettings, storePrebakeScript } from '../../../settings.mjs'; import { SpecialActionInput } from '../button/action-button'; +import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs'; async function importScript(script) { const reader = new FileReader(); @@ -23,7 +24,19 @@ export function ImportPrebakeScriptButton() { type="file" label="import prebake script" accept=".strudel" - onChange={(e) => importScript(e.target.files[0])} + onChange={async (e) => { + const file = e.target.files[0]; + const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG); + if (!confirmed) { + return; + } + try { + await importScript(file); + window.location.reload(); + } catch (e) { + errorLogger(e); + } + }} /> ); } diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index b26c2ede..cfe90ae3 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -9,6 +9,7 @@ import { useLogger } from '../useLogger'; import { WelcomeTab } from './WelcomeTab'; import { PatternsTab } from './PatternsTab'; import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid'; +import ExportTab from './ExportTab'; const TAURI = typeof window !== 'undefined' && window.__TAURI__; @@ -80,6 +81,7 @@ const tabNames = { patterns: 'patterns', sounds: 'sounds', reference: 'reference', + export: 'export', console: 'console', settings: 'settings', }; @@ -126,6 +128,8 @@ function PanelContent({ context, tab }) { return ; case tabNames.reference: return ; + case tabNames.export: + return ; case tabNames.settings: return ; case tabNames.files: diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index f46fa661..7f488c54 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,13 +1,13 @@ import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; import { Textbox } from '../textbox/Textbox.jsx'; -import { isUdels } from '../../util.mjs'; +import { confirmAndReloadPage, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; -import { ActionButton, SpecialActionButton } from '../button/action-button.jsx'; +import { SpecialActionButton } from '../button/action-button.jsx'; import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; function Checkbox({ label, value, onChange, disabled = false }) { @@ -86,8 +86,6 @@ const fontFamilyOptions = { galactico: 'galactico', }; -const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; - export function SettingsTab({ started }) { const { theme, @@ -127,11 +125,8 @@ export function SettingsTab({ started }) { isDisabled={started} audioDeviceName={audioDeviceName} onChange={(audioDeviceName) => { - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('audioDeviceName', audioDeviceName); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('audioDeviceName', audioDeviceName); }); }} /> @@ -141,11 +136,8 @@ export function SettingsTab({ started }) { { - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('audioEngineTarget', target); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('audioEngineTarget', target); }); }} /> @@ -175,12 +167,9 @@ export function SettingsTab({ started }) { label="Multi Channel Orbits" onChange={(cbEvent) => { const val = cbEvent.target.checked; - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('multiChannelOrbits', val); - setMultiChannelOrbits(val); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('multiChannelOrbits', val); + setMultiChannelOrbits(val); }); }} value={multiChannelOrbits} @@ -297,11 +286,8 @@ export function SettingsTab({ started }) { label="Sync across Browser Tabs / Windows" onChange={(cbEvent) => { const newVal = cbEvent.target.checked; - confirmDialog(RELOAD_MSG).then((r) => { - if (r) { - settingsMap.setKey('isSyncEnabled', newVal); - window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('isSyncEnabled', newVal); }); }} disabled={shouldAlwaysSync} diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 52fbf928..5a7df5ba 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -210,7 +210,11 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' + ? search == '' + ? 'No sounds loaded' + : 'No sounds found' + : ''} ); diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index f1c72b12..697e83c9 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -393,6 +393,8 @@ samples({ bass: { d2: 'https://cdn.freesound.org/previews/608/608286_13074022-lq.mp3' } }) +useRNG('legacy') + stack( // bells n("0").euclidLegato(3,8) @@ -430,6 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3" // @by Felix Roos setcps(1) +useRNG('legacy') n("[-7*3],0,2,6,[8 7]") .echoWith( @@ -454,6 +457,8 @@ export const meltingsubmarine = `// "Melting submarine" // @by Felix Roos samples('github:tidalcycles/dirt-samples') +useRNG('legacy') + stack( s("bd:5,[~ ],hh27(3,4,1)") // drums .speed(perlin.range(.7,.9)) // random sample speed variation @@ -602,6 +607,9 @@ export const belldub = `// "Belldub" samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}}) // "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org + +useRNG('legacy') + stack( // bass note("[0 ~] [2 [0 2]] [4 4*2] [[4 ~] [2 ~] 0@2]".scale('g1 dorian').superimpose(x=>x.add(.02))) @@ -638,6 +646,7 @@ export const dinofunk = `// "Dinofunk" // @by Felix Roos setcps(1) +useRNG('legacy') samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) @@ -666,6 +675,8 @@ export const sampleDemo = `// "Sample demo" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useRNG('legacy') + stack( // percussion s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3") @@ -684,6 +695,8 @@ export const holyflute = `// "Holy flute" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useRNG('legacy') + "c3 eb3(3,8) c4/2 g3*2" .superimpose( x=>x.slow(2).add(12), @@ -699,6 +712,8 @@ export const flatrave = `// "Flatrave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useRNG('legacy') + stack( s("bd*2,~ [cp,sd]").bank('RolandTR909'), @@ -727,6 +742,8 @@ export const amensister = `// "Amensister" samples('github:tidalcycles/dirt-samples') +useRNG('legacy') + stack( // amen n("0 1 2 3 4 5 6 7") @@ -834,6 +851,8 @@ export const arpoon = `// "Arpoon" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useRNG('legacy') + samples('github:tidalcycles/dirt-samples') n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 8dcb6b75..0fbd89e3 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -9,11 +9,13 @@ import { getDrawContext } from '@strudel/draw'; import { evaluate, transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, + renderPatternAudio, webaudioOutput, resetGlobalEffects, resetLoadedSounds, initAudioOnFirstClick, resetDefaults, + initAudio, } from '@strudel/webaudio'; import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; @@ -36,6 +38,7 @@ import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; +import { debugAudiograph } from './audiograph'; const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; @@ -129,6 +132,7 @@ export function useReplContext() { bgFill: false, }); window.strudelMirror = editor; + window.debugAudiograph = debugAudiograph; // init settings initCode().then(async (decoded) => { @@ -207,6 +211,30 @@ export function useReplContext() { const handleEvaluate = () => { editorRef.current.evaluate(); }; + + const handleExport = async (begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) => { + await editorRef.current.evaluate(false); + editorRef.current.repl.scheduler.stop(); + await renderPatternAudio( + editorRef.current.repl.state.pattern, + editorRef.current.repl.scheduler.cps, + begin, + end, + sampleRate, + maxPolyphony, + multiChannelOrbits, + downloadName, + ).finally(async () => { + const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); + await initAudio({ + latestCode, + maxPolyphony, + audioDeviceName, + multiChannelOrbits, + }); + editorRef.current.repl.scheduler.stop(); + }); + }; const handleShuffle = async () => { const patternData = await getRandomTune(); const code = patternData.code; @@ -235,6 +263,7 @@ export function useReplContext() { handleShuffle, handleShare, handleEvaluate, + handleExport, init, error, editorRef, diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 4a7cb26a..06dad246 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,4 +1,4 @@ -import { code2hash, evalScope, hash2code, logger } from '@strudel/core'; +import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; @@ -107,7 +107,19 @@ export function confirmDialog(msg) { resolve(confirmed); }); } - +export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; +export function confirmAndReloadPage(onSuccess) { + confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => { + if (r == true) { + try { + onSuccess(); + return window.location.reload(); + } catch (e) { + errorLogger(e); + } + } + }); +} //RIP due to SPAM // let lastShared; // export async function shareCode(codeToShare) {