diff --git a/.github/FUNDING.yml b/.forgejo/FUNDING.yml similarity index 100% rename from .github/FUNDING.yml rename to .forgejo/FUNDING.yml diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 000000000..561fee658 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,37 @@ +name: Build and Deploy + +on: [workflow_dispatch] + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: docker + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + # cache: "pnpm" + - name: Install Dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Deploy + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file diff --git a/.github/workflows/test.yml b/.forgejo/workflows/test.yml similarity index 78% rename from .github/workflows/test.yml rename to .forgejo/workflows/test.yml index 180f87521..6b3759571 100644 --- a/.github/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -4,20 +4,22 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: docker strategy: matrix: node-version: [20] steps: - uses: actions/checkout@v4 + - name: apt install ztd + run: apt update && apt install -y zstd - uses: pnpm/action-setup@v4 with: version: 9.12.2 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' + cache: "pnpm" - run: pnpm install - run: pnpm run format-check - run: pnpm run lint diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 9a853c783..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build and Deploy - -on: [workflow_dispatch] - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - deployments: write - -# Allow one concurrent deployment -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 9.12.2 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "pnpm" - - name: Install Dependencies - run: pnpm install - - - name: Build - run: pnpm build - - - name: Setup Pages - uses: actions/configure-pages@v2 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: "./website/dist" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml deleted file mode 100644 index 07fd91be8..000000000 --- a/.github/workflows/tauri.yml +++ /dev/null @@ -1,62 +0,0 @@ -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 libasound2-dev - - - 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 diff --git a/.gitignore b/.gitignore index 59d9940e1..2be3ee698 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ fabric.properties samples/* !samples/README.md +.idea/ diff --git a/.prettierignore b/.prettierignore index 950e59f19..c9584aca0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ pnpm-workspace.yaml website/.astro !tidal-drum-machines.json !tidal-drum-machines-alias.json +.pnpm-store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d8170cfc..a7f65b5bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,12 +2,22 @@ Thanks for wanting to contribute!!! There are many ways you can add value to this project +## 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 +``` + + ## Communication Channels To get in touch with the contributors, either -- open a [github discussion](https://github.com/tidalcycles/strudel/discussions) or -- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel +- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question @@ -32,7 +42,7 @@ Use one of the Communication Channels listed above. ## 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 github via the "Edit this page" link located in the right sidebar. +you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) ## Propose a Feature @@ -41,7 +51,7 @@ Maybe you even want to help with the implementation of that feature! ## 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://github.com/tidalcycles/strudel/issues). +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 @@ -71,7 +81,7 @@ To get the project up and running for development, make sure you have installed: then, do the following: ```sh -git clone https://github.com/tidalcycles/strudel.git && cd strudel +git clone https://codeberg.org/uzu/strudel.git && cd strudel pnpm i # install at root to symlink packages pnpm start # start repl ``` @@ -113,7 +123,7 @@ You can run the same check with `pnpm check` ## Package Workflow -The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning. +The project is split into multiple [packages](https://codeberg.org/uzu/strudel/src/branch/main/packages) with independent versioning. When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/` to get the local version, allowing to develop multiple packages at the same time. @@ -140,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main ## useful commands + ```sh #regenerate the test snapshots (ex: when updating or creating new pattern functions) pnpm snapshot @@ -150,6 +161,81 @@ pnpm run osc #build the standalone version pnpm tauri build ``` + +## version tag patching + +here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions. + +the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version. +for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`. +if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to. +before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under: + +as an example, to release version 1.2, do the following: + +1. get date range + +```sh +# get date of last version: +git log -1 --format=%aI @strudel/core@1.1.0 +# 2024-05-31T23:07:26+02:00 + +# get date of current version: +git log -1 --format=%aI @strudel/core@1.2.0 +# 2025-05-01T12:39:24+02:00 +# might also use todays timestamp if version is not yet released +``` + +now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one). + +2. get patterns in question + +```sql +SELECT * +FROM code_v1 +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + +this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns! + +3. insert version tags + +we are now ready to insert the version tag to these patterns. +before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE hash = 'Ns2sMB40yIw4'; +``` + +after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +``` + +4. verify + +we can verify that the edits worked by querying all patterns that contain the new version tag: + +```sql +SELECT * +FROM code_v1 +WHERE code LIKE '%@version 1.1%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + + ## Have Fun Remember to have fun, and that this project is driven by the passion of volunteers! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b0c6618be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:24 + +WORKDIR /app + +RUN npm install pnpm --global + +COPY pnpm-workspace.yaml ./ +COPY package.json pnpm-lock.yaml ./ +COPY packages/ ./packages/ +COPY examples/ ./examples/ +RUN mkdir -p website/public +COPY website/package.json ./website/ + +RUN pnpm install + + +COPY . . + +EXPOSE 4321 + +CMD ["pnpm", "dev"] diff --git a/README.md b/README.md index 12ee85035..baaac82b2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # strudel -[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions) [![DOI](https://zenodo.org/badge/450927247.svg)](https://doi.org/10.5281/zenodo.6659278) - -An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully. +Live coding patterns on the web +https://strudel.cc/ - Try it here: - Docs: @@ -31,19 +30,13 @@ This project is organized into many [packages](./packages), which are also avail Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). -You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details. +You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details. Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository. ## Contributing -There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - - - - - -Made with [contrib.rocks](https://contrib.rocks). +There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors). ## Community @@ -52,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. + +We also have a mastodon account: social.toplap.org/@strudel diff --git a/paper/Makefile b/docs/iclc2023-paper/Makefile similarity index 100% rename from paper/Makefile rename to docs/iclc2023-paper/Makefile diff --git a/paper/README.md b/docs/iclc2023-paper/README.md similarity index 100% rename from paper/README.md rename to docs/iclc2023-paper/README.md diff --git a/paper/bin/code-filter.py b/docs/iclc2023-paper/bin/code-filter.py similarity index 100% rename from paper/bin/code-filter.py rename to docs/iclc2023-paper/bin/code-filter.py diff --git a/paper/citations.json b/docs/iclc2023-paper/citations.json similarity index 100% rename from paper/citations.json rename to docs/iclc2023-paper/citations.json diff --git a/paper/demo-preprocessed.md b/docs/iclc2023-paper/demo-preprocessed.md similarity index 99% rename from paper/demo-preprocessed.md rename to docs/iclc2023-paper/demo-preprocessed.md index e22823d90..2f0871e7c 100644 --- a/paper/demo-preprocessed.md +++ b/docs/iclc2023-paper/demo-preprocessed.md @@ -201,7 +201,7 @@ interfaces. The Strudel REPL is available at , including an interactive tutorial. The repository is at -, all the code is open source +, all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/demo.md b/docs/iclc2023-paper/demo.md similarity index 98% rename from paper/demo.md rename to docs/iclc2023-paper/demo.md index 23fe27754..841fb6064 100644 --- a/paper/demo.md +++ b/docs/iclc2023-paper/demo.md @@ -128,7 +128,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the GPL-3.0 License. +The repository is at , all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/demo.pdf b/docs/iclc2023-paper/demo.pdf similarity index 100% rename from paper/demo.pdf rename to docs/iclc2023-paper/demo.pdf diff --git a/paper/iclc-reviews.md b/docs/iclc2023-paper/iclc-reviews.md similarity index 100% rename from paper/iclc-reviews.md rename to docs/iclc2023-paper/iclc-reviews.md diff --git a/docs/iclc2023-paper/iclc2023.html b/docs/iclc2023-paper/iclc2023.html new file mode 100644 index 000000000..add08377f --- /dev/null +++ b/docs/iclc2023-paper/iclc2023.html @@ -0,0 +1,954 @@ + + + + + + + + Strudel: live coding patterns on the Web + + + + + + + +

Abstract

+
+

+ This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to + native JavaScript and the web. We begin by giving a little background of the first year of development, before + sharing some detail about its implementation and examples of use. We go on to outline the wide range of + synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and + CSound, and introduce Strudel’s REPL live editor, including its built-in visualisations. We then compare Strudel + with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by + Strudel for aligning patterns. +

+
+ +

1 Introduction

+

+ In the following paper, we introduce Strudel, an alternative implementation of the TidalCycles (or + ‘Tidal’ for short) live coding system, using the JavaScript programming language. Strudel is an attempt to make + live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidal’s + approach to algorithmic patterns + (Mclean 2020) up to modern audio/visual + web technologies. The Strudel REPL is a live code editor dedicated to manipulating patterns while they play, with + builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and + readability by applying code transformations on the syntax tree level, allowing language operations that would + otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio + Nodes, OSC (Open Sound Control) messages, Web Serial, Web MIDI and Csound. The project is split into multiple + packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from + many prior existing projects like TidalVortex + (McLean et al. 2022), Gibber + (Roberts and Kuchera-morin 2012), Estuary + (Ogborn et al. 2017), Hydra + (Jack [2022] 2022), Ocarina + (Solomon [2021] 2022) and Feedforward + (McLean 2020). This paper expands the Strudel + Demo paper for the Web Audio Conference 2022 + (Roos and McLean 2022). +

+

+ The first tentative commit to the Strudel project was on 22nd January 2022 by Alex McLean, with the core + representation implemented over the following few days. Although this was his first attempt at a JavaScript-based + application, by 27th January, Alex had managed to upload the initial version to the ‘npm’ javascript package + database, sharing with the wider community for comment. By 4th February, Felix Roos had discovered Strudel and + contributed a ‘REPL’ user interface to it, and then contributed a scheduler the next day, so that Strudel could + already make sound. At this point, Alex and Felix shared ownership to the repository, and the project has since + proved to be a productive confluence of Felix’s own work into music representation and visualisation, with Alex’s + experience with making Tidal. Felix has since become the primary contributor to Strudel, with Alex continuing to + jump between developing both Strudel and Tidal. Aspects of Strudel’s development have therefore fed back into + TidalCycles, and both systems have maintained a shared conceptual underpinning. We plan to continue working + towards feature parity between these systems, although within the syntactical trade-offs and library ecosystems of + JavaScript and Haskell, some divergence is inevitable and healthy. +

+

+ Over the first year of its life, Strudel is now a fully-fledged live coding environment, porting Tidal’s core + represention of patterns, pattern transformations, and mininotation for polymetric sequences, combined with a + wealth of features for synthesising and visualising those patterns. +

+

+ 2 From Tidal to Strudel and back +

+

+ As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell + pure functional programming language, and takes advantage of Haskell’s terse syntax and advanced, ‘strong’ type + system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. + Because Tidal leans heavily on many of Haskell’s more unique features, it was not always clear that it could + meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated + with an earlier port to Python [TidalVortex; + McLean et al. (2022)], and we have now + successfully implemented Tidal’s pure functional representation of patterns in Strudel, including partial + application, currying, and the functor, applicative and monadic structures that underlie Tidal’s expressive + pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, + or a function for combining and manipulating patterns, offering a rich creative ground for exploration. +

+

+ This development process has been far from a one-way port, however. The process of porting Tidal’s concepts has + also opened up new possibilities, some just from revisiting every design decision, and some from the particular + affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have + found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this + paper. +

+

+ 3 Representing Patterns +

+

+ Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, + adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can + output a set of events that happen within that time span. It depends on the structure of the Pattern how the + events are located in time. From now on, this process of generating events from a time span will be called + querying. Example: +

+
+
const pattern = sequence(c3, [e3, g3])
+const events = pattern.queryArc(0, 1)
+console.log(events.map(e => e.show()))
+
+

+ In this example, we create a pattern using the sequence function and query it for + the time span from 0 to 1. Those numbers represent units of time called + cycles. The length of one cycle depends on the tempo, which defaults to one cycle per second. The + resulting events are: +

+
+
[{ value: 'c3', begin: 0, end: 1/2 },
+{ value: 'e3', begin: 1/2, end: 3/4 },
+{ value: 'g3', begin: 3/4, end: 1 }]
+
+

+ Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, + the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second + half. This temporal placement is the result of the sequence function, which divides its arguments + equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the + example, e3 and g3 are divided equally over the second half of the whole cycle. +

+

+ The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has + to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query + the pattern for events, which are then scheduled as sound synthesis or other event triggers. Also, the above event + data structure has been simplified for readability. +

+
+ Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation. + +
+

4 Making Patterns

+

+ In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build + patterns using Strudel’s extensive combinator library to create, combine and transform patterns. +

+

+ The live coder will rarely use the sequence function as seen above, as sequencing is implicit in many + functions. For example in the following, the note function constructs a pattern of notes, sequencing + its arguments in the same manner as the previous example. +

+
+
note(c3, [e3, g3])
+
+

+ Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: [^This last + example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal + does not support passing sequences as lists directly to the note function, however.]. +

+
+
note("c3 [e3 g3]")
+
+

+ Such sequences are often treated only a starting point for manipulation, where they then undergo pattern + transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple + timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, + very long and complex generative results can be represented and manipulated without having to store the resulting + sequences in memory. +

+

5 Pattern Example

+

+ The following example showcases how patterns can be utilized to create musical complexity from simple parts, using + repetition and interference: +

+
+
"<0 2 [4 6](3,4,1) 3>"
+.off(1/4, add(2))
+.off(1/2, add(6))
+.scale('D minor')
+.legato(.25)
+.note().s("sawtooth square")
+.delay(.8).delaytime(.125)
+
+

+ The pattern starts with a rhythm of numbers in mini notation, which are later interpreted inside the scale of D + minor. The first line could also be expressed without mini notation: +

+
+
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
+
+

+ These numbers then undergo various pattern transformations. Here is a short description of all the functions used: +

+
    +
  • cat: play elements sequentially, where each lasts one cycle
  • +
  • brackets: elements inside brackets are divided equally over the time of their parent
  • +
  • + .euclid(p, s, o): place p pulses evenly over s steps, with offset o + (Toussaint 2005) +
  • +
  • + .off(n, f): layers a pattern on top of itself, with the new layer offset by n cycles, and with + function f applied +
  • +
  • .legato(n): multiply the duration of all events in a pattern by a factor of n
  • +
  • + .echo(t, n, v): copy each event t times, with n cycles in between each copy, decreasing velocity by + v +
  • +
  • .note(): interpretes values as notes
  • +
  • .s(name): play back each event with the given sound
  • +
  • .delay(wet): add delay
  • +
  • .delaytime(t): set delay time
  • +
+

Much of the above will be familiar to Tidal users.

+ +

+ 6 Ways to make Sound (and other events) +

+

To generate sound, Strudel supports bindings for different outputs:

+
    +
  • Tone.js (deprecated)
  • +
  • Web Audio API
  • +
  • WebDirt, a js recreation of Tidal’s Dirt sample engine (deprecated)
  • +
  • OSC via osc-js, compatible with superdirt
  • +
  • Csound via the Csound WebAssembly build
  • +
  • MIDI via WebMIDI
  • +
  • Serial via WebSerial
  • +
+

+ At first, we used Tone.js as sound output, but it proved to be limited for the use case of Strudel, where each + individual event could potentially have a completely different audio graph. While the Web Audio API takes a + fire-and-forget approach, creating a lot of Tone.js instruments and effects causes performance issues + quickly. For that reason, we chose to search for alternatives. +

+

+ Strudel’s new default output uses the Web Audio API to create a new audio graph for each event. It currently + supports basic oscillators, sample playback, various effects and an experimental support for soundfonts. +

+

+ WebDirt (Ogborn [2016] 2022) was created as part + of the Estuary Live Coding System + (Ogborn et al. 2017), and + proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more + directly with the Web Audio API to be able to integrate new features more tightly. +

+

+ Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy server, it is possible to send network + messages to trigger events. This is mainly used to render sound using SuperDirt + (SuperDirt [2015] 2022), which is the + well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard. +

+

+ Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to + Strudel. Thanks to the WebAssembly distribution of this classic system + (Yi, Lazzarini, and Costello 2018), Csound + ‘orchestra’ synthesisers can be embedded in and then patterned with Strudel code. +

+

+ MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same + device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only + from web browsers that support it (at the time of writing, this means Chromium-based browsers). +

+

+ Finally, Strudel supports Serial output, for example to trigger events via microcontrollers. This has already been + explored for robot choreography by Kate Sicchio and Alex McLean, via a performance presented at the International + Conference on Live Interfaces 2022. +

+

7 The Strudel REPL

+

+ While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the + Strudel REPL[^REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive + programming interface from computing heritage, usually for a commandline interface but also applied to live coding + editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating + Strudel patterns while they play. The REPL features built-in visual feedback, which highlights which elements in + the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is + designed to support both learning and live use of Strudel. +

+

+ Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor + powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available + synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps: +

+
    +
  1. + The user writes and updates code. Each update transpiles and evaluates it to create a + Pattern instance +
  2. +
  3. + While the REPL is running, the Scheduler queries the active Pattern by a regular + interval, generating Events (also known as Haps in Strudel) for the next time span. +
  4. +
  5. + For each scheduling tick, all generated Events are triggered by calling their + onTrigger method, which is set by the output. +
  6. +
+
+ REPL control flow + +
+

7.1 User Code

+

To create a Pattern from the user code, two steps are needed:

+
    +
  1. Transpile the JS input code to make it functional
  2. +
  3. Evaluate the transpiled code
  4. +
+

+ 7.1.1 Transpilation & Evaluation +

+

+ In the JavaScript world, using transpilation is a common practise to be able to use language features that are not + supported by the base language. Tools like babel will transpile code that contains unsupported + language features into a version of the code without those features. +

+

+ In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live + coding. For example, the Strudel REPL lets the user create mini notation patterns using just double quoted + strings, while single quoted strings remain what they are: +

+
+
"c3 [e3 g3]*2"
+
+

is transpiled to:

+
+
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
+
+

+ Here, the string is wrapped in mini, which will create a pattern from a mini notation string. + Additionally, the withMiniLocation method passes the original source code location of the string to + the pattern, which enables highlighting active events. +

+

+ Other convenient features like pseudo variables, operator overloading and top level await are possible with + transpilation. +

+

After the transpilation, the code is ready to be evaluated into a Pattern.

+

+ Behind the scenes, the user code string is parsed with acorn, turning it into an Abstract Syntax Tree + (AST). The AST allows changing the structure of the code before generating the transpiled version using + escodegen. +

+

7.1.2 Mini Notation

+

+ While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use + the Mini Notation as a more compact way to express rhythm. Strudel aims to provide the same Mini Notation features + and syntax as used in Tidal. +

+

+ The Mini Notation parser is implemented using peggy, which allows generating performant parsers for + Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the Mini Notation + string into an AST which is used to call the respective Strudel functions with the given structure. For example, + "c3 [e3 g3]*2" will result in the following calls: +

+
+
seq(
+  reify('c3').withLocation([1,1,1], [1,4,4]),
+  seq(
+    reify('e3').withLocation([1,5,5], [1,8,8]),
+    reify('g3').withLocation([1,8,8], [1,10,10]),
+  ).fast(2)
+)
+
+

+ 7.1.3 Highlighting Locations +

+

+ As seen in the examples above, both the JS and the Mini Notation parser add source code locations using + withMiniLocation and withLocation methods. While the JS parser adds locations relative + to the user code as a whole, the Mini Notation adds locations relative to the position of the mini notation + string. The absolute location of elements within Mini Notation can be calculated by simply adding both locations + together. This absolute location can be used to highlight active events in real time. +

+

7.2 Scheduling Events

+

+ After an instance of Pattern is obtained from the user code, it is used by the scheduler to get + queried for events. Once started, the scheduler runs at a fixed interval to query active pattern for events + withing the current interval’s time span. A simplified implementation looks like this: +

+
+
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
+let interval = 0.5; // query interval in seconds
+let time = 0; // beginning of current time span
+let minLatency = .1; // min time before a hap should trigger
+setInterval(() => {
+  const haps = pattern.queryArc(time, time + interval);
+  time += interval; // increment time
+  haps.forEach((hap) => {
+    const deadline = hap.whole.begin - time + minLatency;
+    onTrigger(hap, deadline, duration);
+  });
+}, interval * 1000); // query each "interval" seconds
+
+

+ Note that the above code is simplified for illustrative purposes. The actual implementation has to work around + imprecise callbacks of setInterval. More about the implementation details can be read in + this blog post. +

+

+ The fact that Pattern.queryArc is a pure function that maps a time span to a set of events allows us + to choose any interval we like without changing the resulting output. It also means that when the pattern is + changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running. +

+

+ The latency between the time the pattern is evaluated and the change is heard is between + minLatency and interval + minLatency, in our example between 100ms and 600ms. In + Strudel, the current query interval is 50ms with a minLatency of 100ms, meaning the latency is between 50ms and + 150ms. +

+

7.3 Output

+

+ The last step is to trigger each event in the chosen output. This is where the given time and value of each event + is used to generate audio or any other form of time based output. The default output of the Strudel REPL is the + WebAudio output. To understand what an output does, we first have to understand what control parameters are. +

+

+ 7.3.1 Control Parameters +

+

+ To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the + value of each event. Example: +

+
+
note("c3 e3").cutoff(1000).s('sawtooth')
+  .queryArc(0, 1).map(hap => hap.value)
+/* [
+  { note: 'c3', cutoff: 1000, s: 'sawtooth' }
+  { note: 'e3', cutoff: 1000, s: 'sawtooth' }
+] */
+
+

+ Here, the control parameter functions note, cutoff and s are used, where + each controls a different property in the value object. Each control parameter function accepts a primitive value, + a list of values to be sequenced into a Pattern, or a Pattern. In the example, + note gets a Pattern from a Mini Notation expression (double quoted), while + cutoff and s are given a Number and a (single quoted) + String respectively. +

+

+ Strudel comes with a large default set of control parameter functions that are based on the ones used by Tidal and + SuperDirt, focusing on music and audio terminology. It is however possible to create custom control paramters for + any purpose: +

+
+
const { x, y } = createParams('x', 'y')
+x(sine.range(0, 200)).y(cosine.range(0,200))
+
+

+ This example creates the custom control parameters x and y which are then used to form a + pattern that descibes the coordinates of a circle. +

+

7.3.2 Outputs

+

+ Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can + use that value to generate anything. The scheduler above was calling the onTrigger function which is + used to implement the output. A very simple version of the web audio output could look like this: +

+
+
function onTrigger(hap, deadline, duration) {
+  const { note } = hap.value;
+  const time = getAudioContext().currentTime + deadline;
+  const o = getAudioContext().createOscillator();
+  o.frequency.value = getFreq(note);
+  o.start(time);
+  o.stop(time + event.duration);
+  o.connect(getAudioContext().destination);
+}
+
+

+ The above example will create an OscillatorNode for each event, where the frequency is controlled by + the note param. In essence, this is how the WebAudio API output of Strudel works, only with many more + parameters to control synths, samples and effects. +

+

+ 8 Pattern alignment and combination +

+

+ One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective + of their structure. Its declarative approach means a live coder does not have to think about the details of + how this is done, only what is to be done. +

+

+ As a simple example, consider two number patterns "0 [1 2] 3", and "10 20". The first + has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four + events in total. There are a very large number of ways in which the structure of these two patterns could be + combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then + take events from the first pattern and match them with those in the second pattern. Therefore, the following two + lines are equivalent: +

+
+
"0 [1 2] 3".add("10 20")
+"10 [11 22] 23"
+
+

+ Where the events only partially overlap, they are treated as fragments of the event in the first pattern. This is + a little difficult to conceptualise, but lets start by comparing the two patterns in the following example: +

+
+
"0 1 2".add("10 20")
+"10 [11 21] 20"
+
+

+ They are similar to the previous example in that the number 1 is split in two, with its two halves + added to 10 and 20 respectively. However, the 11 ‘remembers’ that it is a + fragment of that original 1 event, and so is treated as having a duration of a third of a cycle, + despite only being active for a sixth of a cycle. Likewise, the 21 is also a fragment of that + original 1 event, but a fragment of its second half. Because the start of its event is missing, it + wouldn’t actually trigger a sound (unless it underwent further pattern transformations/combinations). +

+

+ In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is + added in to the first one, and indeed this can be made explicit: +

+
+
"0 1 2".add.in("10 20")
+
+

This makes way for other ways to align the pattern, and several are already defined, in particular:

+
    +
  • + in - as explained above, aligns cycles, and applies values from the pattern on the right + in to the pattern on the left. +
  • +
  • + out - as with in, but values are applied out of the pattern on the left + (i.e. in to the one on the right). +
  • +
  • + mix - structures from both patterns are combined, so that the new events are not fragments but are + created at intersections of events from both sides. +
  • +
  • + squeeze - cycles from the pattern on the right are squeezed into events on the left. So that + e.g. "0 1 2".add.squeeze("10 20") is equivalent to "[10 20] [11 21] [12 22]". +
  • +
  • + squeezeout - as with squeeze, but cycles from the left are squeezed into events on the + right. So, "0 1 2".add.squeezeout("10 20") is equivalent to [10 11 12] [20 21 22]. +
  • +
  • + trig is similar to squeezeout in that cycles from the right are aligned with events on + the left. However those cycles are not ‘squeezed’, rather they are truncated to fit the event. So + "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be equivalent to + 10 11 12 13 20 21 30 31. In effect, events on the right ‘trigger’ cycles on the left. +
  • +
  • + trigzero is similar to trig, but the pattern is ‘triggered’ from its very first cycle, + rather than from the current cycle. trig and trigzero therefore only give different + results where the leftmost pattern differs from one cycle to the next. +
  • +
+

+ We will save going deeper into the background, design and practicalities of these alignment functions for future + publications. However in the next section, we take them as a case study for looking at the different design + affordances offered by Haskell to Tidal, and JavaScript to Strudel. +

+

+ 9 Comparing Strudel and Haskell in use +

+

+ Unlike Haskell, JavaScript lacks the ability to define custom infix operators, or change the meaning of existing + ones. So the above Strudel example of "0 1 2".add.out("10 20") is equivalent to the Tidal expression + "0 1 2" +| "10 20", where the vertical bar in the operator +| stands for + out (where a |+ b would be equivalent of a.add.in(b)). +

+

+ From this we can already see that Tidal tends towards brevity through mixing infix operators with functions, and + Strudel tends towards spelling out operations which are joined together with the . operator. This + then is the design trade-off of Tidal’s tersity, versus Strudel’s simplicity. +

+

To demonstrate this, consider the following Tidal pattern:

+
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
+

This can be directly translated to the Strudel equivalent:

+
+
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
+
+

Although for a more canonical Strudel expression, we would reorder it as:

+
+
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
+
+

+ The Strudel example uses the . method call operator for all operations and combinations, whereas the + Tidal example has # for the default method for combining patterns and uses infix operators for other + methods. The lack of parenthesis in the Tidal example is partly due to the way that arguments are applied to + Haskell’s functions, and partly due to the use of the $ operator as an alternative way to establish + precedence and control the order of evaluation. +

+

+ Considering the above, we argue that the Haskell syntax is a little cleaner, but that the Strudel syntax is easier + to learn. Our informal observation is that while Haskell’s dollar $ operator is very useful in making + code easier to work with, it is one of the most difficult aspects of Tidal use for beginners to learn. On the + other hand, the deeper levels of parenthesis in Strudel code can be difficult to keep track of, especially while + coding under pressure of live musical performance. However this difficulty can be largely be mitigated by + reordering expressions, and further mitigated by supporting editor features. +

+

+ With Strudel, we have little choice but to embrace the affordances and constraints offered by JavaScript, and + while designing a domain-specific language entirely based on method calls is a challenge, through creative + adoption of functional programming techniques like partial application, we are so far very happy with the results. + Tidal’s functional reactive approach to pattern-making has in general translated well to JavaScript, and + opportunities and constraints have overall traded off to create a very approachable and useable live coding + environment. +

+

+ 9.1 The trade-off of flexible typing +

+

+ We have identified one problem with porting Tidal to JavaScript where we have missed Haskell’s strict typing and + type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio + of two integers. This allows representation of musical ratios such that are impossible to represent accurately + using the more common floating point numbers. However while libraries are available that support rational numbers + in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally + expensive conversion from floating point to rational numbers are performed late, and therefore often enough to + overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given + floating point number. To mitigate this problem, we might consider moving to TypeScript in the future. +

+

10 Future Outlook

+

+ The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be +

+
    +
  1. accessible
  2. +
  3. consistent with Tidal’s approach to pattern
  4. +
  5. modular and extensible
  6. +
+

+ While Haskell’s type system makes it a great language for the ongoing development of Tidal’s inner representation + of pattern, JavaScript’s vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc + experiments, including interface design. For the future, it is planned to integrate additional alternative sound + engines such as Glicol (Lan [2020] 2022) and + Faust + (Faust - Programming Language for Audio Applications and Plugins [2016] 2022). Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and + work to be done to improve compatibility with Tidal’s mininotation. Tidal version 2.0 is under development, which + brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, + other ways to render events are being explored, such as graphical, and choreographic output. We are also looking + into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax + to escape the constraints of javascript, and developing hardware/e-textile interfaces. In summary, there is a lot + of fun ahead. +

+

11 Links

+

+ The Strudel REPL is available at https://strudel.cc, including an + interactive tutorial. The repository is at + https://github.com/tidalcycles/strudel, all the + code is open source under the AGPL-3.0 License. +

+

12 Acknowledgments

+

+ Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for + inspiration and support. Alex McLean’s work on this project is supported by a UKRI Future Leaders Fellowship + [grant number MR/V025260/1]. +

+

References

+
+
+ Faust - Programming Language for Audio Applications and Plugins. (2016) 2022. C++. GRAME. + https://github.com/grame-cncm/faust. +
+
+ Jack, Olivia. (2022) 2022. Hydra. + https://github.com/ojack/hydra. +
+
+ Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. + https://github.com/chaosprint/glicol. +
+
+ Mclean, Alex. 2020. “Algorithmic Pattern.” In + Proceedings of the International Conference on New Interfaces for Musical Expression, 265--270. + Birmingham, UK. https://zenodo.org/record/4813352. +
+
+ McLean, Alex. 2020. “Feedforward.” In + Proceedings of New Interfaces for Musical Expression. Birmingham. + https://zenodo.org/record/6353969. +
+
+ McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. 2022. “TidalVortex Zero.” In + Proceedings of the 7th International Conference on Live Coding. Limerick, Ireland: Zenodo. + https://doi.org/10.5281/zenodo.6456380. +
+
+ Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. + https://github.com/dktr0/WebDirt. +
+
+ Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, and Alex McLean. 2017. + “Estuary: Browser-Based Collaborative Projectional Live Coding of Musical Patterns.” In + Proceedings of the International Conference on Live Coding, 11. Morelia. +
+
+ Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live Coding Audio in the Browser.” In + In Proceedings of the 2012 International Computer Music Conference. +
+
+ Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns for the Web.” In. Zenodo. + https://doi.org/10.5281/zenodo.6768844. +
+
+ Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. + https://github.com/mikesol/purescript-ocarina. +
+
+ SuperDirt. (2015) 2022. SuperCollider. musikinformatik. + https://github.com/musikinformatik/SuperDirt. +
+
+ Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates Traditional Musical Rhythms.” In + In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science, 47–56. + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. +
+
+ Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. + “WebAssembly AudioWorklet Csound.” In. Berlin, Germany. + https://mural.maynoothuniversity.ie/16018/. +
+
+ + diff --git a/paper/iclc2023.md b/docs/iclc2023-paper/iclc2023.md similarity index 99% rename from paper/iclc2023.md rename to docs/iclc2023-paper/iclc2023.md index 3afb27828..fdc9f0ca4 100644 --- a/paper/iclc2023.md +++ b/docs/iclc2023-paper/iclc2023.md @@ -451,7 +451,7 @@ While Haskell's type system makes it a great language for the ongoing developmen # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the AGPL-3.0 License. +The repository is at , all the code is open source under the AGPL-3.0 License. # Acknowledgments diff --git a/paper/iclc2023.pdf b/docs/iclc2023-paper/iclc2023.pdf similarity index 100% rename from paper/iclc2023.pdf rename to docs/iclc2023-paper/iclc2023.pdf diff --git a/paper/iclc2023x.pdf b/docs/iclc2023-paper/iclc2023x.pdf similarity index 100% rename from paper/iclc2023x.pdf rename to docs/iclc2023-paper/iclc2023x.pdf diff --git a/paper/images/cc.png b/docs/iclc2023-paper/images/cc.png similarity index 100% rename from paper/images/cc.png rename to docs/iclc2023-paper/images/cc.png diff --git a/paper/images/strudel-screenshot.png b/docs/iclc2023-paper/images/strudel-screenshot.png similarity index 100% rename from paper/images/strudel-screenshot.png rename to docs/iclc2023-paper/images/strudel-screenshot.png diff --git a/paper/images/strudel-screenshot2.png b/docs/iclc2023-paper/images/strudel-screenshot2.png similarity index 100% rename from paper/images/strudel-screenshot2.png rename to docs/iclc2023-paper/images/strudel-screenshot2.png diff --git a/paper/images/strudelflow.png b/docs/iclc2023-paper/images/strudelflow.png similarity index 100% rename from paper/images/strudelflow.png rename to docs/iclc2023-paper/images/strudelflow.png diff --git a/paper/inconsolata.sty b/docs/iclc2023-paper/inconsolata.sty similarity index 100% rename from paper/inconsolata.sty rename to docs/iclc2023-paper/inconsolata.sty diff --git a/paper/make.sh b/docs/iclc2023-paper/make.sh similarity index 100% rename from paper/make.sh rename to docs/iclc2023-paper/make.sh diff --git a/docs/iclc2023-paper/pandoc/iclc.html b/docs/iclc2023-paper/pandoc/iclc.html new file mode 100755 index 000000000..c47d6595c --- /dev/null +++ b/docs/iclc2023-paper/pandoc/iclc.html @@ -0,0 +1,63 @@ +$if(false)$ This is a pandoc template and should not be edited. $endif$ + + + + + + + $for(author-meta)$ + + $endfor$ $if(date-meta)$ + + $endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + + $if(quotes)$ + + $endif$ $if(highlighting-css)$ + + $endif$ + + $for(css)$ + + $endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ + + + $for(include-before)$ $include-before$ $endfor$ $if(title)$ +
+

$title$

+ $if(subtitle)$ +

$subtitle$

+ $endif$ +
    + $for(author)$ +
  • $author$
  • + $endfor$ +
+ $if(date)$ +

$date$

+ $endif$ +
+ $endif$ $if(toc)$ +
$toc$
+ $endif$ + +

Abstract

+
+ $if(abstract)$ $abstract$ $else$ Please provide an abstract in the metadata block at the top of your markdown + document. Refer to template.txt for details. $endif$ +
+ + $body$ $for(include-after)$ $include-after$ $endfor$ + + diff --git a/paper/pandoc/iclc.latex b/docs/iclc2023-paper/pandoc/iclc.latex similarity index 100% rename from paper/pandoc/iclc.latex rename to docs/iclc2023-paper/pandoc/iclc.latex diff --git a/paper/pandoc/iclc.sty b/docs/iclc2023-paper/pandoc/iclc.sty similarity index 100% rename from paper/pandoc/iclc.sty rename to docs/iclc2023-paper/pandoc/iclc.sty diff --git a/paper/paper-preprocessed.md b/docs/iclc2023-paper/paper-preprocessed.md similarity index 100% rename from paper/paper-preprocessed.md rename to docs/iclc2023-paper/paper-preprocessed.md diff --git a/paper/paper.md b/docs/iclc2023-paper/paper.md similarity index 100% rename from paper/paper.md rename to docs/iclc2023-paper/paper.md diff --git a/paper/paper.pdf b/docs/iclc2023-paper/paper.pdf similarity index 100% rename from paper/paper.pdf rename to docs/iclc2023-paper/paper.pdf diff --git a/paper/tex/latex-template.tex b/docs/iclc2023-paper/tex/latex-template.tex similarity index 100% rename from paper/tex/latex-template.tex rename to docs/iclc2023-paper/tex/latex-template.tex diff --git a/paper/tex/sig-alternate.cls b/docs/iclc2023-paper/tex/sig-alternate.cls similarity index 100% rename from paper/tex/sig-alternate.cls rename to docs/iclc2023-paper/tex/sig-alternate.cls diff --git a/paper/tex/waccopyright.sty b/docs/iclc2023-paper/tex/waccopyright.sty similarity index 100% rename from paper/tex/waccopyright.sty rename to docs/iclc2023-paper/tex/waccopyright.sty diff --git a/docs/technical-manual/index.md b/docs/technical-manual/index.md new file mode 100644 index 000000000..16c9a5533 --- /dev/null +++ b/docs/technical-manual/index.md @@ -0,0 +1,197 @@ +This document introduces you to Strudel in a technical sense. + +It is rather out of date, but there might still be useful info below. + +If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). + +## Strudel Packages + +There are different packages for different purposes. They.. + +- split up the code into smaller chunks +- can be selectively used to implement some sort of time based system + +Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) + +## REPL + +The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. + +More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) + +# High Level Overview + + + +## 1. End User Code + +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. + +### 🍭 Syntax Sugar + +JavaScript Transpilation = converting valid JavaScript to valid JavaScript: + +```js +"c3 [e3 g3]".fast(2) +``` + +becomes + +```js +mini('c3 [e3 g3]') + .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location + .fast(2); +``` + +- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) +- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later +- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings +- support for top level await +- operator overloading could be implemented in the future + +This is how it works: + + + +- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST +- The AST is transformed to resolve the syntax sugar +- The AST is used to generate code again (shift-codegen) + +Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 + +### Mini Notation + +Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. + +- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn +- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) +- the generated parser takes a mini notation string and outputs an AST +- the AST can then be used to construct a pattern using the regular Strudel API + +Here's an example AST: + +```json +{ + "type_": "pattern", + "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "c3", + "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } + }, + { + "type_": "element", + "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } + "source_": { + "type_": "pattern", "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "e3", + "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } + }, + { + "type_": "element", "source_": "g3", + "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } + } + ] + }, + } + ] +} +``` + +which translates to `seq(c3, seq(e3, g3))` + +## 2. Querying & Scheduling + +When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. +These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. + +### Querying + +> Querying = Asking a Pattern for Events within a certain time span + +```js +seq('c3', ['e3', 'g3']) // <--- Pattern + .queryArc(0, 2) // query events within 0 and 2 cycles + .map((hap) => hap.showWhole()); // make readable +``` + +yields + +```js +[ + '0/1 -> 1/2: c3', // cycle 0 + '1/2 -> 3/4: e3', + '3/4 -> 1/1: g3', + '1/1 -> 3/2: c3', // cycle 1 + '3/2 -> 7/4: e3', + '7/4 -> 2/1: g3', +]; +``` + +### 🗓️ Scheduling + +The scheduler will query events repeatedly, creating a possibly endless loop of time slices. +Here is a simplified example of how it works + +```js +let step = 0.5; // query interval in seconds +let tick = 0; // how many intervals have passed +let pattern = seq('c3', ['e3', 'g3']); // pattern from user +setInterval(() => { + const events = pattern.queryArc(tick * step, ++tick * step); + events.forEach((event) => { + console.log(event.showWhole()); + const o = getAudioContext().createOscillator(); + o.frequency.value = getFreq(event.value); + o.start(event.whole.begin); + o.stop(event.whole.begin + event.duration); + o.connect(getAudioContext().destination); + }); +}, step * 1000); // query each "step" seconds +``` + +## 3. Sound Output + +The third and last step is to use the scheduled events to make sound. +Patterns are wrapped with param functions to compose different properties of the sound. + +```js +note("[c2(3,8) [ bb1]]") // sets frequency + .s("") // sound source + .gain(.5) // turn down volume + .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff + .slow(2) + .out().logValues()`, + ]} +/> +``` + +Here is an example Hap value with different properties: + +```js +{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } +``` + + +
+ +- Patterns represent just values in time! +- Suitable for any time based output (music, visuals, movement, .. ?) + +### Supported Outputs + +At the time of writing this doc, the following outputs are supported: + +- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) +- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) +- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) + +These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). diff --git a/docs/technical-manual/shiftflow.png b/docs/technical-manual/shiftflow.png new file mode 100644 index 000000000..8d91658dc Binary files /dev/null and b/docs/technical-manual/shiftflow.png differ diff --git a/docs/technical-manual/strudelflow.png b/docs/technical-manual/strudelflow.png new file mode 100644 index 000000000..72cdf494b Binary files /dev/null and b/docs/technical-manual/strudelflow.png differ diff --git a/docs/technical-manual/waa-nodes.png b/docs/technical-manual/waa-nodes.png new file mode 100644 index 000000000..cece1aa91 Binary files /dev/null and b/docs/technical-manual/waa-nodes.png differ diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..c9ff40ca1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', + 'packages/supradough/dough-export.mjs', '**/samples', ], }, @@ -83,4 +84,14 @@ export default [ ], }, }, + { + // Properties provided by AudioWorkletGlobalScope + files: ['packages/superdough/worklets.mjs'], + languageOptions: { + globals: { + currentTime: 'readonly', + sampleRate: 'readonly', + }, + }, + }, ]; diff --git a/examples/tidal-repl/package.json b/examples/tidal-repl/package.json index 4da2f086b..21c8ee177 100644 --- a/examples/tidal-repl/package.json +++ b/examples/tidal-repl/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", @@ -23,9 +23,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/web": "workspace:*", "hs2js": "workspace:*" diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index 09190846f..d59c8dac4 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -1,6 +1,6 @@ /* -jsdoc-synonyms.js - Add support for @synonym tag -Copyright (C) 2023 Strudel contributors - see +jsdoc-synonyms.js - Add support for @synonyms tag +Copyright (C) 2023 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 . */ diff --git a/my-patterns/README.md b/my-patterns/README.md index c8d694ea8..5283ad81b 100644 --- a/my-patterns/README.md +++ b/my-patterns/README.md @@ -5,22 +5,24 @@ made into a pattern swatch. Example: +Please note: These instructions have not been fully tested/adapted since strudel moved to codeberg from github. PRs welcome! + ## deploy -### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git) +### 1. fork the [strudel repo on codeberg](https://codeberg.org/uzu/strudel.git) -### 2. clone your fork to your machine `git clone https://github.com//strudel.git strudel && cd strudel` +### 2. clone your fork to your machine `git clone https://codeberg.org//strudel.git strudel && cd strudel` ### 3. create a separate branch like `git branch patternuary && git checkout patternuary` ### 4. save one or more .txt files in the my-patterns folder -### 5. edit `website/public/CNAME` to contain `.github.io/strudel` +### 5. edit `website/public/CNAME` to contain `.codeberg.page/strudel` -### 6. edit `website/astro.config.mjs` to use site: `https://.github.io` and base `/strudel`, like this +### 6. edit `website/astro.config.mjs` to use site: `https://.codeberg.page` and base `/strudel`, like this ```js -const site = 'https://.github.io'; +const site = 'https://.codeberg.page'; const base = '/strudel'; ``` @@ -30,15 +32,9 @@ const base = '/strudel'; git add . && git commit -m "site config" && git push --set-upstream origin ``` -### 8. deploy to github pages +### 8. deploy to codeberg pages -- go to settings -> pages and select "Github Actions" as source -- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches") -- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary` - -### 9. view your patterns at `.github.io/strudel/swatch/` - -Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site). +### 9. view your patterns at `.codeberg.page/strudel/swatch/` ### 10. optional: automatic deployment diff --git a/package.json b/package.json index df50f1f41..d18291eb2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -43,7 +43,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { @@ -71,6 +71,7 @@ "jsdoc-json": "^2.0.2", "lerna": "^8.1.9", "prettier": "^3.4.2", - "vitest": "^3.0.4" + "vitest": "^3.0.4", + "vite-plugin-bundle-audioworklet": "workspace:*" } -} +} \ No newline at end of file diff --git a/packages/README.md b/packages/README.md index 7a7681b67..98938d6c3 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,4 +2,4 @@ Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel). -To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs. +To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs. diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 203ab8556..69aa3bc59 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,68 +1,122 @@ import jsdoc from '../../doc.json'; -// import { javascriptLanguage } from '@codemirror/lang-javascript'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -function plaintext(str) { +const escapeHtml = (str) => { const div = document.createElement('div'); div.innerText = str; return div.innerHTML; -} +}; -const getDocLabel = (doc) => doc.name || doc.longname; -const getInnerText = (html) => { - var div = document.createElement('div'); +const stripHtml = (html) => { + const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; -export function Autocomplete({ doc, label }) { - return h`
-

${label || getDocLabel(doc)}

-${doc.description} -
    - ${doc.params?.map( - ({ name, type, description }) => - `
  • ${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}
  • `, - )} -
-
- ${doc.examples?.map((example) => `
${plaintext(example)}
`)} -
-
`[0]; - /* -
 {
-  console.log('ola!');
-  navigator.clipboard.writeText(example);
-  e.stopPropagation();
-}}
->
-{example}
-
-*/ -} +const getDocLabel = (doc) => doc.name || doc.longname; -const jsdocCompletions = jsdoc.docs - .filter( - (doc) => - getDocLabel(doc) && - !getDocLabel(doc).startsWith('_') && - !['package'].includes(doc.kind) && - !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)), - ) - // https://codemirror.net/docs/ref/#autocomplete.Completion - .map((doc) /*: Completion */ => ({ - label: getDocLabel(doc), - // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. - info: () => Autocomplete({ doc }), - type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type - })); +const buildParamsList = (params) => + params?.length + ? ` +
+

Parameters

+
    + ${params + .map( + ({ name, type, description }) => ` +
  • + ${name} + ${type.names?.join(' | ')} + ${description ? `
    ${stripHtml(description)}
    ` : ''} +
  • + `, + ) + .join('')} +
+
+ ` + : ''; + +const buildExamples = (examples) => + examples?.length + ? ` +
+

Examples

+ ${examples + .map( + (example) => ` +
${escapeHtml(example)}
+ `, + ) + .join('')} +
+ ` + : ''; + +export const Autocomplete = (doc) => + h` +
+
+

${getDocLabel(doc)}

+ ${doc.synonyms_text ? `
Synonyms: ${doc.synonyms_text}
` : ''} + ${doc.description ? `
${doc.description}
` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
+
+`[0]; + +const isValidDoc = (doc) => { + const label = getDocLabel(doc); + return label && !label.startsWith('_') && !['package'].includes(doc.kind); +}; + +const hasExcludedTags = (doc) => + ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); + +export const getSynonymDoc = (doc, synonym) => { + const synonyms = doc.synonyms || []; + const docLabel = getDocLabel(doc); + // Swap `doc.name` in for `s` in the list of synonyms + const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym); + return { + ...doc, + name: synonym, + longname: synonym, + synonyms: synonymsWithDoc, + synonyms_text: synonymsWithDoc.join(', '), + }; +}; + +const jsdocCompletions = (() => { + const seen = new Set(); // avoid repetition + const completions = []; + for (const doc of jsdoc.docs) { + if (!isValidDoc(doc) || hasExcludedTags(doc)) continue; + const docLabel = getDocLabel(doc); + // Remove duplicates + const synonyms = doc.synonyms || []; + let labels = [docLabel, ...synonyms]; + for (const label of labels) { + // https://codemirror.net/docs/ref/#autocomplete.Completion + if (label && !seen.has(label)) { + seen.add(label); + completions.push({ + label, + info: () => Autocomplete(getSynonymDoc(doc, label)), + type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type + }); + } + } + } + return completions; +})(); + +export const strudelAutocomplete = (context) => { + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; -export const strudelAutocomplete = (context /* : CompletionContext */) => { - let word = context.matchBefore(/\w*/); - if (word.from == word.to && !context.explicit) return null; return { from: word.from, options: jsdocCompletions, @@ -74,11 +128,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => { }; }; -export function isAutoCompletionEnabled(on) { - return on - ? [ - autocompletion({ override: [strudelAutocomplete] }), - //javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), - ] - : []; // autocompletion({ override: [] }) -} +export const isAutoCompletionEnabled = (enabled) => + enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : []; diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs new file mode 100644 index 000000000..02294b93a --- /dev/null +++ b/packages/codemirror/basicSetup.mjs @@ -0,0 +1,63 @@ +import { + keymap, + highlightSpecialChars, + drawSelection, + highlightActiveLine, + dropCursor, + rectangularSelection, + crosshairCursor, + lineNumbers, + highlightActiveLineGutter, +} from '@codemirror/view'; +import { + defaultHighlightStyle, + syntaxHighlighting, + bracketMatching, + foldGutter, + foldKeymap, +} from '@codemirror/language'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete'; + +// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts + +export const basicSetup = (() => [ + // lineNumbers(), + // highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + // foldGutter(), + // drawSelection(), + dropCursor(), + // EditorState.allowMultipleSelections.of(true), + // indentOnInput(), + // syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + // autocompletion(), + rectangularSelection(), + crosshairCursor(), + // highlightActiveLine(), + // highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + // ...searchKeymap, + ...historyKeymap, + // ...foldKeymap, + // ...completionKeymap, + ]), +])(); + +/// A minimal set of extensions to create a functional editor. Only +/// includes [the default keymap](#commands.defaultKeymap), [undo +/// history](#commands.history), [special character +/// highlighting](#view.highlightSpecialChars), [custom selection +/// drawing](#view.drawSelection), and [default highlight +/// style](#language.defaultHighlightStyle). +export const minimalSetup = (() => [ + highlightSpecialChars(), + history(), + drawSelection(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + keymap.of([...defaultKeymap, ...historyKeymap]), +])(); diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 5886c4769..4dc23996f 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,8 +1,8 @@ import { closeBrackets } from '@codemirror/autocomplete'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; // import { search, highlightSelectionMatches } from '@codemirror/search'; -import { history } from '@codemirror/commands'; -import { javascript } from '@codemirror/lang-javascript'; +import { indentWithTab } from '@codemirror/commands'; +import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; import { @@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; +import { basicSetup } from './basicSetup.mjs'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -37,6 +38,14 @@ const extensions = { isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []), isFlashEnabled, keybindings, + isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []), + isMultiCursorEnabled: (on) => + on + ? [ + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] + : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -51,6 +60,8 @@ export const defaultSettings = { isFlashEnabled: true, isTooltipEnabled: false, isLineWrappingEnabled: false, + isTabIndentationEnabled: false, + isMultiCursorEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -62,11 +73,12 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), ); + initTheme(settings.theme); let state = EditorState.create({ doc: initialCode, @@ -74,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, - javascript(), + basicSetup, + mondo ? [] : javascript(), + javascriptLanguage.data.of({ + closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] }, + bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] }, + }), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything syntaxHighlighting(defaultHighlightStyle), - history(), EditorView.updateListener.of((v) => onChange(v)), drawSelection({ cursorBlinkRate: 0 }), Prec.highest( @@ -208,6 +224,7 @@ export class StrudelMirror { }, onEvaluate: () => this.evaluate(), onStop: () => this.stop(), + mondo: replOptions.mondo, }); const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 6b37038ff..243500bff 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -11,7 +11,9 @@ export const flashField = StateField.define({ 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` } }); + const mark = Decoration.mark({ + attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, + }); flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index d333986cc..f9f977c6b 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -1,4 +1,4 @@ -import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state'; +import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'; import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); @@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => { setTimeout(() => { updateMiniLocations(config.editor, config.miniLocations); }, 100); - return on ? highlightExtension : []; + return on ? Prec.highest(highlightExtension) : []; }; diff --git a/packages/codemirror/html.mjs b/packages/codemirror/html.mjs index 527275ef6..f240059d3 100644 --- a/packages/codemirror/html.mjs +++ b/packages/codemirror/html.mjs @@ -1,6 +1,7 @@ -const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null; export let html = (string) => { - return parser?.parseFromString(string, 'text/html').querySelectorAll('*'); + const template = document.createElement('template'); + template.innerHTML = string.trim(); + return template.content.childNodes; }; let parseChunk = (chunk) => { if (Array.isArray(chunk)) return chunk.flat().join(''); diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 6fe00eda1..ca5f34f4c 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; import { vim } from '@replit/codemirror-vim'; +// import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { defaultKeymap, historyKeymap } from '@codemirror/commands'; +import { defaultKeymap } from '@codemirror/commands'; const vscodePlugin = ViewPlugin.fromClass( class { @@ -21,11 +22,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); const keymaps = { vim, emacs, + codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, }; export function keybindings(name) { const active = keymaps[name]; - return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []]; - // keymap.of(searchKeymap), + return [active ? Prec.high(active()) : []]; } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 44326b7c7..803b33f3d 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.1.0", + "version": "1.2.5", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@codemirror/autocomplete": "^6.18.4", "@codemirror/commands": "^6.8.0", @@ -42,7 +42,7 @@ "@lezer/highlight": "^1.2.1", "@nanostores/persistent": "^0.10.2", "@replit/codemirror-emacs": "^6.1.0", - "@replit/codemirror-vim": "^6.2.1", + "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 47d6c3c1e..1a523b391 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -4,16 +4,21 @@ import blackscreen, { settings as blackscreenSettings } from './themes/blackscre import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs'; import teletext, { settings as teletextSettings } from './themes/teletext.mjs'; import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs'; -import terminal, { settings as terminalSettings } from './themes/terminal.mjs'; -import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs'; +import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; +import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; +import redText, { settings as redTextSettings } from './themes/red-text.mjs'; +import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; +import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs'; +import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs'; + +import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs'; + import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import aura, { settings as auraSettings } from './themes/aura.mjs'; -import bespin, { settings as bespinSettings } from './themes/bespin.mjs'; import darcula, { settings as darculaSettings } from './themes/darcula.mjs'; import dracula, { settings as draculaSettings } from './themes/dracula.mjs'; import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs'; -import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs'; import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs'; import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs'; import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs'; @@ -31,75 +36,77 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs'; import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs'; import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs'; -// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs'; import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs'; import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; import { setTheme } from '@strudel/draw'; - export const themes = { strudelTheme, - bluescreen, - blackscreen, - whitescreen, - teletext, algoboy, + archBtw, + androidstudio, atomone, aura, + bbedit, + blackscreen, + bluescreen, + bluescreenlight, + CutiePi, darcula, dracula, - // todo: optimize - // bespin, - //abcdef, - androidstudio, duotoneDark, + eclipse, + fruitDaw, githubDark, + githubLight, + greenText, gruvboxDark, + gruvboxLight, + sonicPink, materialDark, - nord, + materialLight, monokai, + noctisLilac, + nord, + redText, solarizedDark, + solarizedLight, sublime, + teletext, tokyoNight, + tokyoNightDay, tokyoNightStorm, vscodeDark, - //xcodeDark, - // LIGHT - bbedit, - //duotoneLight, - eclipse, - githubLight, - gruvboxLight, - materialLight, vscodeLight, - noctisLilac, - solarizedLight, - tokyoNightDay, + whitescreen, xcodeLight, }; export const settings = { strudelTheme: strudelThemeSettings, bluescreen: bluescreenSettings, + bluescreenlight: bluescreenlightsettings, blackscreen: blackscreenSettings, whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, - terminal: terminalSettings, - abcdef: abcdefSettings, + archBtw: archBtwSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, bbedit: bbeditSettings, - bespin: bespinSettings, darcula: darculaSettings, dracula: draculaSettings, - duotoneLight: duotoneLightSettings, duotoneDark: duotoneDarkSettings, eclipse: eclipseSettings, + CutiePi: CutiePiSettings, + sonicPink: sonicPinkSettings, + fruitDaw: fruitDawSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, + greenText: greenTextSettings, + gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, @@ -107,6 +114,7 @@ export const settings = { noctisLilac: noctisLilacSettings, nord: nordSettings, monokai: monokaiSettings, + redText: redTextSettings, solarizedLight: solarizedLightSettings, solarizedDark: solarizedDarkSettings, sublime: sublimeSettings, @@ -115,7 +123,6 @@ export const settings = { vscodeDark: vscodeDarkSettings, vscodeLight: vscodeLightSettings, xcodeLight: xcodeLightSettings, - //xcodeDark: xcodeDarkSettings, tokyoNightDay: tokyoNightDaySettings, }; diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs new file mode 100644 index 000000000..64b38eaa7 --- /dev/null +++ b/packages/codemirror/themes/CutiePi.mjs @@ -0,0 +1,45 @@ +/* + * Cutie Pi + * by Switch Angel + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; +const deepPurple = '#5c019a'; +const yellowPink = '#fbeffc'; +const grey = '#272C35'; +const pinkAccent = '#fee1ff'; +const lightGrey = '#465063'; +const bratGreen = '#9acd3f'; +const lighterGrey = '#97a1b7'; +const pink = '#f6a6fd'; + +export const settings = { + background: 'white', + lineBackground: 'transparent', + foreground: deepPurple, + caret: '#797977', + selection: yellowPink, + selectionMatch: '#2B323D', + gutterBackground: grey, + gutterForeground: lightGrey, + gutterBorder: 'transparent', + lineHighlight: pinkAccent, +}; + +export default createTheme({ + theme: 'light', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: deepPurple, + }, + { tag: [t.tagName, t.heading], color: settings.foreground }, + { tag: t.comment, color: lighterGrey }, + { tag: [t.variableName, t.propertyName, t.labelName], color: pink }, + { tag: [t.attributeName, t.number], color: '#d19a66' }, + { tag: t.className, color: grey }, + { tag: t.keyword, color: deepPurple }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen }, + ], +}); diff --git a/packages/codemirror/themes/abcdef.mjs b/packages/codemirror/themes/abcdef.mjs deleted file mode 100644 index 917823b9f..000000000 --- a/packages/codemirror/themes/abcdef.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @name abcdef - * @author codemirror.net - * https://codemirror.net/5/theme/abcdef.css - */ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#0f0f0f', - lineBackground: '#0f0f0f99', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - gutterBackground: '#555', - gutterForeground: '#FFFFFF', - lineHighlight: '#314151', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#0f0f0f', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - // gutterBackground: '#555', - gutterBackground: 'transparent', - /* gutterForeground: '#FFFFFF', */ - gutterForeground: '#7a7b7c', - lineHighlight: '#0a6bcb3d', - }, - styles: [ - { tag: t.labelName, color: 'inherit' }, - { tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' }, - { tag: t.atom, color: '#77F' }, - { tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' }, - { tag: t.number, color: 'violet' }, - { tag: t.definition(t.variableName), color: '#fffabc' }, - { tag: t.variableName, color: '#abcdef' }, - { tag: t.function(t.variableName), color: '#fffabc' }, - { tag: t.typeName, color: '#FFDD44' }, - { tag: t.tagName, color: '#def' }, - { tag: t.string, color: '#2b4' }, - { tag: t.meta, color: '#C9F' }, - // { tag: t.qualifier, color: '#FFF700' }, - // { tag: t.builtin, color: '#30aabc' }, - { tag: t.bracket, color: '#8a8a8a' }, - { tag: t.attributeName, color: '#DDFF00' }, - { tag: t.heading, color: 'aquamarine', fontWeight: 'bold' }, - { tag: t.link, color: 'blueviolet', fontWeight: 'bold' }, - ], -}); diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 30812d126..049ed596d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,6 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] }, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/androidstudio.mjs b/packages/codemirror/themes/androidstudio.mjs index 097f7951b..77c16c331 100644 --- a/packages/codemirror/themes/androidstudio.mjs +++ b/packages/codemirror/themes/androidstudio.mjs @@ -1,5 +1,5 @@ -/** - * @name androidstudio +/* + * androidstudio */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs new file mode 100644 index 000000000..87546569c --- /dev/null +++ b/packages/codemirror/themes/archBtw.mjs @@ -0,0 +1,38 @@ +/* + * Arch Btw + * Modern terminal inspired theme + * made by Jade + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[1], + selection: hex[2], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index 85951d157..ab9cd3607 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -1,5 +1,5 @@ -/** - * @name Atom One +/* + * Atom One * Atom One dark syntax theme * * https://github.com/atom/one-dark-syntax @@ -38,12 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' }, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/bespin.mjs b/packages/codemirror/themes/bespin.mjs deleted file mode 100644 index 41af821b0..000000000 --- a/packages/codemirror/themes/bespin.mjs +++ /dev/null @@ -1,39 +0,0 @@ -// this is different from https://thememirror.net/bespin -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#28211c', - lineBackground: '#28211c99', - foreground: '#9d9b97', - caret: '#797977', - selection: '#36312e', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: 'rgba(255, 255, 255, 0.1)', -}; -export default createTheme({ - theme: 'dark', - settings: { - background: '#28211c', - foreground: '#9d9b97', - caret: '#797977', - selection: '#4f382b', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: '#ffffff1a', - }, - styles: [ - { tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' }, - { tag: t.comment, color: '#937121' }, - { tag: [t.keyword, t.tagName], color: '#cf6a4c' }, - { tag: t.string, color: '#f9ee98' }, - { tag: t.bracket, color: '#9d9b97' }, - { tag: [t.variableName], color: '#5ea6ea' }, - { tag: t.definition(t.variableName), color: '#cf7d34' }, - { tag: [t.function(t.variableName), t.className], color: '#cf7d34' }, - { tag: [t.propertyName, t.attributeName], color: '#54be0d' }, - ], -}); diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs new file mode 100644 index 000000000..031a3da33 --- /dev/null +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -0,0 +1,37 @@ +/* + * A lighter blue screen theme + * made by Jade + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[3], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[1], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, + { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[2] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); diff --git a/packages/codemirror/themes/darcula.mjs b/packages/codemirror/themes/darcula.mjs index 176acbfe3..ead66e663 100644 --- a/packages/codemirror/themes/darcula.mjs +++ b/packages/codemirror/themes/darcula.mjs @@ -1,6 +1,5 @@ -/** - * @name darcula - * @author darcula +/* + * darcula * Name: IntelliJ IDEA darcula theme * From IntelliJ IDEA by JetBrains */ diff --git a/packages/codemirror/themes/dracula.mjs b/packages/codemirror/themes/dracula.mjs index 10147edaa..bdd9617de 100644 --- a/packages/codemirror/themes/dracula.mjs +++ b/packages/codemirror/themes/dracula.mjs @@ -1,6 +1,5 @@ -/** +/* * @name dracula - * @author dracula * Michael Kaminsky (http://github.com/mkaminsky11) * Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme) */ diff --git a/packages/codemirror/themes/duotoneDark.mjs b/packages/codemirror/themes/duotoneDark.mjs index 057302cb7..f54973e85 100644 --- a/packages/codemirror/themes/duotoneDark.mjs +++ b/packages/codemirror/themes/duotoneDark.mjs @@ -1,6 +1,6 @@ -/** - * @name duotone - * @author Bram de Haan +/* + * duotone + * author Bram de Haan * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/duotoneLight.mjs b/packages/codemirror/themes/duotoneLight.mjs index f7b0214b4..81aa34848 100644 --- a/packages/codemirror/themes/duotoneLight.mjs +++ b/packages/codemirror/themes/duotoneLight.mjs @@ -1,6 +1,6 @@ -/** - * @name duotone - * @author Bram de Haan +/* + * duotone + * author Bram de Haan * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/fruitDaw.mjs b/packages/codemirror/themes/fruitDaw.mjs new file mode 100644 index 000000000..c9e557796 --- /dev/null +++ b/packages/codemirror/themes/fruitDaw.mjs @@ -0,0 +1,50 @@ +/* + * Fruit Daw + * made by Jade + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = [ + 'rgb(84, 93, 98)', + 'rgb(255, 255, 255)', + 'rgba(255, 255, 255, .25)', + 'rgb(67, 76, 81)', + 'rgb(186, 230, 115)', + 'rgb(252, 184, 67)', + 'rgb(124, 206, 254)', + 'rgb(83, 101, 102)', + 'rgba(46, 62, 72,.5)', + 'rgb(94, 100, 108)', + 'rgb(167, 216, 177)', +]; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[10], + selection: hex[8], + selectionMatch: hex[0], + gutterBackground: hex[3], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[3], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, + { tag: [t.variableName], color: hex[1] }, + { tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] }, + { tag: [t.attributeName, t.number], color: hex[6] }, + { tag: t.keyword, color: hex[5] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] }, + ], +}); diff --git a/packages/codemirror/themes/githubDark.mjs b/packages/codemirror/themes/githubDark.mjs index 5187dcc21..477e7d7b8 100644 --- a/packages/codemirror/themes/githubDark.mjs +++ b/packages/codemirror/themes/githubDark.mjs @@ -1,6 +1,3 @@ -/** - * @name github - */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/githubLight.mjs b/packages/codemirror/themes/githubLight.mjs index 382c7ac66..2ccec93b6 100644 --- a/packages/codemirror/themes/githubLight.mjs +++ b/packages/codemirror/themes/githubLight.mjs @@ -1,6 +1,3 @@ -/** - * @name github - */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs new file mode 100644 index 000000000..08c7a4bbe --- /dev/null +++ b/packages/codemirror/themes/green-text.mjs @@ -0,0 +1,39 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/gruvboxDark.mjs b/packages/codemirror/themes/gruvboxDark.mjs index 31d7e42c1..67d8bffc1 100644 --- a/packages/codemirror/themes/gruvboxDark.mjs +++ b/packages/codemirror/themes/gruvboxDark.mjs @@ -1,7 +1,6 @@ -/** - * @name gruvbox-dark - * @author morhetz - * Name: Gruvbox +/* + * gruvbox-dark + * author morhetz * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/gruvboxLight.mjs b/packages/codemirror/themes/gruvboxLight.mjs index ffb4d716c..5d4b4ec01 100644 --- a/packages/codemirror/themes/gruvboxLight.mjs +++ b/packages/codemirror/themes/gruvboxLight.mjs @@ -1,7 +1,6 @@ -/** - * @name gruvbox-light - * @author morhetz - * Name: Gruvbox +/* + * gruvbox-light + * author morhetz * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs new file mode 100644 index 000000000..44dda5dc0 --- /dev/null +++ b/packages/codemirror/themes/red-text.mjs @@ -0,0 +1,39 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs new file mode 100644 index 000000000..5a19555ab --- /dev/null +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -0,0 +1,39 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c']; + +export const settings = { + background: '#000000', + lineBackground: 'transparent', + foreground: hex[4], + selection: hex[6], + gutterBackground: hex[0], + gutterForeground: hex[5], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[4], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] }, + + { tag: t.comment, color: '#54636D' }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] }, + { tag: [t.attributeName, t.number], color: hex[3] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 7fd6bb651..7c4a3a919 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,14 +5,11 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', - // foreground: '#75baff', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', - // lineHighlight: '#8a91991a', // original lineHighlight: '#00000050', gutterBackground: 'transparent', - // gutterForeground: '#8a919966', gutterForeground: '#8a919966', }; @@ -20,6 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' }, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/xcodeDark.mjs b/packages/codemirror/themes/xcodeDark.mjs deleted file mode 100644 index 41440cfd1..000000000 --- a/packages/codemirror/themes/xcodeDark.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#292A30', - lineBackground: '#292A3099', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#2F3239', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#292A30', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#ffffff0f', - }, - styles: [ - { tag: [t.comment, t.quote], color: '#7F8C98' }, - { tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' }, - { tag: [t.string, t.meta], color: '#FF8170' }, - { tag: [t.typeName], color: '#DABAFF' }, - { tag: [t.definition(t.variableName)], color: '#6BDFFF' }, - { tag: [t.name], color: '#6BAA9F' }, - { tag: [t.variableName], color: '#ACF2E4' }, - { tag: [t.regexp, t.link], color: '#FF8170' }, - ], -}); diff --git a/packages/codemirror/tooltip.mjs b/packages/codemirror/tooltip.mjs index f67e6d14a..d1d0479b2 100644 --- a/packages/codemirror/tooltip.mjs +++ b/packages/codemirror/tooltip.mjs @@ -1,6 +1,6 @@ import { hoverTooltip } from '@codemirror/view'; import jsdoc from '../../doc.json'; -import { Autocomplete } from './autocomplete.mjs'; +import { Autocomplete, getSynonymDoc } from './autocomplete.mjs'; const getDocLabel = (doc) => doc.name || doc.longname; @@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip( let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; if (!entry) { // Try for synonyms - entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; - if (!entry) { + const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; + if (!doc) { return null; } + entry = getSynonymDoc(doc, word); } return { @@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip( create(view) { let dom = document.createElement('div'); dom.className = 'strudel-tooltip'; - const ac = Autocomplete({ doc: entry, label: word }); + const ac = Autocomplete(entry); dom.appendChild(ac); return { dom }; }, diff --git a/packages/core/bench/pattern.bench.mjs b/packages/core/bench/pattern.bench.mjs index 1b5be0b9c..56a849801 100644 --- a/packages/core/bench/pattern.bench.mjs +++ b/packages/core/bench/pattern.bench.mjs @@ -1,11 +1,11 @@ import { describe, bench } from 'vitest'; -import { calculateTactus, sequence, stack } from '../index.mjs'; +import { calculateSteps, sequence, stack } from '../index.mjs'; const pat64 = sequence(...Array(64).keys()); describe('steps', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -14,7 +14,7 @@ describe('steps', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -25,7 +25,7 @@ describe('steps', () => { }); describe('stack', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -34,7 +34,7 @@ describe('stack', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -43,4 +43,4 @@ describe('stack', () => { { time: 1000 }, ); }); -calculateTactus(true); +calculateSteps(true); diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index bcaf28725..77a45362e 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -113,7 +113,7 @@ self.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -// used to consistently schedule events, for use in a service worker - see +// used to consistently schedule events, for use in a service worker - see function createClock( getTime, callback, // called slightly before each cycle diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e703..7e3c2a8e2 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1,16 +1,17 @@ /* -controls.mjs - -Copyright (C) 2022 Strudel contributors - see +controls.mjs - Registers audio controls for pattern manipulation and effects. +Copyright (C) 2022 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 . */ -import { Pattern, register, sequence } from './pattern.mjs'; +import { Pattern, register, reify } from './pattern.mjs'; export function createParam(names) { let isMulti = Array.isArray(names); names = !isMulti ? [names] : names; const name = names[0]; + // todo: make this less confusing const withVal = (xs) => { let bag; // check if we have an object with an unnamed control (.value) @@ -35,25 +36,34 @@ export function createParam(names) { } }; - const func = (...pats) => sequence(...pats).withValue(withVal); - - const setter = function (...pats) { - if (!pats.length) { - return this.fmap(withVal); + // todo: make this less confusing + const func = function (value, pat) { + if (!pat) { + return reify(value).withValue(withVal); } - return this.set(func(...pats)); + if (typeof value === 'undefined') { + return pat.fmap(withVal); + } + return pat.set(reify(value).withValue(withVal)); + }; + Pattern.prototype[name] = function (value) { + return func(value, this); }; - Pattern.prototype[name] = setter; return func; } // maps control alias names to the "main" control name const controlAlias = new Map(); +export function isControlName(name) { + return controlAlias.has(name); +} + export function registerControl(names, ...aliases) { const name = Array.isArray(names) ? names[0] : names; let bag = {}; bag[name] = createParam(names); + controlAlias.set(name, name); aliases.forEach((alias) => { bag[alias] = bag[name]; controlAlias.set(alias, name); @@ -77,10 +87,244 @@ export function registerControl(names, ...aliases) { */ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); +/** + * Position in the wavetable of the wavetable oscillator + * + * @name wt + * @param {number | Pattern} position Position in the wavetable from 0 to 1 + * @synonyms wavetablePosition + * @example + * s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1") + */ +export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition'); + +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name wtenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { wtenv } = registerControl('wtenv'); +/** + * Attack time of the wavetable oscillator's position envelope + * + * @name wtattack + * @synonyms wtatt + * @param {number | Pattern} time attack time in seconds + */ +export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt'); + +/** + * Decay time of the wavetable oscillator's position envelope + * + * @name wtdecay + * @synonyms wtdec + * @param {number | Pattern} time decay time in seconds + */ +export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec'); + +/** + * Sustain time of the wavetable oscillator's position envelope + * + * @name wtsustain + * @synonyms wtsus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus'); + +/** + * Release time of the wavetable oscillator's position envelope + * + * @name wtrelease + * @synonyms wtrel + * @param {number | Pattern} time release time in seconds + */ +export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel'); + +/** + * Rate of the LFO for the wavetable oscillator's position + * + * @name wtrate + * @param {number | Pattern} rate rate in hertz + */ +export const { wtrate } = registerControl('wtrate'); +/** + * cycle synced rate of the LFO for the wavetable oscillator's position + * + * @name wtsync + * @param {number | Pattern} rate rate in cycles + */ +export const { wtsync } = registerControl('wtsync'); + +/** + * Depth of the LFO for the wavetable oscillator's position + * + * @name wtdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { wtdepth } = registerControl('wtdepth'); + +/** + * Shape of the LFO for the wavetable oscillator's position + * + * @name wtshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { wtshape } = registerControl('wtshape'); + +/** + * DC offset of the LFO for the wavetable oscillator's position + * + * @name wtdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { wtdc } = registerControl('wtdc'); + +/** + * Skew of the LFO for the wavetable oscillator's position + * + * @name wtskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { wtskew } = registerControl('wtskew'); + +/** + * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator + * + * @name warp + * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 + * @synonyms wavetableWarp + * @example + * s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("spin") + */ +export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp'); + +/** + * Attack time of the wavetable oscillator's warp envelope + * + * @name warpattack + * @synonyms warpatt + * @param {number | Pattern} time attack time in seconds + */ +export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt'); + +/** + * Decay time of the wavetable oscillator's warp envelope + * + * @name warpdecay + * @synonyms warpdec + * @param {number | Pattern} time decay time in seconds + */ +export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec'); + +/** + * Sustain time of the wavetable oscillator's warp envelope + * + * @name warpsustain + * @synonyms warpsus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus'); + +/** + * Release time of the wavetable oscillator's warp envelope + * + * @name warprelease + * @synonyms warprel + * @param {number | Pattern} time release time in seconds + */ +export const { warprelease, warprel } = registerControl('warprelease', 'warprel'); + +/** + * Rate of the LFO for the wavetable oscillator's warp + * + * @name warprate + * @param {number | Pattern} rate rate in hertz + */ +export const { warprate } = registerControl('warprate'); + +/** + * Depth of the LFO for the wavetable oscillator's warp + * + * @name warpdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { warpdepth } = registerControl('warpdepth'); + +/** + * Shape of the LFO for the wavetable oscillator's warp + * + * @name warpshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { warpshape } = registerControl('warpshape'); + +/** + * DC offset of the LFO for the wavetable oscillator's warp + * + * @name warpdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { warpdc } = registerControl('warpdc'); + +/** + * Skew of the LFO for the wavetable oscillator's warp + * + * @name warpskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { warpskew } = registerControl('warpskew'); + +/** + * Type of warp (alteration of the waveform) to apply to the wavetable oscillator. + * + * The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit, + * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip + * + * @name warpmode + * @param {number | string | Pattern} mode Warp mode + * @synonyms wavetableWarpMode + * @example + * s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("*2") + * + */ +export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode'); + +/** + * Amount of randomness of the initial phase of the wavetable oscillator. + * + * @name wtphaserand + * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) + * @synonyms wavetablePhaseRand + * @example + * s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>") + * + */ +export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand'); + +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name warpenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { warpenv } = registerControl('warpenv'); + +/** + * cycle synced rate of the LFO for the wavetable warp position + * + * @name warpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { warpsync } = registerControl('warpsync'); + /** * Define a custom webaudio node to use as a sound source. * * @name source + * @synonyms src * @param {function} getSource * @synonyms src * @@ -96,14 +340,14 @@ export const { source, src } = registerControl('source', 'src'); * @example * s("bd sd [~ bd] sd,hh*6").n("<0 1>") */ -// also see https://github.com/tidalcycles/strudel/pull/63 +// also see https://codeberg.org/uzu/strudel/pulls/63 export const { n } = registerControl('n'); /** * Plays the given note name or midi number. A note name consists of * * - a letter (a-g or A-G) * - optional accidentals (b or #) - * - optional octave number (0-9). Defaults to 3 + * - optional (possibly negative) octave number (0-9). Defaults to 3 * * Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5` * @@ -116,6 +360,8 @@ export const { n } = registerControl('n'); * note("c4 a4 f4 e4") * @example * note("60 69 65 64") + * @example + * note("fbb1 a#0 cbbb-1 e##-2").sound("saw") */ export const { note } = registerControl(['note', 'n']); @@ -131,8 +377,8 @@ export const { note } = registerControl(['note', 'n']); */ export const { accelerate } = registerControl('accelerate'); /** - * * Sets the velocity from 0 to 1. Is multiplied together with gain. + * * @name velocity * @example * s("hh*8") @@ -242,6 +488,20 @@ export const { fmenv } = registerControl('fmenv'); * */ export const { fmattack } = registerControl('fmattack'); + +/** + * Waveform of the fm modulator + * + * @name fmwave + * @param {number | Pattern} wave waveform + * @example + * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) + * @example + * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) + * + */ +export const { fmwave } = registerControl('fmwave'); + /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * @@ -285,6 +545,17 @@ export const { fmvelocity } = registerControl('fmvelocity'); */ export const { bank } = registerControl('bank'); +/** + * mix control for the chorus effect + * + * @name chorus + * @param {string | Pattern} chorus mix amount between 0 and 1 + * @example + * note("d d a# a").s("sawtooth").chorus(.5) + * + */ +export const { chorus } = registerControl('chorus'); + // analyser node send amount 0 - 1 (used by scope) export const { analyze } = registerControl('analyze'); // fftSize of analyser @@ -296,6 +567,7 @@ export const { fft } = registerControl('fft'); * * @name decay * @param {number | Pattern} time decay time in seconds + * @synonyms dec * @example * note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0) * @@ -348,11 +620,11 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' * s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>") * */ -// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'bandq' https://codeberg.org/uzu/strudel/issues/496 // ['bpq'], export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** - * a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. + * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. * * @memberof Pattern * @name begin @@ -413,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); */ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); /** - * bit crusher effect. + * Bit crusher effect. * * @name crush * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). @@ -424,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); // ['clhatdecay'], export const { crush } = registerControl('crush'); /** - * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers + * Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * * @name coarse * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. @@ -435,7 +707,80 @@ export const { crush } = registerControl('crush'); export const { coarse } = registerControl('coarse'); /** - * filter overdrive for supported filter types + * Modulate the amplitude of a sound with a continuous waveform + * + * @name tremolo + * @synonyms trem + * @param {number | Pattern} speed modulation speed in HZ + * @example + * note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>") + * + */ +export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); + +/** + * Modulate the amplitude of a sound with a continuous waveform + * + * @name tremolosync + * @synonyms tremsync + * @param {number | Pattern} cycles modulation speed in cycles + * @example + * note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>") + * + */ +export const { tremolosync } = registerControl( + ['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'], + 'tremsync', +); + +/** + * Depth of amplitude modulation + * + * @name tremolodepth + * @synonyms tremdepth + * @param {number | Pattern} depth + * @example + * note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>") + * + */ +export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); +/** + * Alter the shape of the modulation waveform + * + * @name tremoloskew + * @synonyms tremskew + * @param {number | Pattern} amount between 0 & 1, the shape of the waveform + * @example + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>") + * + */ +export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); + +/** + * Alter the phase of the modulation waveform + * + * @name tremolophase + * @synonyms tremphase + * @param {number | Pattern} offset the offset in cycles of the modulation + * @example + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>") + * + */ +export const { tremolophase } = registerControl('tremolophase', 'tremphase'); + +/** + * Shape of amplitude modulation + * + * @name tremoloshape + * @synonyms tremshape + * @param {number | Pattern} shape tri | square | sine | saw | ramp + * @example + * note("{f g c d}%16").tremsync(4).tremoloshape("").s("sawtooth") + * + */ +export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); +/** + * Filter overdrive for supported filter types * * @name drive * @param {number | Pattern} amount @@ -445,6 +790,118 @@ export const { coarse } = registerControl('coarse'); */ export const { drive } = registerControl('drive'); +/** + * Modulate the amplitude of an orbit to create a "sidechain" like effect. + * + * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")` + * + * @name duckorbit + * @synonyms duck + * @param {number | Pattern} orbit target orbit + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1) + * + */ +export const { duck } = registerControl('duckorbit', 'duck'); + +/** + * The amount of ducking applied to target orbit + * + * Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckdepth + * @param {number | Pattern} depth depth of modulation from 0 to 1 + * @example + * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>")) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5") + * + */ +export const { duckdepth } = registerControl('duckdepth'); + +/** + * The time required for the ducked signal(s) to reach their lowest volume. + * Can be used to prevent clicking or for creative rhythmic effects. + * + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckonset + * @synonyms duckons + * + * @param {number | Pattern} time The onset time in seconds + * @example + * // Clicks + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0) + * @example + * // No clicks + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0) + * @example + * // Rhythmic + * noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below + * hhat: s("hh*16").orbit(7) + * ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25) + * + */ +export const { duckonset } = registerControl('duckonset', 'duckons'); + +/** + * The time required for the ducked signal(s) to return to their normal volume. + * + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckattack + * @synonyms duckatt + * + * @param {number | Pattern} time The attack time in seconds + * @example + * sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1) + * @example + * moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * lessduck: s("hh*16").orbit(5) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1") + * + */ +export const { duckattack } = registerControl('duckattack', 'duckatt'); + +/** + * Create byte beats with custom expressions + * + * @name byteBeatExpression + * @synonyms bbexpr + * + * @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat + * @example + * s("bytebeat").bbexpr('t*(t>>15^t>>66)') + * + */ +export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr'); + +/** + * Create byte beats with custom expressions + * + * @name byteBeatStartTime + * @synonyms bbst + * + * @param {number | Pattern} byteBeatStartTime in samples (t) + * @example + * note("c3!8".add("{0 0 12 0 7 5 3}%8")).s("bytebeat:5").bbst("<3 1>".mul(10000))._scope() + * + */ +export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', 'bbst'); + /** * Allows you to set the output channels on the interface * @@ -458,6 +915,41 @@ export const { drive } = registerControl('drive'); */ export const { channels, ch } = registerControl('channels', 'ch'); +/** + * Controls the pulsewidth of the pulse oscillator + * + * @name pw + * @param {number | Pattern} pulsewidth + * @example + * note("{f a c e}%16").s("pulse").pw(".8:1:.2") + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0 .75 .5 1") + */ +export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); + +/** + * Controls the lfo rate for the pulsewidth of the pulse oscillator + * + * @name pwrate + * @param {number | Pattern} rate + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") + + * + */ +export const { pwrate } = registerControl('pwrate'); + +/** + * Controls the lfo sweep for the pulsewidth of the pulse oscillator + * + * @name pwsweep + * @param {number | Pattern} sweep + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") + * + */ +export const { pwsweep } = registerControl('pwsweep'); + /** * Phaser audio effect that approximates popular guitar pedals. * @@ -489,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl( export const { phasersweep, phs } = registerControl('phasersweep', 'phs'); /** - * The center frequency of the phaser in HZ. Defaults to 1000 + * The center frequency of the phaser in HZ. Defaults to 1000 * * @name phasercenter * @synonyms phc @@ -506,7 +998,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); * The amount the signal is affected by the phaser effect. Defaults to 0.75 * * @name phaserdepth - * @synonyms phd + * @synonyms phd, phasdp * @param {number | Pattern} depth number between 0 and 1 * @example * n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5) @@ -517,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp'); /** - * choose the channel the pattern is sent to in superdirt + * Choose the channel the pattern is sent to in superdirt * * @name channel * @param {number | Pattern} channel channel number @@ -794,7 +1286,7 @@ export const { fanchor } = registerControl('fanchor'); * s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>") * */ -// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496 // ['hpf'], /** * Applies a vibrato to the frequency of the oscillator. @@ -861,7 +1353,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>") * */ -// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'resonance' https://codeberg.org/uzu/strudel/issues/496 export const { resonance, lpq } = registerControl('resonance', 'lpq'); /** * DJ filter, below 0.5 is low pass filter, above is high pass filter. @@ -869,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq'); * @name djf * @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter * @example - * n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc() + * n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>") * */ export const { djf } = registerControl('djf'); @@ -903,26 +1395,55 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] * */ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); + +/** + * Sets the level of the signal that is fed back into the delay. + * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it + * + * @name delayfeedback + * @param {number | Pattern} feedback between 0 and 1 + * @synonyms delayfb, dfb + * @example + * s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>") + * + */ +export const { delayspeed } = registerControl('delayspeed'); /** * Sets the time of the delay effect. * - * @name delaytime - * @param {number | Pattern} seconds between 0 and Infinity + * @name delayspeed + * @param {number | Pattern} delayspeed controls the pitch of the delay feedback * @synonyms delayt, dt * @example - * s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>") + * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") * */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); -/* // TODO: test + +/** + * Sets the time of the delay effect in cycles. + * + * @name delaysync + * @param {number | Pattern} cycles delay length in cycles + * @synonyms delayt, dt + * @example + * s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8)) + * + */ +export const { delaysync } = registerControl('delaysync'); + +/** * Specifies whether delaytime is calculated relative to cps. * * @name lock * @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle. + * @superdirtOnly * @example * s("sd").delay().lock(1).osc() * + * */ + export const { lock } = registerControl('lock'); /** * Set detune for stacked voices of supported oscillators @@ -972,6 +1493,7 @@ export const { dry } = registerControl('dry'); * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime + * @synonyms fadeOutTime * @param {number | Pattern} time between 0 and 1 * @example * s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc() @@ -1227,7 +1749,7 @@ export const { semitone } = registerControl('semitone'); // TODO: synth param export const { voice } = registerControl('voice'); -// voicings // https://github.com/tidalcycles/strudel/issues/506 +// voicings // https://codeberg.org/uzu/strudel/issues/506 // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings export const { chord } = registerControl('chord'); // which dictionary to use for the voicings @@ -1306,6 +1828,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); * */ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); + +/** + * Sets speed of the sample for the impulse response. + * @name irspeed + * @param {string | Pattern} speed + * @example + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5) + * + */ +export const { irspeed } = registerControl('irspeed'); + +/** + * Sets the beginning of the IR response sample + * @name irbegin + * @param {string | Pattern} begin between 0 and 1 + * @synonyms ir + * @example + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6) + * + */ +export const { irbegin } = registerControl('irbegin'); /** * Sets the room size of the reverb, see `room`. * When this property is changed, the reverb will be recaculated, so only change this sparsely.. @@ -1461,7 +2006,7 @@ export const { vowel } = registerControl('vowel'); * @name waveloss */ export const { waveloss } = registerControl('waveloss'); -/* +/** * Noise crackle density * * @name density @@ -1474,18 +2019,6 @@ export const { density } = registerControl('density'); // ['modwheel'], export const { expression } = registerControl('expression'); export const { sustainpedal } = registerControl('sustainpedal'); -/* // TODO: doesn't seem to do anything - * - * Tremolo Audio DSP effect - * - * @name tremolodepth - * @param {number | Pattern} depth between 0 and 1 - * @example - * n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc() - * - */ -export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp'); -export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr'); export const { fshift } = registerControl('fshift'); export const { fshiftnote } = registerControl('fshiftnote'); @@ -1513,22 +2046,11 @@ export const { scram } = registerControl('scram'); export const { binshift } = registerControl('binshift'); export const { hbrick } = registerControl('hbrick'); export const { lbrick } = registerControl('lbrick'); -export const { midichan } = registerControl('midichan'); -export const { midimap } = registerControl('midimap'); -export const { midiport } = registerControl('midiport'); -export const { control } = registerControl('control'); -export const { ccn } = registerControl('ccn'); -export const { ccv } = registerControl('ccv'); -export const { polyTouch } = registerControl('polyTouch'); -export const { midibend } = registerControl('midibend'); -export const { miditouch } = registerControl('miditouch'); -export const { ctlNum } = registerControl('ctlNum'); + export const { frameRate } = registerControl('frameRate'); export const { frames } = registerControl('frames'); export const { hours } = registerControl('hours'); -export const { midicmd } = registerControl('midicmd'); export const { minutes } = registerControl('minutes'); -export const { progNum } = registerControl('progNum'); export const { seconds } = registerControl('seconds'); export const { songPtr } = registerControl('songPtr'); export const { uid } = registerControl('uid'); @@ -1573,7 +2095,6 @@ export const { zmod } = registerControl('zmod'); // like crush but scaled differently export const { zcrush } = registerControl('zcrush'); export const { zdelay } = registerControl('zdelay'); -export const { tremolo } = registerControl('tremolo'); export const { zzfx } = registerControl('zzfx'); /** @@ -1621,6 +2142,151 @@ export const ar = register('ar', (t, pat) => { return pat.set({ attack, release }); }); +//MIDI + +/** + * MIDI channel: Sets the MIDI channel for the event. + * + * @name midichan + * @param {number | Pattern} channel MIDI channel number (0-15) + * @example + * note("c4").midichan(1).midi() + */ +export const { midichan } = registerControl('midichan'); + +export const { midimap } = registerControl('midimap'); + +/** + * MIDI port: Sets the MIDI port for the event. + * + * @name midiport + * @param {number | Pattern} port MIDI port + * @example + * note("c a f e").midiport("<0 1 2 3>").midi() + */ +export const { midiport } = registerControl('midiport'); + +/** + * MIDI command: Sends a MIDI command message. + * + * @name midicmd + * @param {number | Pattern} command MIDI command + * @example + * midicmd("clock*48,/2").midi() + */ +export const { midicmd } = registerControl('midicmd'); + +/** + * MIDI control: Sends a MIDI control change message. + * + * @name control + * @param {number | Pattern} MIDI control number (0-127) + * @param {number | Pattern} MIDI controller value (0-127) + */ +export const control = register('control', (args, pat) => { + if (!Array.isArray(args)) { + throw new Error('control expects an array of [ccn, ccv]'); + } + const [_ccn, _ccv] = args; + return pat.ccn(_ccn).ccv(_ccv); +}); + +/** + * MIDI control number: Sends a MIDI control change message. + * + * @name ccn + * @param {number | Pattern} MIDI control number (0-127) + */ +export const { ccn } = registerControl('ccn'); +/** + * MIDI control value: Sends a MIDI control change message. + * + * @name ccv + * @param {number | Pattern} MIDI control value (0-127) + */ +export const { ccv } = registerControl('ccv'); +export const { ctlNum } = registerControl('ctlNum'); +// TODO: ctlVal? + +/** + * MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message. + * @name nrpnn + * @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127) + * @example + * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() + */ +export const { nrpnn } = registerControl('nrpnn'); +/** + * MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message. + * @name nrpv + * @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127) + * @example + * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() + */ +export const { nrpv } = registerControl('nrpv'); + +/** + * MIDI program number: Sends a MIDI program change message. + * + * @name progNum + * @param {number | Pattern} program MIDI program number (0-127) + * @example + * note("c4").progNum(10).midichan(1).midi() + */ +export const { progNum } = registerControl('progNum'); + +/** + * MIDI sysex: Sends a MIDI sysex message. + * @name sysex + * @param {number | Pattern} id Sysex ID + * @param {number | Pattern} data Sysex data + * @example + * note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi() + */ +export const sysex = register('sysex', (args, pat) => { + if (!Array.isArray(args)) { + throw new Error('sysex expects an array of [id, data]'); + } + const [id, data] = args; + return pat.sysexid(id).sysexdata(data); +}); +/** + * MIDI sysex ID: Sends a MIDI sysex identifier message. + * @name sysexid + * @param {number | Pattern} id Sysex ID + * @example + * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() + */ +export const { sysexid } = registerControl('sysexid'); +/** + * MIDI sysex data: Sends a MIDI sysex message. + * @name sysexdata + * @param {number | Pattern} data Sysex data + * @example + * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() + */ +export const { sysexdata } = registerControl('sysexdata'); + +/** + * MIDI pitch bend: Sends a MIDI pitch bend message. + * @name midibend + * @param {number | Pattern} midibend MIDI pitch bend (-1 - 1) + * @example + * note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi() + */ +export const { midibend } = registerControl('midibend'); +/** + * MIDI key after touch: Sends a MIDI key after touch message. + * @name miditouch + * @param {number | Pattern} miditouch MIDI key after touch (0-1) + * @example + * note("c4").miditouch(sine.slow(4).range(0,1)).midi() + */ +export const { miditouch } = registerControl('miditouch'); + +// TODO: what is this? +export const { polyTouch } = registerControl('polyTouch'); + export const getControlName = (alias) => { if (controlAlias.has(alias)) { return controlAlias.get(alias); @@ -1632,14 +2298,46 @@ export const getControlName = (alias) => { * Sets properties in a batch. * * @name as - * @param {Array} mapping the control names that are set + * @param {String | Array} mapping the control names that are set * @example * "c:.5 a:1 f:.25 e:.8".as("note:clip") + * @example + * "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1) */ export const as = register('as', (mapping, pat) => { + mapping = Array.isArray(mapping) ? mapping : [mapping]; return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); return v; }); }); + +/** + * Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file + * in the optional array syntax ex: "0.5:2", the second value controls the speed of playback + * @name scrub + * @memberof Pattern + * @returns Pattern + * @example + * samples('github:switchangel/pad') + * s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8") + * @example + * samples('github:yaxu/clean-breaks/main'); + * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) + */ + +export const scrub = register( + 'scrub', + (beginPat, pat) => { + return beginPat.outerBind((v) => { + if (!Array.isArray(v)) { + v = [v]; + } + const [beginVal, speedMultiplier = 1] = v; + + return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1); + }); + }, + false, +); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index bd2db1223..ad0961032 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,11 +1,11 @@ /* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 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 . */ import createClock from './zyklus.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; export class Cyclist { constructor({ @@ -65,8 +65,9 @@ export class Cyclist { (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; const duration = hap.duration / this.cps; // the following line is dumb and only here for backwards compatibility - // see https://github.com/tidalcycles/strudel/pull/1004 + // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; + // this onTrigger has another signature onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; @@ -75,7 +76,7 @@ export class Cyclist { } }); } catch (e) { - logger(`[cyclist] error: ${e.message}`); + errorLogger(e); onError?.(e); } }, diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 91b86b4ab..7509c0f6f 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,6 +1,6 @@ /* drawLine.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index ad0b01486..44ab07f11 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -2,7 +2,7 @@ euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms Copyright (C) 2023 Rohan Drape and strudel contributors -See for authors of this file. +See for authors of this file. The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape - https://rohandrape.net/?t=hmt @@ -10,9 +10,9 @@ https://rohandrape.net/?t=hmt 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 . */ -import { Pattern, timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; -import Fraction from './fraction.mjs'; +import Fraction, { lcm } from './fraction.mjs'; const left = function (n, x) { const [ons, offs] = n; @@ -42,29 +42,26 @@ const _bjork = function (n, x) { export const bjork = function (ons, steps) { const inverted = ons < 0; - ons = Math.abs(ons); - const offs = steps - ons; - const x = Array(ons).fill([1]); - const y = Array(offs).fill([0]); - const result = _bjork([ons, offs], [x, y]); - const p = flatten(result[1][0]).concat(flatten(result[1][1])); - if (inverted) { - return p.map((x) => (x === 0 ? 1 : 0)); - } - return p; + const absOns = Math.abs(ons); + const offs = steps - absOns; + const ones = Array(absOns).fill([1]); + const zeros = Array(offs).fill([0]); + const result = _bjork([absOns, offs], [ones, zeros]); + const pattern = flatten(result[1][0]).concat(flatten(result[1][1])); + return inverted ? pattern.map((x) => 1 - x) : pattern; }; /** - * Changes the structure of the pattern to form an euclidean rhythm. - * Euclidian rhythms are rhythms obtained using the greatest common + * Changes the structure of the pattern to form an Euclidean rhythm. + * Euclidean rhythms are rhythms obtained using the greatest common * divisor of two numbers. They were described in 2004 by Godfried - * Toussaint, a canadian computer scientist. Euclidian rhythms are + * Toussaint, a Canadian computer scientist. Euclidean rhythms are * really useful for computer/algorithmic music because they can * describe a large number of rhythms with a couple of numbers. * * @memberof Pattern * @name euclid - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @returns Pattern * @example @@ -76,7 +73,7 @@ export const bjork = function (ons, steps) { * Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence. * @memberof Pattern * @name euclidRot - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @returns Pattern @@ -86,13 +83,13 @@ export const bjork = function (ons, steps) { */ /** - * @example // A thirteenth century Persian rhythm called Khafif-e-ramal. + * @example // A thirteenth-century Persian rhythm called Khafif-e-ramal. * note("c3").euclid(2,5) * @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad. * note("c3").euclid(3,4) * @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm. * note("c3").euclidRot(3,5,2) - * @example // A Ruchenitza rhythm used in a Bulgarian folk-dance. + * @example // A Ruchenitza rhythm used in a Bulgarian folk dance. * note("c3").euclid(3,7) * @example // The Cuban tresillo pattern. * note("c3").euclid(3,8) @@ -142,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) { return pat.struct(_euclidRot(pulses, steps, 0)); }); +export const e = register('e', function (euc, pat) { + if (!Array.isArray(euc)) { + euc = [euc]; + } + const [pulses, steps = pulses, rot = 0] = euc; + return pat.struct(_euclidRot(pulses, steps, rot)); +}); + export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { return pat.struct(_euclidRot(pulses, steps, rotation)); }); @@ -151,8 +156,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * so there will be no gaps. * @name euclidLegato * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill + * @param rotation offset in steps + * @param pat * @example * note("c3").euclidLegato(3,8) */ @@ -161,13 +168,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) { if (pulses < 1) { return silence; } - const bin_pat = _euclidRot(pulses, steps, rotation); + const bin_pat = _euclidRot(pulses, steps, 0); const gapless = bin_pat .join('') .split('1') .slice(1) .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)); + return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps)); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { @@ -180,7 +187,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * the resulting sequence * @name euclidLegatoRot * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @example @@ -189,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { return _euclidLegato(pulses, steps, rotation, pat); }); + +/** + * A 'euclid' variant with an additional parameter that morphs the resulting + * rhythm from 0 (no morphing) to 1 (completely 'even'). For example + * `sound("bd").euclidish(3,8,0)` would be the same as + * `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the + * same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a + * groove somewhere between. + * Inspired by the work of Malcom Braff. + * @name euclidish + * @synonyms eish + * @memberof Pattern + * @param {number} pulses the number of onsets + * @param {number} steps the number of steps to fill + * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) + * @example + * sound("hh").euclidish(7,12,sine.slow(8)) + * .pan(sine.slow(8)) + */ +export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { + const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + return pat.struct(morphed).setSteps(steps); +}); diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..0599d8662 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -1,9 +1,11 @@ /* evaluate.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ +export const strudelScope = {}; + export const evalScope = async (...args) => { const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); @@ -18,6 +20,7 @@ export const evalScope = async (...args) => { modules.forEach((module) => { Object.entries(module).forEach(([name, value]) => { globalThis[name] = value; + strudelScope[name] = value; }); }); return modules; diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index dc7fe27a7..076fbadf6 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -1,6 +1,6 @@ /* fraction.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -126,6 +126,8 @@ export const lcm = (...fractions) => { ); }; +export const isFraction = (x) => x instanceof Fraction; + fraction._original = Fraction; export default fraction; diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index 7a9e0a620..5f820d644 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -1,9 +1,10 @@ /* hap.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import Fraction from './fraction.mjs'; +import { stringifyValues } from './util.mjs'; export class Hap { /* @@ -148,13 +149,7 @@ export class Hap { } showWhole(compact = false) { - return `${this.whole == undefined ? '~' : this.whole.show()}: ${ - typeof this.value === 'object' - ? compact - ? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') - : JSON.stringify(this.value) - : this.value - }`; + return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`; } combineContext(b) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index a10b68b09..e4daf445a 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index e13bf86ca..6727c2422 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -4,6 +4,13 @@ let debounce = 1000, lastMessage, lastTime; +export function errorLogger(e, origin = 'cyclist') { + if (process.env.NODE_ENV === 'development') { + console.error(e); + } + logger(`[${origin}] error: ${e.message}`); +} + export function logger(message, type, data = {}) { let t = performance.now(); if (lastMessage === message && t - lastTime < debounce) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index ad22cf006..261f08acb 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,6 +1,6 @@ /* neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -11,7 +11,6 @@ export class NeoCyclist { constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; - this.lastTick = 0; // absolute time when last tick (clock callback) happened this.getTime = getTime; // get absolute time this.time_at_last_tick_message = 0; // the clock of the worker and the audio context clock can drift apart over time diff --git a/packages/core/package.json b/packages/core/package.json index 9123259a5..7cf20cea7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.1.0", + "version": "1.2.4", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -27,7 +27,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4e5eb15d0..d0be3f599 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1,11 +1,11 @@ /* pattern.mjs - Core pattern representation for strudel -Copyright (C) 2025 Strudel contributors - see +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 . */ import TimeSpan from './timespan.mjs'; -import Fraction, { lcm } from './fraction.mjs'; +import Fraction, { isFraction, lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; @@ -21,6 +21,8 @@ import { numeralArgs, parseNumeral, pairs, + zipWith, + stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -852,14 +854,29 @@ export class Pattern { ); } - log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) { + /** + * Writes the content of the current event to the console (visible in the side menu). + * @name log + * @memberof Pattern + * @example + * s("bd sd").log() + */ + log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { return this.onTrigger((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } - logValues(func = id) { - return this.log((_, hap) => func(hap.value)); + /** + * A simplified version of `log` which writes all "values" (various configurable parameters) + * within the event to the console (visible in the side menu). + * @name logValues + * @memberof Pattern + * @example + * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues() + */ + logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) { + return this.log((hap) => func(hap.value)); } ////////////////////////////////////////////////////////////////////// @@ -869,6 +886,31 @@ export class Pattern { console.log(drawLine(this)); return this; } + + ////////////////////////////////////////////////////////////////////// + // methods relating to breaking patterns into subcycles + + // Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern. + unjoin(pieces, func = id) { + return pieces.withHap((hap) => + hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)), + ); + } + + /** + * Breaks a pattern into pieces according to the structure of a given pattern. + * True values in the given pattern cause the corresponding subcycle of the + * source pattern to be looped, and for an (optional) given function to be + * applied. False values result in the corresponding part of the source pattern + * to be played unchanged. + * @name into + * @memberof Pattern + * @example + * sound("bd sd ht lt").into("1 0", hurry(2)) + */ + into(pieces, func) { + return this.unjoin(pieces, func).innerJoin(); + } } ////////////////////////////////////////////////////////////////////// @@ -904,12 +946,13 @@ Pattern.prototype.collect = function () { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) * */ -Pattern.prototype.arpWith = function (func) { - return this.collect() +export const arpWith = register('arpWith', (func, pat) => { + return pat + .collect() .fmap((v) => reify(func(v))) .innerJoin() .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value))); -}; +}); /** * Selects indices in in stacked notes. @@ -917,9 +960,11 @@ Pattern.prototype.arpWith = function (func) { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]") * */ -Pattern.prototype.arp = function (pat) { - return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); -}; +export const arp = register( + 'arp', + (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])), + false, +); /* * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are @@ -954,7 +999,7 @@ addToPrototype('weaveWith', function (t, ...funcs) { // compose matrix functions function _nonArrayObject(x) { - return !Array.isArray(x) && typeof x === 'object'; + return !Array.isArray(x) && typeof x === 'object' && !isFraction(x); } function _composeOp(a, b, func) { if (_nonArrayObject(a) || _nonArrayObject(b)) { @@ -1201,7 +1246,8 @@ export const silence = gap(1); /* Like silence, but with a 'steps' (relative duration) of 0 */ export const nothing = gap(0); -/** A discrete value that repeats once per cycle. +/** + * A discrete value that repeats once per cycle. * * @returns {Pattern} * @example @@ -1254,13 +1300,14 @@ export function sequenceP(pats) { return result; } -/** The given items are played at the same time at the same length. +/** + * The given items are played at the same time at the same length. * * @return {Pattern} * @synonyms polyrhythm, pr * @example * stack("g3", "b3", ["e4", "d4"]).note() - * // "g3,b3,[e4,d4]".note() + * // "g3,b3,[e4 d4]".note() * * @example * // As a chained function: @@ -1328,7 +1375,7 @@ export function stackBy(by, ...pats) { left: stackLeft, right: stackRight, expand: stack, - repeat: (...args) => polymeterSteps(steps, ...args), + repeat: (...args) => polymeter(...args).steps(steps), }; return by .inhabit(lookup) @@ -1337,11 +1384,11 @@ export function stackBy(by, ...pats) { .setSteps(steps); } -/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: - * - * synonyms: `cat` +/** + * Concatenation: combines a list of patterns, switching between them successively, one per cycle. * * @return {Pattern} + * @synonyms cat * @example * slowcat("e5", "b4", ["d5", "c5"]) * @@ -1541,7 +1588,7 @@ export const func = curry((a, b) => reify(b).func(a)); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. * - * @param {string} name name of the function + * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @noAutocomplete * @@ -1981,6 +2028,7 @@ export const apply = register('apply', function (func, pat) { /** * Plays the pattern at the given cycles per minute. + * @deprecated * @example * s(",hh*2").cpm(90) // = 90 bpm */ @@ -2043,7 +2091,7 @@ export const zoom = register('zoom', function (s, e, pat) { return nothing; } const d = e.sub(s); - const steps = __steps ? pat._steps.mulmaybe(d) : undefined; + const steps = __steps ? pat._steps?.mulmaybe(d) : undefined; return pat .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) @@ -2105,11 +2153,13 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. + * @name segment + * @synonyms seg * @param {number} segments number of segments per cycle * @example * note(saw.range(40,52).segment(24)) */ -export const segment = register('segment', function (rate, pat) { +export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2349,6 +2399,57 @@ export const stut = register('stut', function (times, feedback, time, pat) { return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i))); }); +export const applyN = register('applyN', function (n, func, p) { + let result = p; + for (let i = 0; i < n; i++) { + result = func(result); + } + return result; +}); + +/** + * The plyWith function repeats each event the given number of times, applying the given function to each event.\n + * @name plyWith + * @synonyms plywith + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern + * @example + * "<0 [2 4]>" + * .plyWith(4, (p) => p.add(2)) + * .scale("C:minor").note() + */ +export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { + const result = pat + .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + +/** + * The plyForEach function repeats each event the given number of times, applying the given function to each event. + * This version of ply uses the iteration index as an argument to the function, similar to echoWith. + * @name plyForEach + * @synonyms plyforeach + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern and the iteration index + * @example + * "<0 [2 4]>" + * .plyForEach(4, (p,n) => p.add(n*2)) + * .scale("C:minor").note() + */ +export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) { + const result = pat + .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + /** * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. * @name iter @@ -2439,9 +2540,14 @@ const _chunk = function (n, func, pat, back = false, fast = false) { return pat.when(binary_pat, func); }; -export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) { - return _chunk(n, func, pat, false, false); -}); +export const { chunk, slowchunk, slowChunk } = register( + ['chunk', 'slowchunk', 'slowChunk'], + function (n, func, pat) { + return _chunk(n, func, pat, false, false); + }, + true, + true, +); /** * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles @@ -2453,9 +2559,14 @@ export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', ' * "0 1 2 3".chunkBack(4, x=>x.add(7)) * .scale("A:minor").note() */ -export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) { - return _chunk(n, func, pat, true); -}); +export const { chunkBack, chunkback } = register( + ['chunkBack', 'chunkback'], + function (n, func, pat) { + return _chunk(n, func, pat, true); + }, + true, + true, +); /** * Like `chunk`, but the cycles of the source pattern aren't repeated @@ -2469,8 +2580,44 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun * .fastChunk(4, x => x.color('red')).slow(2) * .scale("C2:major").note() */ -export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) { - return _chunk(n, func, pat, false, true); +export const { fastchunk, fastChunk } = register( + ['fastchunk', 'fastChunk'], + function (n, func, pat) { + return _chunk(n, func, pat, false, true); + }, + true, + true, +); + +/** + * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @name chunkInto + * @synonyms chunkinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); +}); + +/** + * Like `chunkInto`, but moves backwards through the chunks. + * @name chunkBackInto + * @synonyms chunkbackinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) { + return pat.into( + fastcat(true, ...Array(n - 1).fill(false)) + ._iter(n) + ._early(1), + func, + ); }); // TODO - redefine elsewhere in terms of mask @@ -2485,16 +2632,24 @@ export const bypass = register( ); /** - * Loops the pattern inside at `offset` for `cycles`. + * Loops the pattern inside an `offset` for `cycles`. + * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. + * @name ribbon + * @synonyms rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example - * note("").ribbon(1, 2).fast(2) + * note("").ribbon(1, 2) * @example * // Looping a portion of randomness - * note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2) + * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) + * @example + * // rhythm generator + * s("bd!16?").ribbon(29,.5) */ -export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) => + pat.early(offset).restart(pure(1).slow(cycles)), +); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); @@ -2648,34 +2803,10 @@ export function _polymeterListSteps(steps, ...args) { /** * *Experimental* * - * Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters. - * - * @name polymeterSteps - * @param {number} steps how many items are placed in one cycle - * @param {any[]} patterns one or more patterns - * @example - * // the same as "{c d, e f g}%4" - * polymeterSteps(4, "c d", "e f g").note() - */ -export function polymeterSteps(steps, ...args) { - if (args.length == 0) { - return silence; - } - if (Array.isArray(args[0])) { - // Support old behaviour - return _polymeterListSteps(steps, ...args); - } - - return polymeter(...args).pace(steps); -} - -/** - * *Experimental* - * - * Aligns the steps of the patterns, to match the steps per cycle of the first pattern, creating polymeters. See `polymeterSteps` to set the target steps explicitly. + * Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps. * @synonyms pm * @example - * // The same as note("{c eb g, c2 g2}") + * // The same as note("{c eb g, c2 g2}%6") * polymeter("c eb g", "c2 g2").note() * */ @@ -2691,13 +2822,12 @@ export function polymeter(...args) { if (args.length == 0) { return silence; } - const steps = args[0]._steps; + const steps = lcm(...args.map((x) => x._steps)); if (steps.eq(Fraction(0))) { return nothing; } - const [head, ...tail] = args; - const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps)))); + const result = stack(...args.map((x) => x.pace(steps))); result._steps = steps; return result; } @@ -2719,7 +2849,7 @@ export function stepcat(...timepats) { if (timepats.length === 0) { return nothing; } - const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]); + const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]); timepats = timepats.map(findsteps); if (timepats.find((x) => x[0] === undefined)) { const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); @@ -3065,8 +3195,6 @@ export const timeCat = stepcat; // Deprecated stepwise aliases export const s_cat = stepcat; export const s_alt = stepalt; -export const s_polymeterSteps = polymeterSteps; -Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps; export const s_polymeter = polymeter; Pattern.prototype.s_polymeter = Pattern.prototype.polymeter; export const s_taper = shrink; @@ -3194,6 +3322,25 @@ export const slice = register( false, // turns off auto-patternification ); +/** + * + * make something happen on event time + * uses browser timeout which is innacurate for audio tasks + * @name onTriggerTime + * @memberof Pattern + * @returns Pattern + * @example + * s("bd!8").onTriggerTime((hap) => {console.log(hap)}) + */ +Pattern.prototype.onTriggerTime = function (func) { + return this.onTrigger((hap, currentTime, _cps, targetTime) => { + const diff = targetTime - currentTime; + window.setTimeout(() => { + func(hap); + }, diff * 1000); + }, false); +}; + /** * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * @name splice @@ -3323,3 +3470,69 @@ export const { beat } = register( ['beat'], __beat((x) => x.innerJoin()), ); + +export const _morph = (from, to, by) => { + by = Fraction(by); + const dur = Fraction(1).div(from.length); + const positions = (list) => { + const result = []; + for (const [pos, value] of list.entries()) { + if (value) { + result.push([Fraction(pos).div(list.length), value]); + } + } + return result; + }; + const arcs = zipWith( + ([posa, valuea], [posb, valueb]) => { + const b = by.mul(posb - posa).add(posa); + const e = b.add(dur); + return new TimeSpan(b, e); + }, + positions(from), + positions(to), + ); + function query(state) { + const cycle = state.span.begin.sam(); + const cycleArc = state.span.cycleArc(); + const result = []; + for (const whole of arcs) { + const part = whole.intersection(cycleArc); + if (part !== undefined) { + result.push( + new Hap( + whole.withTime((x) => x.add(cycle)), + part.withTime((x) => x.add(cycle)), + true, + ), + ); + } + } + return result; + } + return new Pattern(query).splitQueries(); +}; + +/** + * Takes two binary rhythms represented as lists of 1s and 0s, and a number + * between 0 and 1 that morphs between them. The two lists should contain the same + * number of true values. + * @example + * sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm + * [1,1,0,1,0,1,0], // wonky rhythm + * 0.25 // creates a slightly wonky rhythm + * ) + * ) + * @example + * sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm + * "1:1:0:1:0:1:0", // wonky rhythm + * sine.slow(8) // slowly morph between the rhythms + * ) + * ) + */ +export const morph = (frompat, topat, bypat) => { + frompat = reify(frompat); + topat = reify(topat); + bypat = reify(bypat); + return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); +}; diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 334551e15..206fa619b 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -1,6 +1,6 @@ /* pick.mjs - methods that use one pattern to pick events from other patterns. -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 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 . */ @@ -156,8 +156,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) { return _pick(lookup, pat, true).resetJoin(); }); -/** - /** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). +/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. * @name inhabit * @synonyms pickSqueeze diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..53562f216 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,7 +1,7 @@ import { NeoCyclist } from './neocyclist.mjs'; import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; @@ -21,6 +21,7 @@ export function repl({ setInterval, clearInterval, id, + mondo = false, }) { const state = { schedulerError: undefined, @@ -73,6 +74,14 @@ export function repl({ return silence; }; + // helper to get a patternified pure value out + function unpure(pat) { + if (pat._Pattern) { + return pat.__pure; + } + return pat; + } + const setPattern = async (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; await scheduler.setPattern(pattern, autostart); @@ -84,8 +93,25 @@ export function repl({ const start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); - const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const setCps = (cps) => { + scheduler.setCps(unpure(cps)); + return silence; + }; + + /** + * Changes the global tempo to the given cycles per minute + * + * @name setcpm + * @alias setCpm + * @param {number} cpm cycles per minute + * @example + * setcpm(140/4) // =140 bpm in 4/4 + * $: s("bd*4,[- sd]*2").bank('tr707') + */ + const setCpm = (cpm) => { + scheduler.setCps(unpure(cpm) / 60); + return silence; + }; // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. @@ -102,8 +128,9 @@ export function repl({ * all(x => x.pianoroll()) * ``` */ + let allTransforms = []; const all = function (transform) { - allTransform = transform; + allTransforms.push(transform); return silence; }; /** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern. @@ -179,7 +206,12 @@ export function repl({ await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); + allTransforms = []; // reset all transforms shouldHush && hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = Object.values(pPatterns); @@ -191,8 +223,10 @@ export function repl({ } else if (eachTransform) { pattern = eachTransform(pattern); } - if (allTransform) { - pattern = allTransform(pattern); + if (allTransforms.length) { + for (let i in allTransforms) { + pattern = allTransforms[i](pattern); + } } if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; @@ -225,16 +259,17 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => async (hap, deadline, duration, cps, t) => { - // TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004 + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { await defaultOutput(hap, deadline, duration, cps, t); } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); + await hap.context.onTrigger(hap, getTime(), cps, t); } } catch (err) { - logger(`[cyclist] error: ${err.message}`, 'error'); + errorLogger(err, 'getTrigger'); } }; diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..6bd6fde67 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,6 +1,6 @@ /* signal.mjs - continuous patterns -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 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 . */ @@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. - * * @return {Pattern} * @example * n(sine.segment(16).range(0,15)) @@ -100,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); /** * A square signal between 0 and 1. - * * @return {Pattern} * @example * n(square.segment(4).range(0,7)).scale("C:minor") @@ -266,7 +264,7 @@ export const randrun = (n) => { const rands = timeToRands(t.floor().add(0.5), n); const nums = rands .map((n, i) => [n, i]) - .sort((a, b) => a[0] > b[0] - a[0] < b[0]) + .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) .map((x) => x[1]); const i = t.cyclePos().mul(n).floor() % n; return nums[i]; @@ -279,26 +277,26 @@ const _rearrangeWith = (ipat, n, pat) => { }; /** - * @name shuffle * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. + * @name shuffle * @example * note("c d e f").sound("piano").shuffle(4) * @example - * note("c d e f".shuffle(4), "g").sound("piano") + * seq("c d e f".shuffle(4), "g").note().sound("piano") */ export const shuffle = register('shuffle', (n, pat) => { return _rearrangeWith(randrun(n), n, pat); }); /** - * @name scramble * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. + * @name scramble * @example * note("c d e f").sound("piano").scramble(4) * @example - * note("c d e f".scramble(4), "g").sound("piano") + * seq("c d e f".scramble(4), "g").note().sound("piano") */ export const scramble = register('scramble', (n, pat) => { return _rearrangeWith(_irand(n)._segment(n), n, pat); @@ -398,6 +396,10 @@ export const chooseInWith = (pat, xs) => { */ export const choose = (...xs) => chooseWith(rand, xs); +// todo: doc +export const chooseIn = (...xs) => chooseInWith(rand, xs); +export const chooseOut = choose; + /** * 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 @@ -486,13 +488,34 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair export const wrandcat = wchooseCycles; -// this function expects pat to be a pattern of floats... -export const perlinWith = (pat) => { - const pata = pat.fmap(Math.floor); - const patb = pat.fmap((t) => Math.floor(t) + 1); +function _perlin(t) { + let ta = Math.floor(t); + let tb = ta + 1; const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3; const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a); - return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand)); + const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb)); + return v; +} +export const perlinWith = (tpat) => { + return tpat.fmap(_perlin); +}; + +function _berlin(t) { + const prevRidgeStartIndex = Math.floor(t); + const nextRidgeStartIndex = prevRidgeStartIndex + 1; + + const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex); + const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint; + + const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex); + const interp = (a, b, t) => { + return a + (b - a) * t; + }; + return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2; +} + +export const berlinWith = (tpat) => { + return tpat.fmap(_berlin); }; /** @@ -506,6 +529,18 @@ export const perlinWith = (pat) => { */ export const perlin = perlinWith(time.fmap((v) => Number(v))); +/** + * Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful, + * like perlin noise but with sawtooth waves), in the range 0..1. + * + * @name berlin + * @example + * // ascending arpeggios + * n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor") + * + */ +export const berlin = berlinWith(time.fmap((v) => Number(v))); + export const degradeByWith = register( 'degradeByWith', (withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), @@ -526,6 +561,9 @@ export const degradeByWith = register( * s("hh*8").degradeBy(0.2) * @example * s("[hh?0.2]*8") + * @example + * //beat generator + * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( 'degradeBy', diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index 6ae959544..e0f60184e 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -1,6 +1,6 @@ /* speak.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) { } export const speak = register('speak', function (lang, voice, pat) { - return pat.onTrigger((_, hap) => { + return pat.onTrigger((hap) => { triggerSpeech(hap.value, lang, voice); }); }); diff --git a/packages/core/state.mjs b/packages/core/state.mjs index db1f77eff..162dc7da9 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -1,6 +1,6 @@ /* state.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index f0af02eb6..667c3a4e5 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -1,6 +1,6 @@ /* controls.test.mjs - -Copyright (C) 2023 Strudel contributors - see +Copyright (C) 2023 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 . */ diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index 84359a7cf..2240080cd 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,6 +1,6 @@ /* drawLine.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js new file mode 100644 index 000000000..a33ec9514 --- /dev/null +++ b/packages/core/test/euclid.test.js @@ -0,0 +1,89 @@ +import { bjork } from '../euclid.mjs'; +import { describe, expect, it } from 'vitest'; +import { fastcat } from '../pattern.mjs'; + +describe('bjork', () => { + it('should apply bjorklund to ons and steps', () => { + expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); + expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]); + expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]); + expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]); + expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); + }); +}); + +describe('euclid', () => { + it('Can create euclid', () => { + expect( + fastcat('a') + .euclid(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']); + expect( + fastcat('a') + .euclid(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); + }); +}); + +describe('euclidRot', () => { + it('Can create euclidRot', () => { + expect( + fastcat('a') + .euclidRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']); + expect( + fastcat('a') + .euclidRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']); + }); +}); + +describe('euclidLegato', () => { + it('Can create euclidLegato', () => { + expect( + fastcat('a') + .euclidLegato(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']); + expect( + fastcat('a') + .euclidLegato(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); + }); +}); + +describe('euclidLegatoRot', () => { + it('Can create euclidLegatoRot', () => { + expect( + fastcat('a') + .euclidLegatoRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']); + expect( + fastcat('a') + .euclidLegatoRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']); + }); +}); diff --git a/packages/core/test/fraction.test.mjs b/packages/core/test/fraction.test.mjs index 6b710d043..b167c9483 100644 --- a/packages/core/test/fraction.test.mjs +++ b/packages/core/test/fraction.test.mjs @@ -1,6 +1,6 @@ /* fraction.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 0b72d08a8..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1,12 +1,12 @@ /* pattern.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import Fraction from 'fraction.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { TimeSpan, @@ -22,7 +22,6 @@ import { sequence, palindrome, polymeter, - polymeterSteps, polyrhythm, silence, fast, @@ -53,8 +52,11 @@ import { stackCentre, stepcat, sometimes, + expand, } from '../index.mjs'; +import { log, logValues } from '../pattern.mjs'; + import { steady } from '../signal.mjs'; import { n, s } from '../controls.mjs'; @@ -611,17 +613,10 @@ describe('Pattern', () => { }); describe('polymeter()', () => { it('Can layer up cycles, stepwise, with lists', () => { - expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual( - fastcat(pure('d'), pure('e'), pure('d')).firstCycle(), - ); - expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual( stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(), ); }); - it('Can layer up cycles, stepwise, with weighted patterns', () => { - sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b')); - }); }); describe('firstOf()', () => { @@ -892,7 +887,7 @@ describe('Pattern', () => { ); }); it('Doesnt drop haps in the 9th cycle', () => { - // fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a + // fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6); }); }); @@ -1009,7 +1004,7 @@ describe('Pattern', () => { }); describe('hurry', () => { it('Can speed up patterns and sounds', () => { - sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2)); + sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2)); }); }); /*describe('composable functions', () => { @@ -1187,6 +1182,9 @@ describe('Pattern', () => { it('calculates undefined steps as the average', () => { expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3))); }); + it('works with auto-reified values', () => { + expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))); + }); }); describe('shrink', () => { it('can shrink', () => { @@ -1275,4 +1273,75 @@ describe('Pattern', () => { ); }); }); + describe('unjoin', () => { + it('destructures a pattern into subcycles', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd') + .unjoin(fastcat(true, fastcat(true, true))) + .fmap(fast(2)) + .join(), + fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'), + ); + }); + }); + describe('into', () => { + it('applies a function to subcycles of a pattern', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)), + fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'), + ); + }); + }); + describe('chunkinto', () => { + it('chunks into subcycles', () => { + sameFirst( + fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')), + ); + }); + }); + describe('chunkbackinto', () => { + it('chunks into subcycles backwards', () => { + sameFirst( + fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), + fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'), + ); + }); + }); + describe('log', () => { + it('logs to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').log(); + const haps = pattern.queryArc(0, 1); + + // Force a trigger + haps.forEach((hap) => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] 0/1 → 1/1: a', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); + describe('logValues', () => { + it('logs values to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').note('c#').logValues(); + const haps = pattern.queryArc(0, 1); + + // Force a trigger + haps.forEach((hap) => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] value:a note:c#', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); }); diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 6b1053a3f..d3033a82c 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -1,26 +1,30 @@ /* util.test.mjs - Tests for the core 'util' module -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import { pure } from '../pattern.mjs'; import { - isNote, - tokenizeNote, - noteToMidi, - midiToFreq, - freqToMidi, _mod, compose, + flatten, + fractionalArgs, + freqToMidi, getFrequency, getPlayableNoteValue, - parseNumeral, - parseFractional, + isNote, + midiToFreq, + noteToMidi, numeralArgs, - fractionalArgs, + parseFractional, + parseNumeral, + rotate, + splitAt, + tokenizeNote, + zipWith, } from '../util.mjs'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; describe('isNote', () => { it('should recognize notes without accidentals', () => { @@ -233,3 +237,46 @@ describe('fractionalArgs', () => { expect(add('q', 2)).toBe(2.25); }); }); + +describe('rotate', () => { + it('should rotate array to the left', () => { + expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]); + expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]); + expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]); + expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]); + expect(rotate([0], 3)).toStrictEqual([0]); + expect(rotate([], 3)).toStrictEqual([]); + }); +}); + +describe('flatten', () => { + it('should flatten array by one level', () => { + expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]); + expect(flatten([0])).toStrictEqual([0]); + expect(flatten([])).toStrictEqual([]); + }); +}); + +describe('splitAt', () => { + it('should split array into two', () => { + expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([ + [0, 1], + [2, 3], + ]); + expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]); + expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]); + expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]); + }); +}); + +describe('zipWith', () => { + it('should use the function to combine the two arrays element-wise', () => { + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]); + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]); + expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]); + expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]); + expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]); + }); +}); diff --git a/packages/core/test/value.test.mjs b/packages/core/test/value.test.mjs index 87cba57d0..35d9f5e10 100644 --- a/packages/core/test/value.test.mjs +++ b/packages/core/test/value.test.mjs @@ -1,6 +1,6 @@ /* value.test.mjs - -Copyright (C) 2022 Strudel contributors - see +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 . */ diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 4cbfb999a..446156bbf 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -1,6 +1,6 @@ /* timespan.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -72,7 +72,7 @@ export class TimeSpan { } intersection(other) { - // Intersection of two timespans, returns None if they don't intersect. + // Intersection of two timespans, returns undefined if they don't intersect. const intersect_begin = this.begin.max(other.begin); const intersect_end = this.end.min(other.end); diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index 86ceb2863..5a2c54a30 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -1,6 +1,6 @@ /* ui.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b7b1e8413..ef3f1e961 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -1,6 +1,6 @@ /* util.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -8,12 +8,12 @@ import { logger } from './logger.mjs'; // 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][#bsf]*[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])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } @@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse()); // Removes 'None' values from given list export const removeUndefineds = (xs) => xs.filter((x) => x != undefined); +// flattens by one level export const flatten = (arr) => [].concat(...arr); export const id = (a) => a; @@ -237,6 +238,7 @@ export const splitAt = function (index, value) { return [value.slice(0, index), value.slice(index)]; }; +// Uses the function f to combine the arrays xs, ys element-wise export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const pairs = function (xs) { @@ -485,3 +487,13 @@ export function getCurrentKeyboardState() { // } // return lcm((x * y) / gcd(x, y), ...z); // }; + +// Takes values -- typically derived from events, i.e. `hap`s -- and renders them +// into a readable format +export function stringifyValues(value, compact = false) { + return typeof value === 'object' + ? compact + ? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') + : JSON.stringify(value) + : value; +} diff --git a/packages/core/value.mjs b/packages/core/value.mjs index ef98bc370..9496405eb 100644 --- a/packages/core/value.mjs +++ b/packages/core/value.mjs @@ -1,6 +1,6 @@ /* value.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -9,7 +9,7 @@ import { logger } from './logger.mjs'; export function unionWithObj(a, b, func) { if (b?.value !== undefined && Object.keys(b).length === 1) { - // https://github.com/tidalcycles/strudel/issues/1026 + // https://codeberg.org/uzu/strudel/issues/1026 logger(`[warn]: Can't do arithmetic on control pattern.`); return a; } diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index 1eeaf6fa5..e44765227 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => { instrument = instrument || 'triangle'; init(); // not async to support csound inside other patterns + to be able to call pattern methods after it // TODO: find a alternative way to wait for csound to load (to wait with first time playback) - return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { p1 = `"${instrument}"`; } init(); // not async to support csound inside other patterns + to be able to call pattern methods after it - return pat.onTrigger((tidal_time, hap) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { throw new Error('csound only support objects as hap values'); } // Time in seconds counting from now. - const p2 = tidal_time - getAudioContext().currentTime; + const p2 = targetTime - currentTime; const p3 = hap.duration.valueOf() + 0; const frequency = getFrequency(hap); let { gain = 1, velocity = 0.9 } = hap.value; diff --git a/packages/csound/package.json b/packages/csound/package.json index cf71dfbc3..90130a101 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.1.0", + "version": "1.2.5", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@csound/browser": "6.18.7", "@strudel/core": "workspace:*", diff --git a/packages/desktopbridge/index.mjs b/packages/desktopbridge/index.mjs index 591bbe34f..ffb88783f 100644 --- a/packages/desktopbridge/index.mjs +++ b/packages/desktopbridge/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/desktopbridge/midibridge.mjs b/packages/desktopbridge/midibridge.mjs index 471c826ea..2436d8ad7 100644 --- a/packages/desktopbridge/midibridge.mjs +++ b/packages/desktopbridge/midibridge.mjs @@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80; const CC_MESSAGE = 0xb0; Pattern.prototype.midi = function (output) { - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; //magic number to get audio engine to line up, can probably be calculated somehow const latencyMs = 34; diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 9bead6d19..2568c78e5 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs'; const collator = new ClockCollator({}); -export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { const controls = parseControlsFromHap(hap, cps); const params = []; const timestamp = collator.calculateTimestamp(currentTime, targetTime); diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index 45e89f44e..a01ec1f8a 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -19,11 +19,11 @@ "author": "Jade Rowland ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "dependencies": { "@strudel/core": "workspace:*", "@tauri-apps/api": "^2.2.0" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" -} \ No newline at end of file + "homepage": "https://codeberg.org/uzu/strudel#readme" +} diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..a429395ff 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -1,6 +1,6 @@ /* draw.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -150,7 +150,7 @@ export class Drawer { this.lastFrame = phase; this.visibleHaps = (this.visibleHaps || []) // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.endClipped >= phase - lookbehind - lookahead) + .filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead) // add new haps with onset (think right edge bars scrolling in) .concat(haps.filter((h) => h.hasOnset())); const time = phase - lookahead; @@ -175,7 +175,7 @@ export class Drawer { // +0.1 = workaround for weird holes in query.. 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); + this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t); this.painters = []; // will get populated by .onPaint calls attached to the pattern // query future haps const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters }); diff --git a/packages/draw/package.json b/packages/draw/package.json index e93f3ed22..6a4c57540 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.1.0", + "version": "1.2.4", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index d874c9686..1cf218fa0 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -1,6 +1,6 @@ /* pianoroll.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/embed/package.json b/packages/embed/package.json index afd887887..6a00ee904 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,12 +1,12 @@ { "name": "@strudel/embed", - "version": "1.1.0", + "version": "1.1.1", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -18,7 +18,7 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" + "homepage": "https://codeberg.org/uzu/strudel#readme" } diff --git a/packages/gamepad/README.md b/packages/gamepad/README.md new file mode 100644 index 000000000..239353fb7 --- /dev/null +++ b/packages/gamepad/README.md @@ -0,0 +1,93 @@ +# @strudel/gamepad + +This package adds gamepad input functionality to strudel Patterns. + +## Install + +```sh +npm i @strudel/gamepad --save +``` + +## Usage + +```javascript +import { gamepad } from '@strudel/gamepad'; + +// Initialize gamepad (optional index parameter, defaults to 0) +const pad = gamepad(0); + +// Use gamepad inputs in patterns +const pattern = sequence([ + // Button inputs + pad.a, // A button value (0-1) + pad.tglA, // A button toggle (0 or 1) + + // Analog stick inputs + pad.x1, // Left stick X (0-1) + pad.x1_2, // Left stick X (-1 to 1) +]); +``` + +## Available Controls + +### Buttons +- Face Buttons + - `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) + - Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` +- Shoulder Buttons + - `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) + - Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` +- D-Pad + - `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) + - Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`) + +### Analog Sticks +- Left Stick + - `x1`, `y1` (0 to 1 range) + - `x1_2`, `y1_2` (-1 to 1 range) +- Right Stick + - `x2`, `y2` (0 to 1 range) + - `x2_2`, `y2_2` (-1 to 1 range) + +## Examples + +```javascript +// Use button values to control amplitude +$: sequence([ + s("bd").gain(pad.X), // X button controls gain + s("[hh oh]").gain(pad.tglY), // Y button toggles gain +]); + +// Use analog stick for continuous control +$: note("c4*4".add(pad.y1_2.range(-24,24))) // Left stick Y controls pitch shift + .pan(pad.x1_2); // Left stick X controls panning + +// Use toggle buttons to switch patterns on/off + +// Define button sequences +const HADOKEN = [ + 'd', // Down + 'r', // Right + 'a', // A +]; + +const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA + +// Add these lines to enable buttons(but why?) +$:pad.D.segment(16).gain(0) +$:pad.R.segment(16).gain(0) +$:pad.A.segment(16).gain(0) + +// Check button sequence (returns 1 when detected, 0 when not within last 1 second) +$: sound("hadoken").gain(pad.checkSequence(HADOKEN)) + +``` + +## Multiple Gamepads + +You can connect multiple gamepads by specifying the gamepad index: + +```javascript +const pad1 = gamepad(0); // First gamepad +const pad2 = gamepad(1); // Second gamepad +``` diff --git a/packages/gamepad/docs/gamepad.mdx b/packages/gamepad/docs/gamepad.mdx new file mode 100644 index 000000000..e2da594ea --- /dev/null +++ b/packages/gamepad/docs/gamepad.mdx @@ -0,0 +1,118 @@ +import { MiniRepl } from '../../../website/src/docs/MiniRepl'; + +# Gamepad + +The Gamepad module allows you to integrate gamepad input functionality into your musical patterns. This can be particularly useful for live performances or interactive installations where you want to manipulate sounds using a game controller. + +## Getting Started + +Initialize a gamepad by calling the gamepad() function with an optional index parameter. + + + +## Available Controls + +The gamepad module provides access to buttons and analog sticks as normalized signals (0-1) that can modulate your patterns. + +### Buttons + +| Type | Controls | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| Face Buttons | `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) | +| | Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` | +| Shoulder Buttons | `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) | +| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` | +| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) | +| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) | + +### Analog Sticks + +| Stick | Controls | +| ----------- | ------------------------------ | +| Left Stick | `x1`, `y1` (0 to 1 range) | +| | `x1_2`, `y1_2` (-1 to 1 range) | +| Right Stick | `x2`, `y2` (0 to 1 range) | +| | `x2_2`, `y2_2` (-1 to 1 range) | + +### Button Sequence + +| Stick | Controls | +| --------------- | --------------------------------------- | +| Button Sequence | `btnSequence()`, `btnSeq()`, `btnseq()` | + +## Using Gamepad Inputs + +Once initialized, you can use various gamepad inputs in your patterns. Here are some examples: + +### Button Inputs + +You can use button inputs to control different aspects of your music, such as gain or triggering events. + + + +### Analog Stick Inputs + +Analog sticks can be used for continuous control, such as pitch shifting or panning. + + + +### Button Sequences + +You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected. + + + +## Multiple Gamepads + +Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices. + + diff --git a/packages/gamepad/gamepad.mjs b/packages/gamepad/gamepad.mjs new file mode 100644 index 000000000..7667a3620 --- /dev/null +++ b/packages/gamepad/gamepad.mjs @@ -0,0 +1,246 @@ +// @strudel/gamepad/index.mjs + +import { signal } from '@strudel/core'; + +// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216) +export const buttonMap = { + a: 0, + b: 1, + x: 2, + y: 3, + lb: 4, + rb: 5, + lt: 6, + rt: 7, + back: 8, + start: 9, + u: 12, + up: 12, + d: 13, + down: 13, + l: 14, + left: 14, + r: 15, + right: 15, +}; + +class ButtonSequenceDetector { + constructor(timeWindow = 1000) { + this.sequence = []; + this.timeWindow = timeWindow; + this.lastInputTime = 0; + this.buttonStates = Array(16).fill(0); // Track previous state of each button + // Button mapping for character inputs + } + + addInput(buttonIndex, buttonValue) { + const currentTime = Date.now(); + + // Only add input on button press (rising edge) + if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) { + // Clear sequence if too much time has passed + if (currentTime - this.lastInputTime > this.timeWindow) { + this.sequence = []; + } + + // Store the button name instead of index + const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString(); + + this.sequence.push({ + input: buttonName, + timestamp: currentTime, + }); + + this.lastInputTime = currentTime; + + //console.log(this.sequence); + // Keep only inputs within the time window + this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow); + } + + // Update button state + this.buttonStates[buttonIndex] = buttonValue; + } + + checkSequence(targetSequence) { + if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') { + console.error('ButtonSequenceDetector: targetSequence must be an array or string'); + return 0; + } + + if (this.sequence.length < targetSequence.length) return 0; + + // Convert string input to array if needed + const sequence = + typeof targetSequence === 'string' + ? targetSequence.toLowerCase().split('') + : targetSequence.map((s) => s.toString().toLowerCase()); + + //console.log(this.sequence); + + // Get the last n inputs where n is the target sequence length + const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input); + + // Compare sequences + return lastInputs.every((input, index) => { + const target = sequence[index]; + // Check if either the input matches directly or they refer to the same button in the map + return ( + input === target || + buttonMap[input] === buttonMap[target] || + // Also check if the numerical index matches + buttonMap[input] === parseInt(target) + ); + }) + ? 1 + : 0; + } +} + +class GamepadHandler { + constructor(index = 0) { + // Add index parameter + this._gamepads = {}; + this._activeGamepad = index; // Use provided index + this._axes = [0, 0, 0, 0]; + this._buttons = Array(16).fill(0); + this.setupEventListeners(); + } + + setupEventListeners() { + window.addEventListener('gamepadconnected', (e) => { + this._gamepads[e.gamepad.index] = e.gamepad; + if (!this._activeGamepad) { + this._activeGamepad = e.gamepad.index; + } + }); + + window.addEventListener('gamepaddisconnected', (e) => { + delete this._gamepads[e.gamepad.index]; + if (this._activeGamepad === e.gamepad.index) { + this._activeGamepad = Object.keys(this._gamepads)[0] || null; + } + }); + } + + poll() { + if (this._activeGamepad !== null) { + const gamepad = navigator.getGamepads()[this._activeGamepad]; + if (gamepad) { + // Update axes (normalized to 0-1 range) + this._axes = gamepad.axes.map((axis) => (axis + 1) / 2); + // Update buttons + this._buttons = gamepad.buttons.map((button) => button.value); + } + } + } + + getAxes() { + return this._axes; + } + getButtons() { + return this._buttons; + } +} + +// Module-level state store for toggle states +const gamepadStates = new Map(); + +export const gamepad = (index = 0) => { + const handler = new GamepadHandler(index); + const sequenceDetector = new ButtonSequenceDetector(2000); + + // Base signal that polls gamepad state and handles sequence detection + const baseSignal = signal((t) => { + handler.poll(); + const axes = handler.getAxes(); + const buttons = handler.getButtons(); + + // Add all button inputs to sequence detector + buttons.forEach((value, i) => { + sequenceDetector.addInput(i, value); + }); + + return { axes, buttons, t }; + }); + + // Create axes patterns + const axes = { + x1: baseSignal.fmap((state) => state.axes[0]), + y1: baseSignal.fmap((state) => state.axes[1]), + x2: baseSignal.fmap((state) => state.axes[2]), + y2: baseSignal.fmap((state) => state.axes[3]), + }; + + // Add bipolar versions + axes.x1_2 = axes.x1.toBipolar(); + axes.y1_2 = axes.y1.toBipolar(); + axes.x2_2 = axes.x2.toBipolar(); + axes.y2_2 = axes.y2.toBipolar(); + + // Create button patterns + const buttons = Array(16) + .fill(null) + .map((_, i) => { + // Create unique key for this gamepad+button combination + const stateKey = `gamepad${index}_btn${i}`; + + // Initialize toggle state if it doesn't exist + if (!gamepadStates.has(stateKey)) { + gamepadStates.set(stateKey, { + lastButtonState: 0, + toggleState: 0, + }); + } + + // Direct button value pattern (no longer needs to call addInput) + const btn = baseSignal.fmap((state) => state.buttons[i]); + + // Button toggle pattern with persistent state + const toggle = baseSignal.fmap((state) => { + const currentState = state.buttons[i]; + const buttonState = gamepadStates.get(stateKey); + + if (currentState === 1 && buttonState.lastButtonState === 0) { + // Toggle the state on rising edge + buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0; + } + + buttonState.lastButtonState = currentState; + return buttonState.toggleState; + }); + + return { value: btn, toggle }; + }); + + // Create sequence checker pattern + const btnSequence = (sequence) => { + return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence)); + }; + const checkSequence = btnSequence; + const btnSeq = btnSequence; + const btnseq = btnSeq; + + // Return an object with all controls + return { + ...axes, + buttons, + ...Object.fromEntries( + Object.entries(buttonMap).flatMap(([key, index]) => [ + [key.toLowerCase(), buttons[index].value], + [key.toUpperCase(), buttons[index].value], + [`tgl${key.toLowerCase()}`, buttons[index].toggle], + [`tgl${key.toUpperCase()}`, buttons[index].toggle], + ]), + ), + checkSequence, + btnSequence, + btnSeq, + btnseq, + raw: baseSignal, + }; +}; + +// Optional: Export for debugging or state management +export const getGamepadStates = () => Object.fromEntries(gamepadStates); +export const clearGamepadStates = () => gamepadStates.clear(); diff --git a/packages/gamepad/index.mjs b/packages/gamepad/index.mjs new file mode 100644 index 000000000..c5558fb99 --- /dev/null +++ b/packages/gamepad/index.mjs @@ -0,0 +1,3 @@ +import './gamepad.mjs'; + +export * from './gamepad.mjs'; diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json new file mode 100644 index 000000000..555eac03f --- /dev/null +++ b/packages/gamepad/package.json @@ -0,0 +1,37 @@ +{ + "name": "@strudel/gamepad", + "version": "1.2.4", + "description": "Gamepad Inputs for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://codeberg.org/uzu/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://codeberg.org/uzu/strudel/issues" + }, + "homepage": "https://codeberg.org/uzu/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "vite": "^6.0.11" + } +} diff --git a/packages/gamepad/vite.config.js b/packages/gamepad/vite.config.js new file mode 100644 index 000000000..5df3edc1b --- /dev/null +++ b/packages/gamepad/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/hs2js/README.md b/packages/hs2js/README.md index 24d9c7749..f84f9cded 100644 --- a/packages/hs2js/README.md +++ b/packages/hs2js/README.md @@ -2,7 +2,7 @@ Experimental haskell in javascript interpreter. Many haskell features are not implemented. This projects mainly exists to be able to write and interpret [Tidal Cycles](https://tidalcycles.org/) code in the browser, -as part of [Strudel](https://github.com/tidalcycles/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). +as part of [Strudel](https://codeberg.org/uzu/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). ## Installation diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index 34bb7f368..c0bf8fa8c 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -1,6 +1,7 @@ { "name": "hs2js", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Haskell in JavaScript interpreter", "main": "src/index.mjs", "type": "module", @@ -15,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "haskell", @@ -24,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel", + "homepage": "https://codeberg.org/uzu/strudel/", "dependencies": { "web-tree-sitter": "^0.24.7" }, diff --git a/packages/hydra/package.json b/packages/hydra/package.json index c687d5497..7ba79d5d0 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.1.0", + "version": "1.2.4", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/midi/README.md b/packages/midi/README.md index 6bb649f2b..aa90992da 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns. ```sh npm i @strudel/midi --save ``` + +## Available Controls + +The following MIDI controls are available: + +OUTPUT: + +- `midi` - opens a midi output device. +- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127) +- `midichan` - Sets the MIDI channel (1-16, defaults to 1) +- `velocity` - Sets note velocity (0-1, defaults to 0.9) +- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1) +- `control` - Sets MIDI control change messages +- `ccn` - Sets MIDI CC controller number (0-127) +- `ccv` - Sets MIDI CC value (0-1) +- `progNum` - Sends MIDI program change messages (0-127) +- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127) +- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127) +- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127) +- `midibend` - Sets MIDI pitch bend (-1 - 1) +- `miditouch` - Sets MIDI key after touch (0-1) +- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices. +- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127) +- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127) + + +INPUT: + +- `midin` - Opens a MIDI input port to receive MIDI control change messages. + +Additional controls can be mapped using the mapping object passed to `.midi()`: + +## Examples + +### midi(outputName?, options?) + +Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. +If no outputName is given, it uses the first midi output it finds. + +```javascript +$: chord("").voicing().midi('IAC Driver') +``` + +In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` + +### Options + +The `.midi()` function accepts an options object with the following properties: + +```javascript +$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'}) +``` + +
+Available Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers | +| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine | +| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching | +| midichannel | number | 1 | Default MIDI channel (1-16) | +| velocity | number | 0.9 | Default note velocity (0-1) | +| gain | number | 1 | Default gain multiplier for velocity (0-1) | +| midimap | string | 'default' | Name of MIDI mapping to use for control changes | +| midiport | string/number | - | MIDI device name or index | + +
+ + + + +### midiport(outputName) + +Selects the MIDI output device to use, pattern can be used to switch between devices. + +```javascript +$: midiport('IAC Driver') +$: note("c a f e").midiport("<0 1 2 3>").midi() +``` + +### midichan(number) + +Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. + +### control, ccn && ccv + +`control` sends MIDI control change messages to your MIDI device. + +- `ccn` sets the cc number. Depends on your synths midi mapping +- `ccv` sets the cc value. normalized from 0 to 1. + +```javascript +$: note("c a f e").control([74, sine.slow(4)]).midi() +$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi() +``` + +In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. +Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default. +But you can also control cc messages separately like this: + +```javascript +$: note("c a f e").midi() +$: ccv(sine.segment(16).slow(4)).ccn(74).midi() +``` + +### progNum (Program Change) + +`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +Program change values should be numbers between 0 and 127. + +```javascript +// Play notes while changing programs +note("c3 e3 g3").progNum("<0 1 2>").midi() +``` + +Program change messages are useful for switching between different instrument sounds or presets during a performance. +The exact sound that each program number maps to depends on your MIDI device's configuration. + +## sysex, sysexid && sysexdata (System Exclusive Message) + +`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +The value should be an array of numbers between 0-255 representing the SysEx data bytes. + +```javascript +// Send a simple SysEx message +let id = 0x43; //Yamaha +//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers +let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" +$: note("c d e f e d c").sysex(id, data).midi(); +$: note("c d e f e d c").sysexid(id).sysexdata(data).midi(); +``` + +The exact format of SysEx messages depends on your MIDI device's specification. +Consult your device's MIDI implementation guide for details on supported SysEx messages. + +### midibend && miditouch + +`midibend` sets MIDI pitch bend (-1 - 1) +`miditouch` sets MIDI key after touch (0-1) + +```javascript + +$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi(); +$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi(); + +``` + +### midicmd + +`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. + +It supports the following commands: + +- `clock`/`midiClock` - Sends MIDI timing clock messages +- `start` - Sends MIDI start message +- `stop` - Sends MIDI stop message +- `continue` - Sends MIDI continue message + +```javascript +// You can control the clock with a pattern and ensure it starts in sync when the repl begins. +// Note: It might act unexpectedly if MIDI isn't set up initially. +stack( + midicmd("clock*48,/2").midi('IAC Driver') +) +``` + +`midicmd` also supports sending control change, program change and sysex messages. + +- `cc` - sends MIDI control change messages. +- `progNum` - sends MIDI program change messages. +- `sysex` - sends MIDI system exclusive messages. + +```javascript +stack( + // "cc:ccn:ccv" + midicmd("cc:74:1").midi('IAC Driver'), + // "progNum:progNum" + midicmd("progNum:1").midi('IAC Driver'), + // "sysex:[sysexid]:[sysexdata]" + midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver') +) +``` \ No newline at end of file diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 2c1d1f200..ae983d200 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -1,6 +1,6 @@ /* midi.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi'; import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core'; import { noteToMidi, getControlName } from '@strudel/core'; import { Note } from 'webmidi'; + // if you use WebMidi from outside of this package, make sure to import that instance: export const { WebMidi } = _WebMidi; @@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) { resolve(WebMidi); return; } - WebMidi.enable((err) => { - if (err) { - reject(err); - } - onReady?.(WebMidi); - resolve(WebMidi); - }); + WebMidi.enable( + (err) => { + if (err) { + reject(err); + } + onReady?.(WebMidi); + resolve(WebMidi); + }, + { sysex: true }, + ); }); } @@ -174,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) { normalized = Math.min(1, Math.max(0, normalized)); return Math.pow(normalized, exp); } + function mapCC(mapping, value) { return Object.keys(value) .filter((key) => !!mapping[getControlName(key)]) @@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) { device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } -Pattern.prototype.midi = function (output) { - if (isPattern(output)) { +// sends a program change message to the given device on the given channel +function sendProgramChange(progNum, device, midichan, timeOffsetString) { + if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { + throw new Error('expected progNum (program change) to be a number between 0 and 127'); + } + device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); +} + +// sends a sysex message to the given device on the given channel +function sendSysex(sysexid, sysexdata, device, timeOffsetString) { + if (Array.isArray(sysexid)) { + if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('all sysexid bytes must be integers between 0 and 255'); + } + } else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) { + throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); + } + + if (!Array.isArray(sysexdata)) { + throw new Error('expected sysex to be an array of numbers (0-255)'); + } + if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('all sysex bytes must be integers between 0 and 255'); + } + device.sendSysex(sysexid, sysexdata, { time: timeOffsetString }); +} + +// sends a NRPN message to the given device on the given channel +function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) { + if (Array.isArray(nrpnn)) { + if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('all nrpnn bytes must be integers between 0 and 255'); + } + } else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) { + throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); + } + + device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString }); +} + +// sends a pitch bend message to the given device on the given channel +function sendPitchBend(midibend, device, midichan, timeOffsetString) { + if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) { + throw new Error('expected midibend to be a number between -1 and 1'); + } + device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); +} + +// sends a channel aftertouch message to the given device on the given channel +function sendAftertouch(miditouch, device, midichan, timeOffsetString) { + if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) { + throw new Error('expected miditouch to be a number between 0 and 1'); + } + device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); +} + +// sends a note message to the given device on the given channel +function sendNote(note, velocity, duration, device, midichan, timeOffsetString) { + if (note == null || note === '') { + throw new Error('note cannot be null or empty'); + } + if (velocity != null && (typeof velocity !== 'number' || velocity < 0 || velocity > 1)) { + throw new Error('velocity must be a number between 0 and 1'); + } + if (duration != null && (typeof duration !== 'number' || duration < 0)) { + throw new Error('duration must be a positive number'); + } + + const midiNumber = typeof note === 'number' ? note : noteToMidi(note); + const midiNote = new Note(midiNumber, { attack: velocity, duration }); + device.playNote(midiNote, midichan, { + time: timeOffsetString, + }); +} + +/** + * MIDI output: Opens a MIDI output port. + * @param {string | number} midiport MIDI device name or index defaulting to 0 + * @param {object} options Additional MIDI configuration options + * @example + * note("c4").midichan(1).midi('IAC Driver Bus 1') + * @example + * note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 }) + */ + +Pattern.prototype.midi = function (midiport, options = {}) { + if (isPattern(midiport)) { throw new Error( - `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${ + `.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${ WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } + // For backward compatibility + if (typeof midiport === 'object') { + const { port, isController = false, ...configOptions } = midiport; + options = { + isController, + ...configOptions, + ...options, // Keep any options passed separately + }; + midiport = port; + } + + let midiConfig = { + // Default configuration values + isController: false, // Disable sending notes for midi controllers + latencyMs: 34, // Default latency to get audio engine to line up in ms + noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms + midichannel: 1, // Default MIDI channel + velocity: 0.9, // Default velocity + gain: 1, // Default gain + midimap: 'default', // Default MIDI map + midiport: midiport, // Store the port in the config + ...options, // Override defaults with provided options + }; + enableWebMidi({ onEnabled: ({ outputs }) => { - const device = getDevice(output, outputs); + const device = getDevice(midiConfig.midiport, outputs); const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( `Midi enabled! Using "${device.name}". ${ @@ -219,28 +333,37 @@ Pattern.prototype.midi = function (output) { logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), }); - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { if (!WebMidi.enabled) { - console.log('not enabled'); + logger('Midi not enabled'); return; } hap.ensureObjectValue(); + //magic number to get audio engine to line up, can probably be calculated somehow - const latencyMs = 34; + const latencyMs = midiConfig.latencyMs; // passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`; - // destructure value + // midi event values from hap with configurable defaults let { note, + nrpnn, + nrpv, ccn, ccv, - midichan = 1, + midichan = midiConfig.midichannel, midicmd, - gain = 1, - velocity = 0.9, - midimap = 'default', - midiport = output, + midibend, + miditouch, + polyTouch, + gain = midiConfig.gain, + velocity = midiConfig.velocity, + progNum, + sysexid, + sysexdata, + midimap = midiConfig.midimap, + midiport = midiConfig.midiport, } = hap.value; const device = getDevice(midiport, WebMidi.outputs); @@ -252,24 +375,62 @@ Pattern.prototype.midi = function (output) { } velocity = gain * velocity; + + // Handle midimap // if midimap is set, send a cc messages from defined controls if (midicontrolMap.has(midimap)) { const ccs = mapCC(midicontrolMap.get(midimap), hap.value); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString)); + } else if (midimap !== 'default') { + // Add warning when a non-existent midimap is specified + logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`); } - // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length - const duration = (hap.duration.valueOf() / cps) * 1000 - 10; - if (note != null) { - const midiNumber = typeof note === 'number' ? note : noteToMidi(note); - const midiNote = new Note(midiNumber, { attack: velocity, duration }); - device.playNote(midiNote, midichan, { - time: timeOffsetString, - }); + // Handle note + if (note !== undefined && !midiConfig.isController) { + // note off messages will often a few ms arrive late, + // try to prevent glitching by subtracting noteOffsetMs from the duration length + const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs; + + sendNote(note, velocity, duration, device, midichan, timeOffsetString); } + + // Handle program change + if (progNum !== undefined) { + sendProgramChange(progNum, device, midichan, timeOffsetString); + } + + // Handle sysex + // sysex data is consist of 2 arrays, first is sysexid, second is sysexdata + // sysexid is a manufacturer id it is either a number or an array of 3 numbers. + // list of manufacturer ids can be found here : https://midi.org/sysexidtable + // if sysexid is an array the first byte is 0x00 + + if (sysexid !== undefined && sysexdata !== undefined) { + sendSysex(sysexid, sysexdata, device, timeOffsetString); + } + + // Handle control change if (ccv !== undefined && ccn !== undefined) { sendCC(ccn, ccv, device, midichan, timeOffsetString); } + + // Handle NRPN non-registered parameter number + if (nrpnn !== undefined && nrpv !== undefined) { + sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString); + } + + // Handle midibend + if (midibend !== undefined) { + sendPitchBend(midibend, device, midichan, timeOffsetString); + } + + // Handle miditouch + if (miditouch !== undefined) { + sendAftertouch(miditouch, device, midichan, timeOffsetString); + } + + // Handle midicmd if (hap.whole.begin + 0 === 0) { // we need to start here because we have the timing info device.sendStart({ time: timeOffsetString }); @@ -282,6 +443,19 @@ Pattern.prototype.midi = function (output) { device.sendStop({ time: timeOffsetString }); } else if (['continue'].includes(midicmd)) { device.sendContinue({ time: timeOffsetString }); + } else if (Array.isArray(midicmd)) { + if (midicmd[0] === 'progNum') { + sendProgramChange(midicmd[1], device, midichan, timeOffsetString); + } else if (midicmd[0] === 'cc') { + if (midicmd.length === 2) { + sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString); + } + } else if (midicmd[0] === 'sysex') { + if (midicmd.length === 3) { + const [_, id, data] = midicmd; + sendSysex(id, data, device, timeOffsetString); + } + } } }); }; @@ -289,6 +463,14 @@ Pattern.prototype.midi = function (output) { let listeners = {}; const refs = {}; +/** + * MIDI input: Opens a MIDI input port to receive MIDI control change messages. + * @param {string | number} input MIDI device name or index defaulting to 0 + * @returns {Function} + * @example + * let cc = await midin('IAC Driver Bus 1') + * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") + */ export async function midin(input) { if (isPattern(input)) { throw new Error( @@ -311,6 +493,9 @@ export async function midin(input) { otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' }`, ); + } + // ensure refs for this input are initialized + if (!refs[input]) { refs[input] = {}; } const cc = (cc) => ref(() => refs[input][cc] || 0); diff --git a/packages/midi/package.json b/packages/midi/package.json index c7c5f4988..2342cf7e9 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.1.0", + "version": "1.2.5", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/mini/bench/mini.bench.mjs b/packages/mini/bench/mini.bench.mjs index 782ac86ba..e7471bf22 100644 --- a/packages/mini/bench/mini.bench.mjs +++ b/packages/mini/bench/mini.bench.mjs @@ -1,10 +1,10 @@ import { describe, bench } from 'vitest'; -import { calculateTactus } from '../../core/index.mjs'; +import { calculateSteps } from '../../core/index.mjs'; import { mini } from '../index.mjs'; describe('mini', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -13,7 +13,7 @@ describe('mini', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -21,5 +21,5 @@ describe('mini', () => { }, { time: 1000 }, ); - calculateTactus(true); + calculateSteps(true); }); diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index a593b4167..8af82caee 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -1,6 +1,6 @@ /* krill.pegjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 6277daa91..d138d9fa0 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -1,6 +1,6 @@ /* mini.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/mini/package.json b/packages/mini/package.json index d05d60364..6eeaab0da 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.1.0", + "version": "1.2.4", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 15d501111..c38997308 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -1,6 +1,6 @@ /* mini.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/mondo/README.md b/packages/mondo/README.md new file mode 100644 index 000000000..954f425a9 --- /dev/null +++ b/packages/mondo/README.md @@ -0,0 +1,39 @@ +# mondo + +a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet. +see the `test` folder for usage examples + +more info: + +- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) +- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) + +## Example Usage + +```js +import { MondoRunner } from 'mondolang' +// define our library of functions and variables +let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, +}; +// this function will evaluate nodes in the syntax tree +function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function`); + } + return fn(...args); +} +const runner = new MondoRunner({ evaluator }); +const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 +``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs new file mode 100644 index 000000000..b879f24bf --- /dev/null +++ b/packages/mondo/mondo.mjs @@ -0,0 +1,484 @@ +/* +mondo.mjs - +Copyright (C) 2022 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 . +*/ + +// evolved from https://garten.salat.dev/lisp/parser.html +export class MondoParser { + // these are the tokens we expect + token_types = { + comment: /^\/\/(.*?)(?=\n|$)/, + quotes_double: /^"(.*?)"/, + quotes_single: /^'(.*?)'/, + open_list: /^\(/, + close_list: /^\)/, + open_angle: /^/, + open_square: /^\[/, + close_square: /^\]/, + open_curly: /^\{/, + close_curly: /^\}/, + number: /^-?[0-9]*\.?[0-9]+/, // before pipe! + // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. + // dollar: /^\$/, + pipe: /^#/, + stack: /^[,$]/, + or: /^[|]/, + plain: /^[a-zA-Z0-9-~_^#]+/, + }; + // matches next token + next_token(code, offset = 0) { + for (let type in this.token_types) { + const match = code.match(this.token_types[type]); + if (match) { + let token = { type, value: match[0] }; + if (offset !== -1) { + // add location + token.loc = [offset, offset + match[0].length]; + } + return token; + } + } + throw new Error(`mondo: could not match '${code}'`); + } + // takes code string, returns list of matched tokens (if valid) + tokenize(code, offset = 0) { + let tokens = []; + let locEnabled = offset !== -1; + let trim = () => { + // trim whitespace at start, update offset + offset += code.length - code.trimStart().length; + // trim start and end to not confuse parser + return code.trim(); + }; + code = trim(); + while (code.length > 0) { + code = trim(); + const token = this.next_token(code, locEnabled ? offset : -1); + code = code.slice(token.value.length); + offset += token.value.length; + tokens.push(token); + } + return tokens; + } + // take code, return abstract syntax tree + parse(code, offset) { + this.code = code; + this.offset = offset; + this.tokens = this.tokenize(code, offset); + const expressions = []; + while (this.tokens.length) { + expressions.push(this.parse_expr()); + } + if (expressions.length === 0) { + // empty case + return { type: 'list', children: [] }; + } + // do we have multiple top level expressions or a single non list? + if (expressions.length > 1 || expressions[0].type !== 'list') { + return { + type: 'list', + children: this.desugar(expressions), + }; + } + // we have a single list + return expressions[0]; + } + // parses any valid expression + parse_expr() { + if (!this.tokens[0]) { + throw new Error(`unexpected end of file`); + // TODO: could we allow that? like (((((((( s bd + // return { type: 'list', children: [] }; + } + let next = this.tokens[0]?.type; + if (next === 'open_list') { + return this.parse_list(); + } + if (next === 'open_angle') { + return this.parse_angle(); + } + if (next === 'open_square') { + return this.parse_square(); + } + if (next === 'open_curly') { + return this.parse_curly(); + } + return this.consume(next); + } + // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] + split_children(children, split_type) { + const chunks = []; + while (true) { + let splitIndex = children.findIndex((child) => child.type === split_type); + if (splitIndex === -1) break; + const chunk = children.slice(0, splitIndex); + chunks.push(chunk); + children = children.slice(splitIndex + 1); + } + chunks.push(children); + return chunks; + } + desugar_split(children, split_type, next) { + const chunks = this.split_children(children, split_type); + if (chunks.length === 1) { + return next(children); + } + // collect args of stack function + const args = chunks + .map((chunk) => { + if (!chunk.length) { + return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty) + } + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } + // chunks of multiple args + chunk = next(chunk); + return { type: 'list', children: chunk }; + }) + .filter(Boolean); // ignore empty chunks + return [{ type: 'plain', value: split_type }, ...args]; + } + // prevents to get a list, e.g. ((x y)) => (x y) + unwrap_children(children) { + if (children.length === 1) { + return children[0].children; + } + return children; + } + desugar_ops(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'op'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + //throw new Error(`cannot use operator as last child.`); + children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]" + continue; + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + // convert infix to prefix notation + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + if (left.type === 'pipe') { + // "x !* 2" => (* 2 x) + children[opIndex] = op; + continue; + } + // some careful error handling + if (left.type === 'op') { + throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`); + } + if (right.type === 'op') { + let err = `got 2 ops in a row: "${op.value}${right.value}"`; + if (op.value === '-') { + // yes i know this file is not supposed to know about rests x.X + err += '. you probably want a rest, which is "_" in mondo!'; + } + throw new Error(err); + } + const call = { type: 'list', children: [op, right, left] }; + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + children = this.unwrap_children(children); + } + return children; + } + get_lambda(args, children) { + // (.fast 2) = (fn (_) (fast _ 2)) + children = this.desugar(children); + const body = children.length === 1 ? children[0] : { type: 'list', children }; + return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body]; + } + // returns location range of given ast (even if desugared) + get_range(ast, range = [Infinity, 0]) { + let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])]; + if (ast.loc) { + return union(range, ast.loc); + } + if (ast.type !== 'list') { + return range; + } + return ast.children.reduce((range, child) => { + const childrange = this.get_range(child, range); + return union(range, childrange); + }, range); + } + errorhead(ast) { + return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`; + } + // returns original user code where the given ast originates (even if desugared) + get_code_snippet(ast) { + const [min, max] = this.get_range(ast); + return this.code.slice(min - this.offset, max - this.offset); + } + desugar_pipes(children) { + let chunks = this.split_children(children, 'pipe'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + + if (!left.length) { + const arg = { type: 'plain', value: '_' }; + return this.get_lambda([arg], [arg, ...children]); + } + // s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; + } + // return next(chunks[0]); + return chunks[0]; + } + parse_pair(open_type, close_type) { + const begin = this.tokens[0].loc?.[0]; + this.consume(open_type); + const children = []; + while (this.tokens[0]?.type !== close_type) { + children.push(this.parse_expr()); + } + const end = this.tokens[0].loc?.[1]; + this.consume(close_type); + const node = { type: 'list', children }; + if (begin !== undefined) { + node.loc = [begin, end]; + node.raw = this.code.slice(begin, end); + } + return node; + } + desugar(children, type) { + // if type is given, the first element is expected to contain it as plain value + // e.g. with (square a b, c), we want to split (a b, c) and ignore "square" + children = type ? children.slice(1) : children; + children = this.desugar_split(children, 'stack', (children) => + this.desugar_split(children, 'or', (children) => { + // chunks of multiple args + if (type) { + // the type we've removed before splitting needs to be added back + children = [{ type: 'plain', value: type }, ...children]; + } + children = this.desugar_ops(children); + // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + children = this.desugar_pipes(children); + return children; + }), + ); + return children; + } + parse_list() { + let node = this.parse_pair('open_list', 'close_list'); + node.children = this.desugar(node.children); + return node; + } + parse_angle() { + let node = this.parse_pair('open_angle', 'close_angle'); + node.children.unshift({ type: 'plain', value: 'angle' }); + node.children = this.desugar(node.children, 'angle'); + return node; + } + parse_square() { + let node = this.parse_pair('open_square', 'close_square'); + node.children.unshift({ type: 'plain', value: 'square' }); + node.children = this.desugar(node.children, 'square'); + return node; + } + parse_curly() { + let node = this.parse_pair('open_curly', 'close_curly'); + node.children.unshift({ type: 'plain', value: 'curly' }); + node.children = this.desugar(node.children, 'curly'); + return node; + } + consume(type) { + // shift removes first element and returns it + const token = this.tokens.shift(); + if (token.type !== type) { + throw new Error(`expected token type ${type}, got ${token.type}`); + } + return token; + } + get_locations(code, offset = 0) { + let walk = (ast, locations = []) => { + if (ast.type === 'list') { + return ast.children.forEach((child) => walk(child, locations)); + } + if (ast.loc) { + locations.push(ast.loc); + } + }; + const ast = this.parse(code, offset); + let locations = []; + walk(ast, locations); + return locations; + } +} + +export function printAst(ast, compact = false, lvl = 0) { + const br = compact ? '' : '\n'; + const spaces = compact ? '' : Array(lvl).fill(' ').join(''); + if (ast.type === 'list') { + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; + } + return `${ast.value}`; +} + +// lisp runner +export class MondoRunner { + constructor({ evaluator } = {}) { + this.parser = new MondoParser(); + this.evaluator = evaluator; + this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); + } + // a helper to check conditions and throw if they are not met + assert(condition, error) { + if (!condition) { + throw new Error(error); + } + } + run(code, scope, offset = 0) { + const ast = this.parser.parse(code, offset); + //console.log(printAst(ast)); + return this.evaluate(ast, scope); + } + evaluate_let(ast, scope) { + // (let ((x 3) (y 4)) ...body) + // = ((fn (x y) ...body) 3 4) + const defs = ast.children[1].children; + const args = defs.map((pair) => pair.children[0]); + const vals = defs.map((pair) => pair.children[1]); + const body = ast.children.slice(2); + const lambda = { + type: 'list', + children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body], + }; + return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope); + } + evaluate_def(ast, scope) { + // function definition special form? + if (ast.children[1].type === 'list') { + // (def (add a b) (+ a b)) + // => (def add (fn (a b) (+ a b)) ) + const args = ast.children[1].children.slice(1); + const lambda = { + // lambda + type: 'list', + children: [ + { type: 'plain', value: 'fn' }, + { type: 'list', children: args }, + ...ast.children.slice(2), // body + ], + }; + // we mutate to make sure the old ast wont make a mess later + ast.children[1] = ast.children[1].children[0]; + ast.children[2] = lambda; + ast.children = ast.children.slice(0, 3); // throw away rest + } + // (def name body) + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); + } + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + // def with fall through + } + evaluate_match(ast, scope) { + // (match (p1 e1) (p2 e2) ... (pn en)) + // = cond in lisp + if (ast.children.length < 2) { + return; + } + const [_, ...body] = ast.children; + for (let i = 0; i < body.length; ++i) { + const [predicate, exp] = body[i].children; + if (predicate.value === 'else') { + return this.evaluate(exp, scope); + } + const outcome = this.evaluate(predicate, scope); + if (outcome) { + return this.evaluate(exp, scope); + } + } + return undefined; // nothing was matched + } + evaluate_if(ast, scope) { + // if is a special case of match + if (ast.children.length !== 4) { + return; + } + // (if predicate consequent alternative) + const [_, predicate, consequent, alternative] = ast.children; + // (match (predicate consequent) (else alternative)) + const matcher = { + type: 'list', + children: [ + { type: 'plain', value: 'match' }, + { type: 'list', children: [predicate, consequent] }, + { type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] }, + ], + }; + return this.evaluate_match(matcher, scope); + } + evaluate_lambda(ast, scope) { + // (fn (_) (ply 2 _) + // ^args ^ body + const [_, formalArgs, ...body] = ast.children; + return (...args) => { + const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); + const closure = { + ...scope, + ...params, + }; + // body can have multiple expressions + const res = body.map((exp) => this.evaluate(exp, closure)); + // last expression is the return value + return res[res.length - 1]; + }; + } + evaluate_list(ast, scope) { + // evaluate all children before evaluating list (dont mutate!!!) + const args = ast.children + .filter((child) => child.type !== 'comment') // ignore comments + .map((arg) => this.evaluate(arg, scope)); + const node = { type: 'list', children: args }; + return this.evaluator(node, scope); + } + evaluate_leaf(ast, scope) { + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + ast.value = ast.value.slice(1, -1); + ast.type = 'string'; + } + return this.evaluator(ast, scope); + } + evaluate(ast, scope = {}) { + if (ast.type !== 'list') { + return this.evaluate_leaf(ast, scope); + } + const name = ast.children[0]?.value; + if (name === 'fn') { + return this.evaluate_lambda(ast, scope); + } + if (name === 'match') { + return this.evaluate_match(ast, scope); + } + if (name === 'if') { + return this.evaluate_if(ast, scope); + } + if (name === 'let') { + return this.evaluate_let(ast, scope); + } + if (name === 'def') { + this.evaluate_def(ast, scope); + } + return this.evaluate_list(ast, scope); + } +} diff --git a/packages/mondo/package.json b/packages/mondo/package.json new file mode 100644 index 000000000..a59bfc344 --- /dev/null +++ b/packages/mondo/package.json @@ -0,0 +1,37 @@ +{ + "name": "mondolang", + "version": "1.1.1", + "description": "a language for functional composition that translates to js", + "main": "mondo.mjs", + "type": "module", + "publishConfig": { + "main": "dist/mondo.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "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 ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md", + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs new file mode 100644 index 000000000..6393b10f9 --- /dev/null +++ b/packages/mondo/test/mondo.test.mjs @@ -0,0 +1,978 @@ +/* +mondo.test.mjs - +Copyright (C) 2022 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 . +*/ + +import { describe, expect, it } from 'vitest'; +import { MondoParser, printAst, MondoRunner } from '../mondo.mjs'; + +const parser = new MondoParser(); +const p = (code) => parser.parse(code, -1); + +describe('mondo tokenizer', () => { + const parser = new MondoParser(); + it('should tokenize with locations', () => + expect( + parser + .tokenize('(one two three)') + .map((t) => t.value + '=' + t.loc.join('-')) + .join(' '), + ).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15')); + // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get loangleions', () => + expect(parser.get_locations('s bd rim')).toEqual([ + [0, 1], + [2, 4], + [5, 8], + ])); +}); +describe('mondo s-expressions parser', () => { + it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); + it('should parse a single item', () => + expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] })); + it('should parse a list with 1 item', () => + expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse a list with 2 items', () => + expect(p('(a b)')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'plain', value: 'b' }, + ], + })); + it('should parse a list with 2 items', () => + expect(p('(a (b c))')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { + type: 'list', + children: [ + { type: 'plain', value: 'b' }, + { type: 'plain', value: 'c' }, + ], + }, + ], + })); + it('should parse numbers', () => + expect(p('(1 .2 1.2 10 22.3)')).toEqual({ + type: 'list', + children: [ + { type: 'number', value: '1' }, + { type: 'number', value: '.2' }, + { type: 'number', value: '1.2' }, + { type: 'number', value: '10' }, + { type: 'number', value: '22.3' }, + ], + })); + it('should parse comments', () => + expect(p('a // hello')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'comment', value: '// hello' }, + ], + })); +}); + +let desguar = (a) => { + return printAst(parser.parse(a), true); +}; + +describe('mondo sugar', () => { + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)')); + it('should desugar <>', () => expect(desguar('')).toEqual('(angle a b c)')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(angle a (angle b c) d)')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); + + it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # within , within []', () => + expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); + + // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + + it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); + it('should desugar , | of []', () => + expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))')); + it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); + it('should desugar , square 3', () => + expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))')); + it('should desugar , angle', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , angle 2', () => expect(desguar('')).toEqual('(stack bd (angle hh oh))')); + it('should desugar , angle 3', () => + expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); + it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar []*', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); + /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ + + it('should desugar README example', () => + expect(desguar('s [bd hh*2 (cp # crush 4) ] # speed .8')).toEqual( + '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', + )); + + it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); + it('should desugar lambda with pipe', () => + expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); + /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); + const target = { type: 'plain', value: 'xyz' }; + it('should desugar_lambda', () => + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ +}); + +describe('mondo arithmetic', () => { + let multi = + (op) => + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); + + let lib = { + '+': multi((a, b) => a + b), + add: multi((a, b) => a + b), + '-': multi((a, b) => a - b), + sub: multi((a, b) => a - b), + '*': multi((a, b) => a * b), + '/': multi((a, b) => a / b), + mod: multi((a, b) => a % b), + eq: (a, b) => a === b, + lt: (a, b) => a < b, + gt: (a, b) => a > b, + and: (a, b) => a && b, + or: (a, b) => a || b, + not: (a) => !a, + run: (...args) => args[args.length - 1], + def: () => 0, + sin: Math.sin, + cos: Math.cos, + PI: Math.PI, + cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])], + car: (pair) => pair[0], + cdr: (pair) => pair.slice(1), + list: (...items) => items, + nil: [], + isnull: (items) => items.length === 0, + concat: (...msgs) => msgs.join(''), + error: (...msgs) => { + throw new Error(msgs.join(' ')); + }, + }; + function evaluator(node, scope) { + if (node.type !== 'list') { + // is leaf + return scope[node.value] ?? lib[node.value] ?? node.value; + } + // is list + const [fn, ...args] = node.children; + if (typeof fn !== 'function') { + throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`); + } + return fn(...args); + } + const runner = new MondoRunner({ evaluator }); + let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope); + let pretty = (exp) => printAst(runner.parser.parse(exp), false); + //it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + + it('eval number', () => expect(evaluate('2')).toEqual(2)); + it('eval string', () => expect(evaluate('abc')).toEqual('abc')); + it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3)); + it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6)); + it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2)); + it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4)); + it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9)); + + // sicp + it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486)); + it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666)); + it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495)); + it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2)); + it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7)); + it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75)); + it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200)); + it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19)); + it('sicp 9.4', () => + expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+ + (* 3 + (+ + (* 2 4) + (+ 3 5) + ) + ) + (+ + (- 10 7) 6 + ) +)`)); // this is not exactly pretty printing by convention.. + + let scope = {}; + it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10)); + it('sicp 11.2', () => + expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159)); + it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); + it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); + it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); + it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); + // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); + it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); + it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); + it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); + it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + it('sicp 22.1', () => + expect( + evaluate( + `(def (abs x) + (match + ((gt x 0) x) + ((eq x 0) 0) + ((lt x 0) (- 0 x)) + ))`, // sicp was doing (- x), which doesnt work with our - + scope, + ), + ).toEqual(0)); + + it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false)); + it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true)); + it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true)); + it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false)); + + it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3)); + it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); + it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); + + it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0)); + it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); + it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); + it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); + + it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0)); + it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0)); + it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19)); + it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4)); + it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16)); + it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6)); + it('sicp ex1.1.7', () => + expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); + + // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... + it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); + it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); + + // 1.1.7 Example: Square Roots by Newton’s Method + it('sicp 30.1', () => + expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0)); + it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); + it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); + it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0)); + it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); + it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0)); + it('sicp 31.1', () => + expect( + evaluate( + `(def (sqrtiter guess x) (if (goodenuf guess x) + guess + (sqrtiter (improve guess x) x)))`, + scope, + ), + ).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); + it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); + it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + // eslint-disable-next-line no-loss-of-precision + it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); + it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); + + // lexical scoping + it('sicp 39.1', () => + expect( + evaluate( + ` +(def (sqrt x) + (def (goodenough guess) + (lt (abs (- (square guess) x)) 0.001)) +(def (improve guess) + (average guess (/ x guess))) +(def (sqrt-iter guess) + (if (goodenough guess) guess (sqrt-iter (improve guess)))) +(sqrtiter 1.0)) + + `, + scope, + ), + ).toEqual(0)); + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + it('sicp 41.3', () => + expect( + evaluate( + ` +(def (factorial n) (factiter 1 1 n)) +(def (factiter product counter maxcount) + (if (gt counter maxcount) + product + (factiter (* counter product) + (+ counter 1) + maxcount))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // 46.1 + /* (def (+ a b) +(if (= a 0) b (inc (+ (dec a) b)))) +(def (+ a b) +(if (= a 0) b (+ (dec a) (inc b)))) */ + + // Exercise 1.10 + // Ackermann’s function + it('sicp 47.1', () => + expect( + evaluate( + ` +(def (A x y) (match ((eq y 0) 0) +((eq x 0) (* 2 y)) +((eq y 1) 2) +(else (A (- x 1) (A x (- y 1)))))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024)); + it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536)); + it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536)); + it('sicp 47.5', () => + expect( + evaluate( + ` +(def (f n) (A 0 n)) +(def (g n) (A 1 n))) +(def (h n) (A 2 n)) +(def (k n) (* 5 n n)) + `, + scope, + ), + ).toEqual(0)); + + // Tree Recursion + // recursive process + it('sicp 48.1', () => + expect( + evaluate( + ` +(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1) +(else (+ (fib (- n 1)) (fib (- n 2)))))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // iterative process + it('sicp 48.2', () => + expect( + evaluate( + ` +(def (fib n) (fibiter 1 0 n)) +(def (fibiter a b count) (if (eq count 0) + b + (fibiter (+ a b) a (- count 1)))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // example: counting change + it('sicp 52.2', () => + expect( + evaluate( + ` +(def (countchange amount) (cc amount 5)) +(def (cc amount kindsofcoins) + (match + ((eq amount 0) 1) + ((or (lt amount 0) (eq kindsofcoins 0)) 0) + (else (+ + (cc amount (- kindsofcoins 1)) + (cc (- amount (firstdenomination kindsofcoins)) kindsofcoins))))) + +(def (firstdenomination kindsofcoins) +(match + ((eq kindsofcoins 1) 1) + ((eq kindsofcoins 2) 5) + ((eq kindsofcoins 3) 10) + ((eq kindsofcoins 4) 25) + ((eq kindsofcoins 5) 50))) + +(countchange 100) + `, + scope, + ), + ).toEqual(292)); + + // todo: pascals triangle + it('sicp 57.1', () => + expect( + evaluate( + ` +(def (cube x) (* x x x)) +(def (p x) (sub (* 3 x) (* 4 (cube x)))) +(def (sine angle) +(if (not (gt (abs angle) 0.1)) angle +(p (sine (/ angle 3.0))))) + +(sine 12.15) + `, + scope, + ), + ).toEqual(-0.39980345741334)); + + // exponentiation recursive + it('sicp 57.2', () => + expect( + evaluate( + ` +(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1))))) +(expt 2 4) +`, + scope, + ), + ).toEqual(16)); + + // exponentiation iterative + it('sicp 58.1b', () => + expect( + evaluate( + ` +(def (expt b n) (exptiter b n 1)) +(def (exptiter b counter product) (if (eq counter 0) + product + (exptiter b (- counter 1) (* b product)))) +(expt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // exponentiation fast + it('sicp 58.2', () => + expect( + evaluate( + ` +(def (fastexpt b n) (match ((eq n 0) 1) +((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1)))))) +(def (iseven n) +(eq (mod n 2) 0)) +(fastexpt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // * = repeated addition + it('sicp 60.1', () => + expect( + evaluate( + `(def (mult a b) (if (eq b 0) + 0 + (+ a (* a (- b 1))))) + (mult 3 15) + `, + ), + ).toEqual(45)); + + // gcd / euclid + it('sicp 63.1', () => + expect( + evaluate( + `(def (gcd a b) (if (eq b 0) + a + (gcd b (mod a b)))) + (gcd 20 6) + `, + scope, + ), + ).toEqual(2)); + + // 65 smallest divisor + // 67 fermat test + // .... + + // higher order procedures + + it('sicp 77.1', () => + expect( + evaluate( + ` +(def (sum term a next b) +(if (gt a b) 0 (+ (term a) +(sum term (next a) next b)))) +`, + scope, + ), + ).toEqual(0)); + + it('sicp 78.1', () => + expect( + evaluate( + ` +(def (inc n) (+ n 1)) +(def (cube a) (* a a a)) +(def (sumcubes a b) +(sum cube a inc b)) +(sumcubes 1 10) + `, + scope, + ), + ).toEqual(3025)); + + it('sicp 78.2', () => + expect( + evaluate( + ` + (def (identity x) x) + (def (sumintegers a b) + (sum identity a inc b)) + (sumintegers 1 10) + `, + scope, + ), + ).toEqual(55)); + + // pisum + it('sicp 79.1', () => + expect( + evaluate( + ` +(def (pisum a b) + (def (piterm x) + (/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) +(sum piterm a pinext b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); + + // integral + it('sicp 79.2', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (def (adddx x) (+ x dx)) +(* (sum f (+ a (/ dx 2.0)) adddx b) dx)) +(integral cube 0 1 0.01) + `, + scope, + ), + ).toEqual(0.24998750000000042)); + // maximum callstack... + //it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001)); + + //lambdas + it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9)); + + it('sicp 83.2', () => + expect( + evaluate( + ` +(def (pisum a b) + (sum (fn (x) (/ 1.0 (* x (+ x 2)))) + a + (fn (x) (+ x 4)) + b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); + it('sicp 83.3', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (* (sum f + (+ a (/ dx 2.0)) + (fn (x) (+ x dx)) + b) + dx)) +(integral cube 0 1 0.01) +`, + scope, + ), + ).toEqual(0.24998750000000042)); + it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); + + // let expressions + it('sicp 87.1', () => + expect( + evaluate( + ` +(+ (let ((x 3)) +(+ x (* x 10))) x) +`, + { x: 5 }, + ), + ).toEqual(38)); + it('sicp 87.2', () => + expect( + evaluate( + ` +(let ((x 3) +(y (+ x 2))) +(* x y)) + `, + { x: 2 }, + ), + ).toEqual(12)); + it('sicp 88.1', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f square) + `, + scope, + ), + ).toEqual(4)); + it('sicp 88.2', () => + expect( + evaluate( + ` + (def (f g) (g 2)) + (f (fn (z) (* z (+ z 1)))) + `, + scope, + ), + ).toEqual(6)); + + // Finding roots of equations by the half-interval method + it('sicp 89.1', () => + expect( + evaluate( + ` +(def (search f negpoint pospoint) + (let ((midpoint (average negpoint pospoint))) + (if (closeenough negpoint pospoint) + midpoint + (let ((testvalue (f midpoint))) + (match ((positive testvalue) + (search f negpoint midpoint)) + ((negative testvalue) + (search f midpoint pospoint)) + (else midpoint)))))) + +(def (closeenough x y) (lt (abs (- x y)) 0.001)) + +(def (negative x) (lt x 0)) +(def (positive x) (gt x 0)) + +(def (halfintervalmethod f a b) (let ((avalue (f a)) +(bvalue (f b))) +(match ((and (negative avalue) (positive bvalue)) +(search f a b)) +((and (negative bvalue) (positive avalue)) +(search f b a)) (else +(error "Values are not of opposite sign" a b))))) + +(halfintervalmethod sin 2.0 4.0) +`, + scope, + ), + ).toEqual(3.14111328125)); + + it('sicp 89.1', () => + expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); + + // Finding fixed points of functions + it('sicp 92.1', () => + expect( + evaluate( + ` +(def tolerance 0.00001) +(def (fixedpoint f first-guess) + (def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance)) + (def (try guess) + (let ((next (f guess))) + (if (closeenough guess next) next (try next)))) + (try first-guess)) + +(fixedpoint cos 1.0) +`, + scope, + ), + ).toEqual(0.7390822985224023)); + it('sicp 93.1', () => + expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173)); + // Maximum call stack size exceeded (expected) + /* it('sicp 93.2', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */ + it('sicp 93.3', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + // Procedures as Returned Values + it('sicp 97.1', () => + expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55)); + it('sicp 98.1', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + it('sicp 98.2', () => + expect( + evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope), + ).toEqual(1.912934258514886)); + it('sicp 99.1', () => + expect( + evaluate( + ` + (def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx))) + (def dx 0.00001) + (def (cube x) (* x x x)) + ((deriv cube) 5) + `, + scope, + ), + ).toEqual(75.00014999664018)); + // With the aid of deriv, we can express Newton’s method as a fixed-point process: + it('sicp 100.1', () => + expect( + evaluate( + ` +(def (newtontransform g) +(fn (x) (- x (/ (g x) ((deriv g) x))))) +(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess)) +(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + // whatever this is + it('sicp 101.1', () => + expect( + evaluate( + ` + (def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess)) + (def (sqrt x) (fixedpointoftransform + (fn (y) (/ x y)) averagedamp 1.0)) + (sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + it('sicp 101.2', () => + expect( + evaluate( + ` +(def (sqrt x) (fixedpointoftransform +(fn (y) (- (square y) x)) newtontransform 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + + // data abstraction + + // rational arithmetic + it('sicp 114.1', () => + expect( + evaluate( + ` +(def (addrat x y) + (makerat (+ (* + (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (subrat x y) + (makerat (- (* (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (mulrat x y) + (makerat (* (numer x) (numer y)) + (* (denom x) (denom y)))) +(def (divrat x y) + (makerat (* (numer x) (denom y)) + (* (denom x) (numer y)))) +(def (equalrat x y) + (eq (* (numer x) (denom y)) + (* (numer y) (denom x)))) + `, + scope, + ), + ).toEqual(0)); + + // markerat number denom + it('sicp 117.1', () => + expect( + evaluate( + ` +(def (makerat n d) (cons n d)) +(def (numer x) (car x)) +(def (denom x) (cdr x)) +(def (printrat x) (concat (numer x) ':' (denom x))) + `, + scope, + ), + ).toEqual(0)); + + it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2')); + it('sicp 117.2', () => + expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6')); + it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6')); + it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9')); + it('sicp 118.1', () => + expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0)); + it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3')); + + let lscope = {}; + // pairs with lambda + it('sicp 124.1', () => + expect( + evaluate( + ` +(def (cons x y) + (def (dispatch m) + (match + ((eq m 0) x) + ((eq m 1) y) + (else (error "argument not 0 or 1: CONS" m))) + ) dispatch) + (def (car z) (z 0)) + (def (cdr z) (z 1)) + `, + lscope, + ), + ).toEqual(0)); + it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); + it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); + // lists + it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4])); + it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1)); + it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4])); + it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2)); + it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4])); + // listref + it('sicp 138.1', () => + expect( + evaluate( + ` +(def (listref items n) (if (eq n 0) (car items) + (listref (cdr items) (- n 1)))) +(def squares (list 1 4 9 16 25)) +(listref squares 3)`, + scope, + ), + ).toEqual(16)); + // length recursive + it('sicp 138.2', () => + expect( + evaluate( + ` + (def (length items) + (if (isnull items) 0 + (+ 1 (length (cdr items))))) + (def odds (list 1 3 5 7)) + (length odds)`, + ), + ).toEqual(4)); + // length iterative + it('sicp 139.1', () => + expect( + evaluate( + ` + (def (length items) + (def (lengthiter a count) + (if (isnull a) count + (lengthiter (cdr a) (+ 1 count)))) + (lengthiter items 0)) + (def odds (list 1 3 5 7)) + (length odds) + `, + scope, + ), + ).toEqual(4)); + // append + it('sicp 139.1', () => + expect( + evaluate( + ` +(def (append list1 list2) +(if (isnull list1) + list2 + (cons (car list1) (append (cdr list1) list2)))) + (append squares odds) + `, + scope, + ), + ).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7])); + // (define (f x y . z) ⟨body⟩) <- tbd: variable argument count + // Mapping over lists + + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (if (isnull items) nil + (cons (* (car items) factor) + (scalelist (cdr items) factor)))) +(scalelist (list 1 2 3 4 5) 10) + `, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); + + it('sicp 143.1', () => + expect( + evaluate( + ` + (def (map proc items) (if (isnull items) nil + (cons (proc (car items)) + (map proc (cdr items))))) + (map abs (list -10 2.5 -11.6 17)) + `, + scope, + ), + ).toEqual([10, 2.5, 11.6, 17])); + it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16])); + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (map (fn (x) (* x factor)) items)) +(scalelist (list 1 2 3 4 5) 10) +`, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); +}); diff --git a/packages/mondo/vite.config.js b/packages/mondo/vite.config.js new file mode 100644 index 000000000..19e53d7f3 --- /dev/null +++ b/packages/mondo/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'mondo.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'mondo.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/mondough/README.md b/packages/mondough/README.md new file mode 100644 index 000000000..3ae758912 --- /dev/null +++ b/packages/mondough/README.md @@ -0,0 +1,3 @@ +# @strudel/mondough + +connects mondo to strudel. diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs new file mode 100644 index 000000000..b7ee83787 --- /dev/null +++ b/packages/mondough/mondough.mjs @@ -0,0 +1,132 @@ +import { + strudelScope, + reify, + fast, + slow, + seq, + stepcat, + extend, + expand, + pace, + chooseIn, + degradeBy, + silence, +} from '@strudel/core'; +import { registerLanguage } from '@strudel/transpiler'; +import { MondoRunner } from 'mondolang'; + +const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); + +const arrayRange = (start, stop, step = 1) => + Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => + start < stop ? start + index * step : start - index * step, + ); +const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); + +let nope = (...args) => args[args.length - 1]; + +let lib = {}; +lib['nope'] = nope; +lib['-'] = (a, b) => b.early(a); +lib['+'] = (a, b) => b.late(a); +lib['_'] = silence; +lib['~'] = silence; +lib.curly = stepcat; +lib.square = (...args) => stepcat(...args).setSteps(1); +lib.angle = (...args) => stepcat(...args).pace(1); +lib['*'] = fast; +lib['/'] = slow; +lib['!'] = extend; +lib['@'] = expand; +lib['%'] = pace; +lib['?'] = degradeBy; // todo: default 0.5 not working.. +lib[':'] = tail; +lib['..'] = range; +lib['def'] = () => silence; +lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" +//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct + +function evaluator(node, scope) { + const { type } = node; + // node is list + if (type === 'list') { + const { children } = node; + const [name, ...args] = children; + // some functions wont be reified to make sure they work (e.g. see extend below) + if (typeof name === 'function') { + return name(...args); + } + if (name.value === 'def') { + return silence; + } + // name is expected to be a pattern of functions! + const first = name.firstCycle(true)[0]; + const type = typeof first?.value; + if (type !== 'function') { + throw new Error(`[mondough] expected function, got "${first?.value}"`); + } + return name + .fmap((fn) => { + if (typeof fn !== 'function') { + throw new Error(`[mondough] "${fn}" is not a function b`); + } + return fn(...args); + }) + .innerJoin(); + } + // node is leaf + let { value } = node; + if (type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const variable = lib[value] ?? strudelScope[value]; + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable + let pat; + if (type === 'plain' && typeof variable !== 'undefined') { + // some function names are not patternable, so we skip reification here + if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) { + return variable; + } + pat = reify(variable); + } else { + pat = reify(value); + } + if (node.loc) { + pat = pat.withLoc(node.loc[0], node.loc[1]); + } + return pat; +} + +let runner = new MondoRunner({ evaluator }); + +export function mondo(code, offset = 0) { + if (Array.isArray(code)) { + code = code.join(''); + } + const pat = runner.run(code, undefined, offset); + return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); +} + +export let getLocations = (code, offset) => runner.parser.get_locations(code, offset); + +export const mondi = (str, offset) => { + const code = `[${str}]`; + return mondo(code, offset); +}; + +// tell transpiler how to get locations for mondo`` calls +registerLanguage('mondo', { + getLocations, +}); + +// this is like mondo, but with a zero offset +export const mondolang = (code) => mondo(code, 0); +registerLanguage('mondolang', { + getLocations: (code) => getLocations(code, 0), +}); +// uncomment the following to use mondo as mini notation language +/* registerLanguage('minilang', { + name: 'mondi', + getLocations, +}); */ diff --git a/packages/mondough/package.json b/packages/mondough/package.json new file mode 100644 index 000000000..c81d76cb6 --- /dev/null +++ b/packages/mondough/package.json @@ -0,0 +1,44 @@ +{ + "name": "@strudel/mondo", + "version": "1.1.4", + "description": "mondo notation for strudel", + "main": "mondough.mjs", + "type": "module", + "publishConfig": { + "main": "dist/mondough.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", + "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 ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*", + "@strudel/transpiler": "workspace:*", + "mondolang": "workspace:*" + }, + "devDependencies": { + "mondo": "*", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js new file mode 100644 index 000000000..06c8c4559 --- /dev/null +++ b/packages/mondough/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'mondough.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'mondough.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/motion/package.json b/packages/motion/package.json index cc117a268..57cac9cc8 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,38 +1,37 @@ { - "name": "@strudel/motion", - "version": "1.1.0", - "description": "DeviceMotion API for strudel", - "main": "index.mjs", - "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, - "scripts": { - "build": "vite build", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" - }, - "keywords": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Yuta Nakayama ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*" - }, - "devDependencies": { + "name": "@strudel/motion", + "version": "1.2.4", + "description": "DeviceMotion API for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://codeberg.org/uzu/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://codeberg.org/uzu/strudel/issues" + }, + "homepage": "https://codeberg.org/uzu/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { "vite": "^6.0.11" - } + } } - \ No newline at end of file diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index c2322600c..aef01bd93 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -1,6 +1,6 @@ /* mqtt.mjs - for patterning the internet of things from strudel -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index ef5c3c680..2e32825fe 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.1.0", + "version": "1.2.4", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "paho-mqtt": "^1.1.0" diff --git a/packages/osc/README.md b/packages/osc/README.md index 3dc65e16e..0a7bfedcf 100644 --- a/packages/osc/README.md +++ b/packages/osc/README.md @@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup ## Usage -OSC will only work if you run the REPL locally + the OSC server besides it: +Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via: -From the project root: - -```js -npm run repl +```sh +npx @strudel/osc ``` -and in a seperate shell: - -```js -npm run osc -``` - -This should give you +You should see something like: ```log osc client running on port 57120 @@ -26,14 +18,32 @@ osc server running on port 57121 websocket server running on port 8080 ``` -Now open Supercollider (with the super dirt startup file) +### --port -Now open the REPL and type: +By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option: -```js -s(" hh").osc() +```sh +npx @strudel/osc --port 7771 # classic dirt ``` -or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)... +### --debug + +To log all incoming osc messages, add the `--debug` flag: + +```sh +npx @strudel/osc --debug +``` + +## Usage in Strudel + +To test it in strudel, you have can use `all(osc)` to send all events through osc: + +```js +$: s("bd*4") + +all(osc) +``` + +[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 4b9ab14e7..fe0691522 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -1,12 +1,12 @@ /* osc.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import OSC from 'osc-js'; -import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core'; +import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; let connection; // Promise function connect() { @@ -37,7 +37,7 @@ function connect() { export function parseControlsFromHap(hap, cps) { hap.ensureObjectValue(); const cycle = hap.wholeOrPart().begin.valueOf(); - const delta = hap.duration.valueOf(); + const delta = hap.duration.valueOf() / cps; const controls = Object.assign({}, { cps, cycle, delta }, hap.value); // make sure n and note are numbers controls.n && (controls.n = parseNumeral(controls.n)); @@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) { const collator = new ClockCollator({}); -export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { const osc = await connect(); const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); @@ -81,6 +81,4 @@ export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetT * @memberof Pattern * @returns Pattern */ -Pattern.prototype.osc = function () { - return this.onTrigger(oscTrigger); -}; +export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger)); diff --git a/packages/osc/package.json b/packages/osc/package.json index 5c56bb2e3..bc828d798 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,8 +1,9 @@ { "name": "@strudel/osc", - "version": "1.1.0", + "version": "1.2.10", "description": "OSC messaging for strudel", "main": "osc.mjs", + "bin": "./server.js", "type": "module", "publishConfig": { "main": "dist/index.mjs" @@ -17,7 +18,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -32,9 +33,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "osc-js": "^2.4.1" diff --git a/packages/osc/server.js b/packages/osc/server.js index 7e0b90592..d7ec21c4b 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -1,11 +1,26 @@ +#!/usr/bin/env node + /* server.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import OSC from 'osc-js'; +const args = process.argv.slice(2); +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + +let udpClientPort = Number(getArgValue('--port')) || 57120; +let debug = Number(getArgValue('--debug')) || 0; + const config = { receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client udpServer: { @@ -17,7 +32,7 @@ const config = { }, udpClient: { host: 'localhost', // @param {string} Hostname of udp client for messaging - port: 57120, // @param {number} Port of udp client for messaging + port: udpClientPort, // @param {number} Port of udp client for messaging }, wsServer: { host: 'localhost', // @param {string} Hostname of WebSocket server @@ -27,8 +42,34 @@ const config = { const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); -osc.open(); // start a WebSocket server on port 8080 +if (debug) { + osc.on('*', (message) => { + const { address, args } = message; + let str = ''; + for (let i = 0; i < args.length; i += 2) { + str += `${args[i]}: ${args[i + 1]} `; + } + console.log(`${address} ${str}`); + }); +} + +osc.on('error', (message) => { + if (message.toString().includes('EADDRINUSE')) { + console.log(`------ ERROR ------- +a server is already running on port 57121! to stop it: +1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) +2. re-run the osc server +`); + } else { + console.log(message); + } +}); + +osc.open(); console.log('osc client running on port', config.udpClient.port); console.log('osc server running on port', config.udpServer.port); console.log('websocket server running on port', config.wsServer.port); +if (debug) { + console.log('debug logs enabled. incoming messages will appear below'); +} diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index 3f48e66bd..a317af8f1 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -1,10 +1,10 @@ -import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; -import { isTauri } from '../desktopbridge/utils.mjs'; +/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; +import { isTauri } from '../desktopbridge/utils.mjs'; */ import { oscTrigger } from './osc.mjs'; -const trigger = isTauri() ? oscTriggerTauri : oscTrigger; +const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { const currentTime = performance.now() / 1000; - return trigger(null, hap, currentTime, cps, targetTime); + return trigger(hap, currentTime, cps, targetTime); }; diff --git a/packages/osc/tidal-sniffer.js b/packages/osc/tidal-sniffer.js index 10bf0ad05..95e1fc329 100644 --- a/packages/osc/tidal-sniffer.js +++ b/packages/osc/tidal-sniffer.js @@ -1,6 +1,6 @@ /* tidal-sniffer.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/reference/package.json b/packages/reference/package.json index f69aa6536..634057e4c 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.1.0", + "version": "1.2.1", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -28,11 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { + "url": "https://codeberg.org/uzu/strudel/issues" }, + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { "vite": "^6.0.11" } diff --git a/packages/reference/vite.config.js b/packages/reference/vite.config.js index 5df3edc1b..44ff1e405 100644 --- a/packages/reference/vite.config.js +++ b/packages/reference/vite.config.js @@ -1,5 +1,4 @@ import { defineConfig } from 'vite'; -import { dependencies } from './package.json'; import { resolve } from 'path'; // https://vitejs.dev/config/ @@ -11,9 +10,6 @@ export default defineConfig({ formats: ['es'], fileName: (ext) => ({ es: 'index.mjs' })[ext], }, - rollupOptions: { - external: [...Object.keys(dependencies)], - }, target: 'esnext', }, }); diff --git a/packages/repl/README.md b/packages/repl/README.md index 46819712c..5b43b5e79 100644 --- a/packages/repl/README.md +++ b/packages/repl/README.md @@ -17,7 +17,7 @@ You can also pin the version like this: ``` This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase. -See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions. +See [releases](https://codeberg.org/uzu/strudel/releases) for the latest versions. ## Use Web Component @@ -91,7 +91,7 @@ or ``` -The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. +The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://codeberg.org/uzu/strudel/src/branch/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code. diff --git a/packages/repl/package.json b/packages/repl/package.json index 7a48ad3fd..41a165c34 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.1.0", + "version": "1.2.6", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", @@ -46,6 +46,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index dd0023fb5..3f421f985 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -36,7 +36,7 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), + samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js index 674e0ebb4..c1ef1bf7f 100644 --- a/packages/repl/vite.config.js +++ b/packages/repl/vite.config.js @@ -1,11 +1,12 @@ import { defineConfig } from 'vite'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c495c2add..1142176b7 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -20,3 +20,13 @@ samples('http://localhost:5432') LOG=1 npx @strudel/sampler # adds logging PORT=5555 npx @strudel/sampler # changes port ``` + +## static json + +when running with `--json`, you will simply get the json logged back: + +```sh +npx --yes @strudel/sampler --json > strudel.json +``` + +this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/package.json b/packages/sampler/package.json index 252fe9350..2bf0607b1 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.1.0", + "version": "0.2.3", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 12520992d..31b83f5a3 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,22 +1,20 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream } from 'fs'; +import { createReadStream, existsSync, writeFileSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; -import { join, sep } from 'path'; +import { join, resolve, sep } from 'path'; +import readline from 'readline'; import os from 'os'; -// eslint-disable-next-line const LOG = !!process.env.LOG || false; +const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; -console.log( - cowsay.say({ - text: 'welcome to @strudel/sampler', - e: 'oO', - T: 'U ', - }), -); +const isAudioFile = (f) => { + const ext = f.split('.').slice(-1)[0].toLowerCase(); + return VALID_AUDIO_EXTENSIONS.includes(ext); +}; async function getFilesInDirectory(directory) { let files = []; @@ -29,53 +27,104 @@ async function getFilesInDirectory(directory) { continue; } try { - const subFiles = (await getFilesInDirectory(fullPath)).filter((f) => - ['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()), - ); + const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile); files = files.concat(subFiles); LOG && console.log(`${dirent.name} (${subFiles.length})`); } catch (err) { LOG && console.warn(`skipped due to error: ${fullPath}`); } } else { - files.push(fullPath); + isAudioFile(fullPath) && files.push(fullPath); } } return files; } -async function getBanks(directory) { +async function getBanks(directory, flat = false) { let files = await getFilesInDirectory(directory); let banks = {}; directory = directory.split(sep).join('/'); files = files.map((path) => { path = path.split(sep).join('/'); - const [bank] = path.split('/').slice(-2); + const subDir = path.replace(directory, ''); + const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore + const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension + let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0]; banks[bank] = banks[bank] || []; - const relativeUrl = path.replace(directory, ''); - banks[bank].push(relativeUrl); - return relativeUrl; + banks[bank].push(subDir); + return subDir; }); banks._base = `http://localhost:5432`; return { banks, files }; } -// eslint-disable-next-line -const directory = process.cwd(); +const args = process.argv.slice(2); + +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + +function getInput(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (response) => { + rl.close(); + resolve(response); + }), + ); +} + +let directory = getArgValue('--dir') || process.cwd(); +directory = resolve(directory); +if (args.includes('--json')) { + const { banks } = await getBanks(directory, getArgValue('--flat')); + const json = JSON.stringify(banks); + const outFile = resolve(directory, 'strudel.json'); + if (existsSync(outFile)) { + const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `); + if (answer.toLowerCase() !== 'y') { + console.log('Aborted.'); + process.exit(0); + } + } + writeFileSync(outFile, json, 'utf8'); + console.log(`Wrote json to ${outFile}`); +} + +console.log( + cowsay.say({ + text: 'welcome to @strudel/sampler', + e: 'oO', + T: 'U ', + }), +); + const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); - const { banks, files } = await getBanks(directory); + const { banks, files } = await getBanks(directory, getArgValue('--flat')); if (req.url === '/') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(banks)); } let subpath = decodeURIComponent(req.url); - if (!files.includes(subpath)) { + const filePath = join(directory, subpath.split('/').join(sep)); + + // console.log('GET:', filePath); + const isFound = existsSync(filePath); + if (!isFound) { res.statusCode = 404; res.end('File not found'); return; } - const filePath = join(directory, subpath.split('/').join(sep)); const readStream = createReadStream(filePath); readStream.on('error', (err) => { res.statusCode = 500; @@ -99,12 +148,6 @@ Object.keys(networkInterfaces).forEach((key) => { }); }); -if (!IP) { - console.error("Unable to determine server's IP address."); - // eslint-disable-next-line - process.exit(1); -} - server.listen(PORT, IP_ADDRESS, () => { console.log(`@strudel/sampler is now serving audio files from: ${directory} @@ -113,6 +156,6 @@ To use them in the Strudel REPL, run: samples('http://localhost:${PORT}') Or on a machine in the same network: - samples('http://${IP}:${PORT}') + ${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`} `); }); diff --git a/packages/serial/package.json b/packages/serial/package.json index 74cef80ab..9ed89cf2a 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.1.0", + "version": "1.2.4", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index e0eeacedd..692109522 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -1,6 +1,6 @@ /* serial.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 8cbe3e81a..427f57e31 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -9,6 +9,13 @@ import { } from '@strudel/webaudio'; import gm from './gm.mjs'; +let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound'; +let soundfontUrl = defaultSoundfontUrl; + +export function setSoundfontUrl(value) { + soundfontUrl = value; +} + let loadCache = {}; async function loadFont(name) { if (loadCache[name]) { @@ -16,7 +23,7 @@ async function loadFont(name) { } const load = async () => { // TODO: make soundfont source configurable - const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`; + const url = `${soundfontUrl}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); return eval('{' + data); diff --git a/packages/soundfonts/gm.mjs b/packages/soundfonts/gm.mjs index c0e57dcb0..d8b7687d7 100644 --- a/packages/soundfonts/gm.mjs +++ b/packages/soundfonts/gm.mjs @@ -537,7 +537,7 @@ export default { ], gm_synth_bass_1: [ // Synth Bass 1: Bass - '0380_Aspirin_sf2_file', + // '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384 '0380_Chaos_sf2_file', '0380_FluidR3_GM_sf2_file', // 0380_GeneralUserGS_sf2_file // laut diff --git a/packages/soundfonts/index.mjs b/packages/soundfonts/index.mjs index c54297b50..31792603c 100644 --- a/packages/soundfonts/index.mjs +++ b/packages/soundfonts/index.mjs @@ -1,6 +1,6 @@ -import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs'; +import { getFontBufferSource, registerSoundfonts, setSoundfontUrl } from './fontloader.mjs'; import * as soundfontList from './list.mjs'; import { startPresetNote } from 'sfumato'; import { loadSoundfont } from './sfumato.mjs'; -export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts }; +export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts, setSoundfontUrl }; diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index af6d2b969..07e35674b 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.1.0", + "version": "1.2.5", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 6d60f80dd..4d5e7ef45 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; Pattern.prototype.soundfont = function (sf, n = 0) { - return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => { + return this.onTrigger((h, ct, cps, targetTime) => { const ctx = getAudioContext(); const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ed5c1e7e0..5b02ad298 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -74,6 +74,6 @@ export const dough = async (code) => { worklet.node.connect(ac.destination); }; -export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) { +export function doughTrigger(hap, currentTime, cps, targetTime) { window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..47bca330d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,8 @@ import { getAudioContext } from './superdough.mjs'; -import { clamp, nanFallback } from './util.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; +import { getNoiseBuffer } from './noise.mjs'; + +export const noises = ['pink', 'white', 'brown', 'crackle']; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -7,6 +10,13 @@ export function gainNode(value) { return node; } +export function effectSend(input, effect, wet) { + const send = gainNode(wet); + input.connect(send); + send.connect(effect); + return send; +} + const getSlope = (y1, y2, x1, x2) => { const denom = x2 - x1; if (denom === 0) { @@ -18,7 +28,9 @@ const getSlope = (y1, y2, x1, x2) => { export function getWorklet(ac, processor, params, config) { const node = new AudioWorkletNode(ac, processor, config); Object.entries(params).forEach(([key, value]) => { - node.parameters.get(key).value = value; + if (value !== undefined) { + node.parameters.get(key).value = value; + } }); return node; } @@ -85,6 +97,35 @@ export const getParamADSR = ( param[ramp](min, end + release); }; +function getModulationShapeInput(val) { + if (typeof val === 'number') { + return val % 5; + } + return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0; +} + +export function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; + const { dcoffset = -0.5, depth = 1 } = properties; + const lfoprops = { + frequency: 1, + depth, + skew: 0.5, + phaseoffset: 0, + time: begin, + begin, + end, + shape: getModulationShapeInput(shape), + dcoffset, + min: dcoffset * depth, + max: dcoffset * depth + depth, + curve: 1, + ...props, + }; + + return getWorklet(audioContext, 'lfo-processor', lfoprops); +} + export function getCompressor(ac, threshold, ratio, knee, attack, release) { const options = { threshold: threshold ?? -3, @@ -112,6 +153,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; +// helper utility for applying standard modulators to a parameter +export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) { + let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; + + if (amount == null) { + const hasADSRParams = values.some((p) => p != null); + amount = hasADSRParams ? defaultAmount : 0; + } + + const min = offset ?? 0; + const max = amount + min; + const diff = Math.abs(max - min); + if (diff) { + const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues); + getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); + } + let lfo; + let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues; + + if (depth == null) { + const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null); + depth = hasLFOParams ? defaultDepth : 0; + } + if (depth) { + lfo = getLfo(audioContext, start, end, { + depth, + dcoffset, + ...getLfoInputs, + }); + lfo.connect(param); + } + + return { lfo, disconnect: () => lfo?.disconnect() }; +} + export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { const curve = 'exponential'; const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); @@ -171,7 +247,7 @@ let curves = ['linear', 'exponential']; export function getPitchEnvelope(param, value, t, holdEnd) { // envelope is active when any of these values is set const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv; - if (!hasEnvelope) { + if (hasEnvelope === undefined) { return; } const penv = nanFallback(value.penv, 1, true); @@ -206,18 +282,46 @@ export function getVibratoOscillator(param, value, t) { // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // a bit of a hack, but it works very well :) export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { - const constantNode = audioContext.createConstantSource(); - constantNode.start(startTime); - constantNode.stop(stopTime); + const constantNode = new ConstantSourceNode(audioContext); + + // Certain browsers requires audio nodes to be connected in order for their onended events + // to fire, so we _mute it_ and then connect it to the destination + const zeroGain = gainNode(0); + zeroGain.connect(audioContext.destination); + constantNode.connect(zeroGain); + + // Schedule the `onComplete` callback to occur at `stopTime` constantNode.onended = () => { + // Ensure garbage collection + try { + zeroGain.disconnect(); + } catch { + // pass + } + try { + constantNode.disconnect(); + } catch { + // pass + } onComplete(); }; + constantNode.start(startTime); + constantNode.stop(stopTime); + return constantNode; } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); - const osc = ctx.createOscillator(); - osc.type = type; - osc.frequency.value = freq; + let osc; + if (noises.includes(type)) { + osc = ctx.createBufferSource(); + osc.buffer = getNoiseBuffer(type, 2); + osc.loop = true; + } else { + osc = ctx.createOscillator(); + osc.type = type; + osc.frequency.value = freq; + } + osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range @@ -252,7 +356,7 @@ export function applyFM(param, value, begin) { modulator = fmmod.node; stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { // no envelope by default modulator.connect(param); } else { @@ -276,3 +380,25 @@ export function applyFM(param, value, begin) { } return { stop }; } + +export const getFrequencyFromValue = (value, defaultNote = 36) => { + let { note, freq } = value; + note = note || defaultNote; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + + return Number(freq); +}; + +export const destroyAudioWorkletNode = (node) => { + if (node == null) { + return; + } + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +}; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index 3247c5b49..a7e87ffae 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -11,3 +11,4 @@ export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; export * from './dspworklet.mjs'; +export * from './wavetable.mjs'; diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index a20af1b3c..99fd0cf39 100644 --- a/packages/superdough/logger.mjs +++ b/packages/superdough/logger.mjs @@ -1,5 +1,12 @@ let log = (msg) => console.log(msg); +export function errorLogger(e, origin = 'superdough') { + if (process.env.NODE_ENV === 'development') { + console.error(e); + } + logger(`[${origin}] error: ${e.message}`); +} + export const logger = (...args) => log(...args); export const setLogger = (fn) => { diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 247794702..5411f2f2f 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type, density) { +export function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 5fb8439bf..a82117774 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.1.0", + "version": "1.2.5", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -28,11 +28,12 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 0f638ca80..2960b597c 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,7 +1,9 @@ import reverbGen from './reverbGen.mjs'; +import { clamp } from './util.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer) { + AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { + const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length); const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); for (let channel = 0; channel < buffer.numberOfChannels; channel++) { @@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') { let newData = newBuffer.getChannelData(channel); for (let i = 0; i < newLength; i++) { - newData[i] = oldData[i] || 0; + // loop the buffer around to prevent + let position = (sampleOffset + i * Math.abs(speed)) % oldData.length; + if (speed < 1) { + position = position * -1; + } + + newData[i] = oldData.at(position) || 0; } } return newBuffer; }; - AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) { + AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { const convolver = this.createConvolver(); - convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => { + convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => { convolver.duration = d; convolver.fade = fade; convolver.lp = lp; convolver.dim = dim; convolver.ir = ir; + convolver.irspeed = irspeed; + convolver.irbegin = irbegin; if (ir) { - convolver.buffer = this.adjustLength(d, ir); + convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin); } else { reverbGen.generateReverb( { @@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') { ); } }; - convolver.generate(duration, fade, lp, dim, ir); + convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin); return convolver; }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f5e46d6bb..b195ba79c 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ -import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; +import { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -22,39 +22,16 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -// deduces relevant info for sample loading from hap.value and sample definition -// it encapsulates the core sampler logic into a pure and synchronous function -// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) export function getSampleInfo(hapValue, bank) { - const { s, n = 0, speed = 1.0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; - let sampleUrl; - let index = 0; - if (Array.isArray(bank)) { - index = getSoundIndex(n, bank.length); - sampleUrl = bank[index]; - } else { - const midiDiff = (noteA) => noteToMidi(noteA) - midi; - // object format will expect keys as notes - const closest = Object.keys(bank) - .filter((k) => !k.startsWith('_')) - .reduce( - (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), - null, - ); - transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, bank[closest].length); - sampleUrl = bank[closest][index]; - } - const label = `${s}:${index}`; + const { speed = 1.0 } = hapValue; + const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); - return { transpose, sampleUrl, index, midi, label, playbackRate }; + return { transpose, url, index, midi, label, playbackRate }; } // takes hapValue and returns buffer + playbackRate. export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); + let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } @@ -79,14 +56,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.buffer = buffer; bufferSource.playbackRate.value = playbackRate; - const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + const loop = hapValue.loop; if (loop) { bufferSource.loop = true; bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; @@ -196,6 +173,52 @@ function getSamplesPrefixHandler(url) { return; } +export async function fetchSampleMap(url) { + // check if custom prefix handler + const handler = getSamplesPrefixHandler(url); + if (handler) { + return handler(url); + } + url = resolveSpecialPaths(url); + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (url.startsWith('shabda:')) { + let [_, path] = url.split('shabda:'); + url = `https://shabda.ndre.gr/${path}.json?strudel=1`; + } + if (url.startsWith('shabda/speech')) { + let [_, path] = url.split('shabda/speech'); + path = path.startsWith('/') ? path.substring(1) : path; + let [params, words] = path.split(':'); + let gender = 'f'; + let language = 'en-GB'; + if (params) { + [language, gender] = params.split('/'); + } + url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + const base = url.split('/').slice(0, -1).join('/'); + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + const json = await fetch(url) + .then((res) => res.json()) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); + return [json, json._base || base]; +} + /** * Loads a collection of samples to use with `s` * @example @@ -217,61 +240,16 @@ function getSamplesPrefixHandler(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); - } - sampleMap = resolveSpecialPaths(sampleMap); - if (sampleMap.startsWith('github:')) { - sampleMap = githubPath(sampleMap, 'strudel.json'); - } - if (sampleMap.startsWith('local:')) { - sampleMap = `http://localhost:5432`; - } - if (sampleMap.startsWith('shabda:')) { - let [_, path] = sampleMap.split('shabda:'); - sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; - } - if (sampleMap.startsWith('shabda/speech')) { - let [_, path] = sampleMap.split('shabda/speech'); - path = path.startsWith('/') ? path.substring(1) : path; - let [params, words] = path.split(':'); - let gender = 'f'; - let language = 'en-GB'; - if (params) { - [language, gender] = params.split('/'); - } - sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; - } - if (typeof fetch !== 'function') { - // not a browser - return; - } - const base = sampleMap.split('/').slice(0, -1).join('/'); - if (typeof fetch === 'undefined') { - // skip fetch when in node / testing - return; - } - return fetch(sampleMap) - .then((res) => res.json()) - .then((json) => samples(json, baseUrl || json._base || base, options)) - .catch((error) => { - console.error(error); - throw new Error(`error loading "${sampleMap}"`); - }); + const [json, base] = await fetchSampleMap(sampleMap); + return samples(json, baseUrl || base, options); } const { prebake, tag } = options; + processSampleMap( sampleMap, - (key, bank) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { - type: 'sample', - samples: bank, - baseUrl, - prebake, - tag, - }), + (key, bank) => { + registerSampleSource(key, bank, { baseUrl, prebake, tag }); + }, baseUrl, ); }; @@ -344,7 +322,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { }; let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); - const stop = (endTime, playWholeBuffer) => {}; + const stop = (endTime) => { + bufferSource.stop(endTime); + }; const handle = { node: out, bufferSource, stop }; // cut groups @@ -359,3 +339,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { return handle; } + +function registerSample(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { + type: 'sample', + samples: bank, + ...params, + }); +} + +export function registerSampleSource(key, bank, params) { + const isWavetable = key.startsWith('wt_'); + if (isWavetable) { + registerWaveTable(key, bank, params); + } else { + registerSample(key, bank, params); + } +} diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..52ed9bff8 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -1,23 +1,49 @@ /* superdough.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; -import { clamp, nanFallback, _mod } from './util.mjs'; -import workletsUrl from './worklets.mjs?worker&url'; -import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; +import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; +import workletsUrl from './worklets.mjs?audioworklet'; +import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { SuperdoughAudioController } from './superdoughoutput.mjs'; + +export const DEFAULT_MAX_POLYPHONY = 128; +const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; + +let maxPolyphony = DEFAULT_MAX_POLYPHONY; + +export function setMaxPolyphony(polyphony) { + maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; +} + +let multiChannelOrbits = false; +export function setMultiChannelOrbits(bool) { + multiChannelOrbits = bool == true; +} export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - soundMap.setKey(key.toLowerCase(), { onTrigger, data }); + key = key.toLowerCase().replace(/\s+/g, '_'); + soundMap.setKey(key, { onTrigger, data }); +} + +let gainCurveFunc = (val) => val; + +export function applyGainCurve(val) { + return gainCurveFunc(val); +} + +export function setGainCurve(newGainCurveFunc) { + gainCurveFunc = newGainCurveFunc; } function aliasBankMap(aliasMap) { @@ -81,11 +107,40 @@ export async function aliasBank(...args) { } } +/** + * Register an alias for a sound. + * @param {string} original - The original sound name + * @param {string} alias - The alias to use for the sound + */ +export function soundAlias(original, alias) { + if (getSound(original) == null) { + logger('soundAlias: original sound not found'); + return; + } + soundMap.setKey(alias, getSound(original)); +} + export function getSound(s) { + if (typeof s !== 'string') { + console.warn(`getSound: expected string got "${s}". fall back to triangle`); + return soundMap.get().triangle; // is this good? + } return soundMap.get()[s.toLowerCase()]; } -const defaultDefaultValues = { +export const getAudioDevices = async () => { + await navigator.mediaDevices.getUserMedia({ audio: true }); + let mediaDevices = await navigator.mediaDevices.enumerateDevices(); + mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const devicesMap = new Map(); + devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, ''); + mediaDevices.forEach((device) => { + devicesMap.set(device.label, device.deviceId); + }); + return devicesMap; +}; + +let defaultDefaultValues = { s: 'triangle', gain: 0.8, postgain: 1, @@ -100,14 +155,26 @@ const defaultDefaultValues = { shapevol: 1, distortvol: 1, delay: 0, + byteBeatExpression: '0', delayfeedback: 0.5, - delaytime: 0.25, + delaysync: 3 / 16, orbit: 1, i: 1, velocity: 1, fft: 8, }; +const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); + +export function setDefault(control, value) { + // const main = getControlName(control); // we cant do this because superdough is independent of strudel/core + defaultDefaultValues[control] = value; +} + +export function resetDefaults() { + defaultDefaultValues = { ...defaultDefaultDefaultValues }; +} + let defaultControls = new Map(Object.entries(defaultDefaultValues)); export function setDefaultValue(key, value) { @@ -136,7 +203,7 @@ export const resetLoadedSounds = () => soundMap.set({}); let audioContext; export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); + audioContext = new AudioContext({ latencyHint: 'playback' }); return audioContext; }; @@ -152,21 +219,56 @@ export function getAudioContextCurrentTime() { return getAudioContext().currentTime; } +let externalWorklets = []; +export function registerWorklet(url) { + externalWorklets.push(url); +} + let workletsLoading; function loadWorklets() { if (!workletsLoading) { - workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + const audioCtx = getAudioContext(); + const allWorkletURLs = externalWorklets.concat([workletsUrl]); + workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))); } + return workletsLoading; } // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false } = options; + const { + disableWorklets = false, + maxPolyphony, + audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME, + multiChannelOrbits = false, + } = options; + + setMaxPolyphony(maxPolyphony); + setMultiChannelOrbits(multiChannelOrbits); if (typeof window === 'undefined') { return; } - await getAudioContext().resume(); + + const audioCtx = getAudioContext(); + + if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) { + try { + const devices = await getAudioDevices(); + const id = devices.get(audioDeviceName); + const isValidID = (id ?? '').length > 0; + if (audioCtx.sinkId !== id && isValidID) { + await audioCtx.setSinkId(id); + } + logger( + `[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`, + ); + } catch { + logger('[superdough] failed to set audio interface', 'warning'); + } + } + + await audioCtx.resume(); if (disableWorklets) { logger('[superdough]: AudioWorklets disabled with disableWorklets'); return; @@ -193,86 +295,21 @@ export async function initAudioOnFirstClick(options) { return audioReady; } -let delays = {}; -const maxfeedback = 0.98; - -let channelMerger, destinationGain; -//update the output channel configuration to match user's audio device -export function initializeAudioOutput() { - const audioContext = getAudioContext(); - const maxChannelCount = audioContext.destination.maxChannelCount; - audioContext.destination.channelCount = maxChannelCount; - channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); - destinationGain = new GainNode(audioContext); - channelMerger.connect(destinationGain); - destinationGain.connect(audioContext.destination); +let controller; +function getSuperdoughAudioController() { + if (controller == null) { + controller = new SuperdoughAudioController(getAudioContext()); + } + return controller; } - -// input: AudioNode, channels: ?Array -export const connectToDestination = (input, channels = [0, 1]) => { - const ctx = getAudioContext(); - if (channelMerger == null) { - initializeAudioOutput(); - } - //This upmix can be removed if correct channel counts are set throughout the app, - // and then strudel could theoretically support surround sound audio files - const stereoMix = new StereoPannerNode(ctx); - input.connect(stereoMix); - - const splitter = new ChannelSplitterNode(ctx, { - numberOfOutputs: stereoMix.channelCount, - }); - stereoMix.connect(splitter); - channels.forEach((ch, i) => { - splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1)); - }); -}; - -export const panic = () => { - if (destinationGain == null) { - return; - } - destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); - destinationGain = null; - channelMerger == null; -}; - -function getDelay(orbit, delaytime, delayfeedback, t) { - if (delayfeedback > maxfeedback) { - //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); - } - delayfeedback = clamp(delayfeedback, 0, 0.98); - if (!delays[orbit]) { - const ac = getAudioContext(); - const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); - dly.start?.(t); // for some reason, this throws when audion extension is installed.. - connectToDestination(dly, [0, 1]); - delays[orbit] = dly; - } - delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); - delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t); - return delays[orbit]; +export function connectToDestination(input, channels) { + const controller = getSuperdoughAudioController(); + controller.output.connectToDestination(input, channels); } function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { - //gain const ac = getAudioContext(); - const lfoGain = ac.createGain(); - lfoGain.gain.value = sweep * 2; - // centerFrequency = centerFrequency * 2; - // sweep = sweep * 1.5; - - const lfo = getWorklet(ac, 'lfo-processor', { - frequency, - depth: 1, - skew: 0, - phaseoffset: 0, - time, - end, - shape: 1, - dcoffset: -0.5, - }); - lfo.connect(lfoGain); + const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); //filters const numStages = 2; //num of filters in series @@ -301,33 +338,6 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -let reverbs = {}; -let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir) { - // If no reverb has been created for a given orbit, create one - if (!reverbs[orbit]) { - const ac = getAudioContext(); - const reverb = ac.createReverb(duration, fade, lp, dim, ir); - connectToDestination(reverb, [0, 1]); - reverbs[orbit] = reverb; - } - if ( - hasChanged(duration, reverbs[orbit].duration) || - hasChanged(fade, reverbs[orbit].fade) || - hasChanged(lp, reverbs[orbit].lp) || - hasChanged(dim, reverbs[orbit].dim) || - reverbs[orbit].ir !== ir - ) { - // only regenerate when something has changed - // avoids endless regeneration on things like - // stack(s("a"), s("b").rsize(8)).room(.5) - // this only works when args may stay undefined until here - // setting default values breaks this - reverbs[orbit].generate(duration, fade, lp, dim, ir); - } - return reverbs[orbit]; -} - export let analysers = {}, analysersData = {}; @@ -360,23 +370,24 @@ export function getAnalyzerData(type = 'time', id = 1) { return analysersData[id]; } -function effectSend(input, effect, wet) { - const send = gainNode(wet); - input.connect(send); - send.connect(effect); - return send; -} - export function resetGlobalEffects() { - delays = {}; - reverbs = {}; + controller?.reset(); analysers = {}; analysersData = {}; } -export const superdough = async (value, t, hapDuration) => { +let activeSoundSources = new Map(); +//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0 + +function mapChannelNumbers(channels) { + return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); +} + +export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { + // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); - t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; + const audioController = getSuperdoughAudioController(); + let { stretch } = value; if (stretch != null) { //account for phase vocoder latency @@ -402,15 +413,27 @@ export const superdough = async (value, t, hapDuration) => { } // destructure let { + tremolo, + tremolosync, + tremolodepth = 1, + tremoloskew, + tremolophase = 0, + tremoloshape, s = getDefaultValue('s'), bank, source, gain = getDefaultValue('gain'), postgain = getDefaultValue('postgain'), density = getDefaultValue('density'), + duckorbit, + duckonset, + duckattack, + duckdepth, + djf, // filters fanchor = getDefaultValue('fanchor'), drive = 0.69, + release = 0, // low pass cutoff, lpenv, @@ -435,7 +458,7 @@ export const superdough = async (value, t, hapDuration) => { bpsustain, bprelease, bandq = getDefaultValue('bandq'), - channels = getDefaultValue('channels'), + //phaser phaserrate: phaser, phaserdepth = getDefaultValue('phaserdepth'), @@ -443,7 +466,9 @@ export const superdough = async (value, t, hapDuration) => { phasercenter, // coarse, + crush, + dry, shape, shapevol = getDefaultValue('shapevol'), distort, @@ -452,7 +477,8 @@ export const superdough = async (value, t, hapDuration) => { vowel, delay = getDefaultValue('delay'), delayfeedback = getDefaultValue('delayfeedback'), - delaytime = getDefaultValue('delaytime'), + delaysync = getDefaultValue('delaysync'), + delaytime, orbit = getDefaultValue('orbit'), room, roomfade, @@ -460,6 +486,8 @@ export const superdough = async (value, t, hapDuration) => { roomdim, roomsize, ir, + irspeed, + irbegin, i = getDefaultValue('i'), velocity = getDefaultValue('velocity'), analyze, // analyser wet @@ -471,16 +499,47 @@ export const superdough = async (value, t, hapDuration) => { compressorRelease, } = value; - gain = nanFallback(gain, 1); + delaytime = delaytime ?? cycleToSeconds(delaysync, cps); - //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior - channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); + const orbitChannels = mapChannelNumbers( + multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), + ); + const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; + const orbitBus = audioController.getOrbit(orbit, channels); + if (duckorbit != null) { + audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); + } + + gain = applyGainCurve(nanFallback(gain, 1)); + postgain = applyGainCurve(postgain); + shapevol = applyGainCurve(shapevol); + distortvol = applyGainCurve(distortvol); + delay = applyGainCurve(delay); + velocity = applyGainCurve(velocity); + tremolodepth = applyGainCurve(tremolodepth); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future - let toDisconnect = []; // audio nodes that will be disconnected when the source has ended - const onended = () => { - toDisconnect.forEach((n) => n?.disconnect()); - }; + + const end = t + hapDuration; + const endWithRelease = end + release; + const chainID = Math.round(Math.random() * 1000000); + + // oldest audio nodes will be destroyed if maximum polyphony is exceeded + for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) { + const ch = activeSoundSources.entries().next(); + const source = ch.value[1]; + const chainID = ch.value[0]; + const endTime = t + 0.25; + source?.node?.gain?.linearRampToValueAtTime(0, endTime); + source?.stop?.(endTime); + activeSoundSources.delete(chainID); + } + + let audioNodes = []; + + if (['-', '~', '_'].includes(s)) { + return; + } if (bank && s) { s = `${bank}_${s}`; value.s = s; @@ -489,13 +548,18 @@ export const superdough = async (value, t, hapDuration) => { // get source AudioNode let sourceNode; if (source) { - sourceNode = source(t, value, hapDuration); + sourceNode = source(t, value, hapDuration, cps); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, value, onended); + const onEnded = () => { + audioNodes.forEach((n) => n?.disconnect()); + activeSoundSources.delete(chainID); + }; + const soundHandle = await onTrigger(t, value, onEnded, cps); + if (soundHandle) { sourceNode = soundHandle.node; - soundHandle.stop(t + hapDuration); + activeSoundSources.set(chainID, soundHandle); } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -532,7 +596,7 @@ export const superdough = async (value, t, hapDuration) => { lprelease, lpenv, t, - t + hapDuration, + end, fanchor, ftype, drive, @@ -556,7 +620,7 @@ export const superdough = async (value, t, hapDuration) => { hprelease, hpenv, t, - t + hapDuration, + end, fanchor, ); chain.push(hp()); @@ -567,20 +631,7 @@ export const superdough = async (value, t, hapDuration) => { if (bandf !== undefined) { let bp = () => - createFilter( - ac, - 'bandpass', - bandf, - bandq, - bpattack, - bpdecay, - bpsustain, - bprelease, - bpenv, - t, - t + hapDuration, - fanchor, - ); + createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); chain.push(bp()); if (ftype === '24db') { chain.push(bp()); @@ -598,6 +649,41 @@ export const superdough = async (value, t, hapDuration) => { shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + if (tremolosync != null) { + tremolo = cps * tremolosync; + } + + if (value.wtPosSynced != null) { + value.wtPosRate /= cps; + } + + if (value.wtWarpSynced != null) { + value.wtWarpRate /= cps; + } + + if (tremolo !== undefined) { + // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload + // EX: a triangle waveform will clip like this /-\ when the depth is above 1 + const gain = Math.max(1 - tremolodepth, 0); + const amGain = new GainNode(ac, { gain }); + + const time = cycle / cps; + const lfo = getLfo(ac, t, endWithRelease, { + skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1), + frequency: tremolo, + depth: tremolodepth, + time, + dcoffset: 0, + shape: tremoloshape, + phaseoffset: tremolophase, + min: 0, + max: 1, + curve: 1.5, + }); + lfo.connect(amGain.gain); + chain.push(amGain); + } + compressorThreshold !== undefined && chain.push( getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease), @@ -611,23 +697,20 @@ export const superdough = async (value, t, hapDuration) => { } // phaser if (phaser !== undefined && phaserdepth > 0) { - const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep); + const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); chain.push(phaserFX); } // last gain const post = new GainNode(ac, { gain: postgain }); chain.push(post); - connectToDestination(post, channels); // delay - let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delyNode = getDelay(orbit, delaytime, delayfeedback, t); - delaySend = effectSend(post, delyNode, delay); + orbitBus.getDelay(delaytime, delayfeedback, t); + orbitBus.sendDelay(post, delay); } // reverb - let reverbSend; if (room > 0) { let roomIR; if (ir !== undefined) { @@ -640,23 +723,32 @@ export const superdough = async (value, t, hapDuration) => { } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); - reverbSend = effectSend(post, reverbNode, room); + orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); + orbitBus.sendReverb(post, room); + } + + if (djf != null) { + orbitBus.getDjf(djf, t); } // analyser - let analyserSend; if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); - analyserSend = effectSend(post, analyserNode, 1); + const analyserSend = effectSend(post, analyserNode, 1); + audioNodes.push(analyserSend); + } + if (dry != null) { + dry = applyGainCurve(dry); + const dryGain = new GainNode(ac, { gain: dry }); + chain.push(dryGain); + orbitBus.connectToOutput(dryGain); + } else { + orbitBus.connectToOutput(post); } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - - // toDisconnect = all the node that should be disconnected in onended callback - // this is crucial for performance - toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); + audioNodes = audioNodes.concat(chain); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs new file mode 100644 index 000000000..afe91e71e --- /dev/null +++ b/packages/superdough/superdoughoutput.mjs @@ -0,0 +1,209 @@ +import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; +import { errorLogger } from './logger.mjs'; +import { clamp } from './util.mjs'; + +let hasChanged = (now, before) => now !== undefined && now !== before; + +export class Orbit { + reverbNode; + delayNode; + output; + summingNode; + djfNode; + audioContext; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode.connect(this.output); + } + + disconnect() { + this.output.disconnect(); + this.summingNode.disconnect(); + this.delayNode?.disconnect(); + this.reverbNode?.disconnect(); + } + + getDjf(value, t = 0) { + if (this.djfNode == null) { + this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value }); + this.summingNode.disconnect(); + this.summingNode.connect(this.djfNode); + this.djfNode.connect(this.output); + } + const val = this.djfNode.parameters.get('value'); + val.setValueAtTime(value, t); + } + + getDelay(delaytime = 0, feedback = 0.5, t) { + const maxfeedback = 0.98; + if (feedback > maxfeedback) { + //logger(`feedback was clamped to ${maxfeedback} to save your ears`); + } + feedback = clamp(feedback, 0, 0.98); + if (this.delayNode == null) { + this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); + this.delayNode.connect(this.summingNode); + this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + } + this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); + this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); + return this.delayNode; + } + + getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { + // If no reverb has been created for a given orbit, create one + if (this.reverbNode == null) { + this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + this.reverbNode.connect(this.summingNode); + } + + if ( + hasChanged(duration, this.reverbNode.duration) || + hasChanged(fade, this.reverbNode.fade) || + hasChanged(lp, this.reverbNode.lp) || + hasChanged(dim, this.reverbNode.dim) || + hasChanged(irspeed, this.reverbNode.irspeed) || + hasChanged(irbegin, this.reverbNode.irbegin) || + this.reverbNode.ir !== ir + ) { + // only regenerate when something has changed + // avoids endless regeneration on things like + // stack(s("a"), s("b").rsize(8)).room(.5) + // this only works when args may stay undefined until here + // setting default values breaks this + this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); + } + return this.reverbNode; + } + sendReverb(node, amount) { + effectSend(node, this.reverbNode, amount); + } + + sendDelay(node, amount) { + effectSend(node, this.delayNode, amount); + } + + duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { + const onset = onsettime; + const attack = Math.max(attacktime, 0.002); + const gainParam = this.output.gain; + webAudioTimeout( + this.audioContext, + () => { + const now = this.audioContext.currentTime; + + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method + const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); + + const t0 = Math.max(t, now); // guard against now > t + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); + }, + 0, + t - 0.01, + ); + } + + connectToOutput(node) { + node.connect(this.summingNode); + } +} + +export class SuperdoughOutput { + channelMerger; + destinationGain; + + constructor(audioContext) { + this.audioContext = audioContext; + this.initializeAudio(); + } + + initializeAudio() { + const audioContext = this.audioContext; + const maxChannelCount = audioContext.destination.maxChannelCount; + this.audioContext.destination.channelCount = maxChannelCount; + this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + this.destinationGain = new GainNode(audioContext); + this.channelMerger.connect(this.destinationGain); + this.destinationGain.connect(audioContext.destination); + } + + reset() { + this.disconnect(); + this.initializeAudio(); + } + disconnect() { + this.channelMerger.disconnect(); + this.destinationGain.disconnect(); + this.destinationGain = null; + this.channelMerger = null; + } + connectToDestination = (input, channels = [0, 1]) => { + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(this.audioContext); + input.connect(stereoMix); + + const splitter = new ChannelSplitterNode(this.audioContext, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); + }); + }; +} + +export class SuperdoughAudioController { + audioContext; + output; + nodes = {}; + + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new SuperdoughOutput(audioContext); + } + + reset() { + Array.from(this.nodes).forEach((node) => { + node.disconnect(); + }); + this.nodes = {}; + this.output.reset(); + } + + duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { + const targetArr = [targetOrbits].flat(); + const onsetArr = [onsettime].flat(); + const attackArr = [attacktime].flat(); + const depthArr = [depth].flat(); + + targetArr.forEach((target, idx) => { + const orbit = this.nodes[target]; + + if (orbit == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + + orbit.duck(t, onset, attack, depth); + }); + } + + getOrbit(orbitNum, channels) { + if (this.nodes[orbitNum] == null) { + this.nodes[orbitNum] = new Orbit(this.audioContext); + this.output.connectToDestination(this.nodes[orbitNum].output, channels); + } + return this.nodes[orbitNum]; + } +} diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 621a6e598..5dc5dc08a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,33 +1,39 @@ -import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { clamp } from './util.mjs'; +import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; import { applyFM, + destroyAudioWorkletNode, gainNode, getADSRValues, + getFrequencyFromValue, + getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, - webAudioTimeout, getWorklet, + noises, + webAudioTimeout, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { - let { note, freq } = value; - note = note || 36; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - - return Number(freq); -}; - const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; -const noises = ['pink', 'white', 'brown', 'crackle']; +const waveformAliases = [ + ['tri', 'triangle'], + ['sqr', 'square'], + ['saw', 'sawtooth'], + ['sin', 'sine'], +]; + +function makeSaturationCurve(amount, n_samples) { + const k = typeof amount === 'number' ? amount : 50; + const curve = new Float32Array(n_samples); + + for (let i = 0; i < n_samples; i++) { + const x = (i * 2) / n_samples - 1; + curve[i] = Math.tanh(x * k); + } + return curve; +} export function registerSynthSounds() { [...waveforms].forEach((s) => { @@ -63,12 +69,83 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, ); }); + + registerSound( + 'sbd', + (t, value, onended) => { + const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; + const ctx = getAudioContext(); + const attackhold = 0.02; + const noiselvl = 1.2; + const noisedecay = 0.025; + const mixGain = 1; + + const o = ctx.createOscillator(); + o.type = 'triangle'; + o.frequency.value = getFrequencyFromValue(value, 29); + o.detune.setValueAtTime(penv * 100, 0); + o.detune.setValueAtTime(penv * 100, t); + o.detune.exponentialRampToValueAtTime(0.001, t + pdecay); + const g = gainNode(1); + g.gain.setValueAtTime(1, t + attackhold); + g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay); + o.start(t); + + const noise = getNoiseOscillator('brown', t, 2); + const noiseGain = gainNode(1); + noiseGain.gain.setValueAtTime(noiselvl, t); + noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay); + + const sat = new WaveShaperNode(ctx); + // tri to sine diode shaper emulation + sat.curve = makeSaturationCurve(2, ctx.sampleRate); + + const mix = gainNode(mixGain); + + o.onended = () => { + o.disconnect(); + g.disconnect(); + sat.disconnect(); + noise.node.disconnect(); + noiseGain.disconnect(); + mix.disconnect(); + onended(); + }; + + const node = o.connect(sat).connect(g).connect(mix); + noise.node.connect(noiseGain).connect(mix); + + const holdEnd = t + decay; + let end = holdEnd + 0.01; + if (clip != null) { + end = Math.min(t + clip * duration, end); + } + + // prevent clicking + mix.gain.setValueAtTime(mixGain, end - 0.01); + mix.gain.linearRampToValueAtTime(0, end); + + o.stop(end); + noise.stop(end); + + return { + node, + stop: (endTime) => { + o.stop(endTime); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + registerSound( 'supersaw', (begin, value, onended) => { @@ -110,10 +187,12 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -123,11 +202,164 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + return { + node: envGain, + stop: (time) => { + timeoutNode.stop(time); + }, + }; + }, + { prebake: true, type: 'synth' }, + ); + + registerSound( + 'bytebeat', + (begin, value, onended) => { + const defaultBeats = [ + '(t%255 >= t/255%255)*255', + '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', + 't', + 't*(t >> 10^t)', + 't&128', + 't&t>>8', + '((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)', + '((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)', + '(t&(t>>7)-t)', + '(sin(t*PI/128)*127+127)', + '((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)', + '((t^t/2+t+64*(cos >> 0))%127.85*2)', + '((t^t/2+t+64)%128*2)', + '(((t * .25)^(t * .25)/100+(t * .25))%128)*2', + '((t^t/2+t+64)%7 * 24)', + ]; + const { n = 0 } = value; + const frequency = getFrequencyFromValue(value); + const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value; + + const ac = getAudioContext(); + + let { duration } = value; + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + const holdend = begin + duration; + const end = holdend + release + 0.01; + + let o = getWorklet( + ac, + 'byte-beat-processor', + { + frequency, + begin, + end, + }, + { + outputChannelCount: [2], + }, + ); + + o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency }); + + let envGain = gainNode(1); + envGain = o.connect(envGain); + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( + ac, + () => { + destroyAudioWorkletNode(o); + envGain.disconnect(); + onended(); + }, + begin, + end, + ); return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, + }; + }, + { prebake: true, type: 'synth' }, + ); + + registerSound( + 'pulse', + (begin, value, onended) => { + const ac = getAudioContext(); + let { pwrate, pwsweep } = value; + if (pwsweep == null) { + if (pwrate != null) { + pwsweep = 0.3; + } else { + pwsweep = 0; + } + } + + if (pwrate == null && pwsweep != null) { + pwrate = 1; + } + + let { duration, pw: pulsewidth = 0.5 } = value; + const frequency = getFrequencyFromValue(value); + + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + const holdend = begin + duration; + const end = holdend + release + 0.01; + let o = getWorklet( + ac, + 'pulse-oscillator', + { + frequency, + begin, + end, + pulsewidth, + }, + { + outputChannelCount: [2], + }, + ); + + getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); + const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fm = applyFM(o.parameters.get('frequency'), value, begin); + let envGain = gainNode(1); + envGain = o.connect(envGain); + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + let lfo; + if (pwsweep != 0) { + lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); + lfo.connect(o.parameters.get('pulsewidth')); + } + let timeoutNode = webAudioTimeout( + ac, + () => { + destroyAudioWorkletNode(o); + destroyAudioWorkletNode(lfo); + envGain.disconnect(); + onended(); + fm?.stop(); + vibratoOscillator?.stop(); + }, + begin, + end, + ); + + return { + node: envGain, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -170,12 +402,15 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, ); }); + waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } export function waveformN(partials, type) { diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 4e3c7e41e..475c05f63 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -7,7 +7,7 @@ export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } @@ -68,3 +68,40 @@ export const _mod = (n, m) => ((n % m) + m) % m; export const getSoundIndex = (n, numSounds) => { return _mod(Math.round(nanFallback(n, 0)), numSounds); }; + +export function cycleToSeconds(cycle, cps) { + return cycle / cps; +} + +export function secondsToCycle(t, cps) { + return t * cps; +} + +// deduces relevant info for sample loading from hap.value and sample definition +// it encapsulates the core sampler logic into a pure and synchronous function +// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getCommonSampleInfo(hapValue, bank) { + const { s, n = 0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; + let url; + let index = 0; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + url = bank[index]; + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - midi; + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + index = getSoundIndex(n, bank[closest].length); + url = bank[closest][index]; + } + const label = `${s}:${index}`; + return { transpose, url, index, midi, label }; +} diff --git a/packages/superdough/vite.config.js b/packages/superdough/vite.config.js index d94e37f41..10ac0f06c 100644 --- a/packages/superdough/vite.config.js +++ b/packages/superdough/vite.config.js @@ -1,10 +1,11 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs new file mode 100644 index 000000000..44367f786 --- /dev/null +++ b/packages/superdough/wavetable.mjs @@ -0,0 +1,333 @@ +import { getAudioContext, registerSound } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { + applyParameterModulators, + destroyAudioWorkletNode, + getADSRValues, + getFrequencyFromValue, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + getWorklet, + webAudioTimeout, +} from './helpers.mjs'; +import { logger } from './logger.mjs'; + +export const Warpmode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +const seenKeys = new Set(); +async function getPayload(url, label, frameLen = 2048) { + const key = `${url},${frameLen}`; + if (!seenKeys.has(key)) { + const buf = await loadBuffer(url, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + const numFrames = Math.max(1, Math.floor(total / frameLen)); + const frames = new Array(numFrames); + for (let i = 0; i < numFrames; i++) { + const start = i * frameLen; + frames[i] = ch0.subarray(start, start + frameLen); + } + seenKeys.add(key); + return { frames, frameLen, numFrames, key }; + } + return { frameLen, key }; // worklet will use the cached version +} + +function humanFileSize(bytes, si) { + var thresh = si ? 1000 : 1024; + if (bytes < thresh) return bytes + ' B'; + var units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + var u = -1; + do { + bytes /= thresh; + ++u; + } while (bytes >= thresh); + return bytes.toFixed(1) + ' ' + units[u]; +} + +// Extract the sample rate of a .wav file +function parseWavSampleRate(arrBuf) { + const dv = new DataView(arrBuf); + // Header is "RIFFWAVE", so 12 bytes + let p = 12; + // Look through chunks for the format header + // (they will always have an 8 byte header (id and size) followed by a payload) + while (p + 8 <= dv.byteLength) { + // Parse id + const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3)); + // Parse chunk size + const size = dv.getUint32(p + 4, true); + if (id === 'fmt ') { + // The format chunk contains the sample rate after + // 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels + // (for a total of 12) + return dv.getUint32(p + 12, true); + } + // Advance to next chunk + p += 8 + size + (size & 1); + } + return null; +} + +async function decodeAtNativeRate(arr) { + const sr = parseWavSampleRate(arr) || 44100; + const tempAC = new OfflineAudioContext(1, 1, sr); + return await tempAC.decodeAudioData(arr); +} + +const loadCache = {}; +const loadBuffer = (url, label) => { + url = url.replace('#', '%23'); + if (!loadCache[url]) { + logger(`[wavetable] load table ${label}..`, 'load-table', { url }); + const timestamp = Date.now(); + loadCache[url] = fetch(url) + .then((res) => res.arrayBuffer()) + .then(async (res) => { + const took = Date.now() - timestamp; + const size = humanFileSize(res.byteLength); + logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); + const decoded = await decodeAtNativeRate(res); + return decoded; + }); + } + return loadCache[url]; +}; + +function githubPath(base, subpath = '') { + if (!base.startsWith('github:')) { + throw new Error('expected "github:" at the start of pseudoUrl'); + } + let [_, path] = base.split('github:'); + path = path.endsWith('/') ? path.slice(0, -1) : path; + if (path.split('/').length === 2) { + // assume main as default branch if none set + path += '/main'; + } + return `https://raw.githubusercontent.com/${path}/${subpath}`; +} + +const _processTables = (json, baseUrl, frameLen, options = {}) => { + baseUrl = json._base || baseUrl; + return Object.entries(json).forEach(([key, tables]) => { + if (key === '_base') return false; + if (typeof tables === 'string') { + tables = [tables]; + } + if (typeof tables !== 'object') { + throw new Error('wrong json format for ' + key); + } + let resolvedUrl = baseUrl; + if (resolvedUrl.startsWith('github:')) { + resolvedUrl = githubPath(resolvedUrl, ''); + } + tables = tables + .map((t) => resolvedUrl + t) + .filter((t) => { + if (!t.toLowerCase().endsWith('.wav')) { + logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`); + return false; + } + return true; + }); + if (tables.length) { + registerWaveTable(key, tables, { baseUrl, frameLen }); + } + }); +}; + +export function registerWaveTable(key, tables, params) { + registerSound( + key, + (t, hapValue, onended, cps) => { + return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048); + }, + { + type: 'wavetable', + tables, + ...params, + }, + ); +} + +/** + * Loads a collection of wavetables to use with `s` + * + * @name tables + */ +export const tables = async (url, frameLen, json, options = {}) => { + if (json !== undefined) return _processTables(json, url, frameLen); + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + return fetch(url) + .then((res) => res.json()) + .then((json) => _processTables(json, url, frameLen, options)) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); +}; + +export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { + const { s, n = 0, duration, clip } = value; + const ac = getAudioContext(); + const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); + let { warpmode } = value; + if (typeof warpmode === 'string') { + warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE; + } + const frequency = getFrequencyFromValue(value); + const { url, label } = getCommonSampleInfo(value, tables); + const payload = await getPayload(url, label, frameLen); + let holdEnd = t + duration; + if (clip !== undefined) { + holdEnd = Math.min(t + clip * duration, holdEnd); + } + const endWithRelease = holdEnd + release; + const envEnd = endWithRelease + 0.01; + const source = getWorklet( + ac, + 'wavetable-oscillator-processor', + { + begin: t, + end: envEnd, + frequency, + detune: value.detune, + position: value.wt, + warp: value.warp, + warpMode: warpmode, + voices: Math.max(value.unison ?? 1, 1), + spread: value.spread, + phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, + }, + { outputChannelCount: [2] }, + ); + source.port.postMessage({ type: 'table', payload }); + if (ac.currentTime > t) { + logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); + return; + } + const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease]; + const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease]; + const wtParams = source.parameters; + const positionParam = wtParams.get('position'); + const warpParam = wtParams.get('warp'); + + let wtrate = value.wtrate; + if (value.wtsync != null) { + wtrate = cps * value.wtsync; + } + + const wtPosModulators = applyParameterModulators( + ac, + positionParam, + t, + endWithRelease, + { + offset: value.wt, + amount: value.wtenv, + defaultAmount: 0.5, + shape: 'linear', + values: posADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: wtrate, + depth: value.wtdepth, + defaultDepth: 0.5, + shape: value.wtshape, + skew: value.wtskew, + dcoffset: value.wtdc ?? 0, + }, + ); + + let warprate = value.warprate; + if (value.warpsync != null) { + warprate = warprate = cps * value.warpsync; + } + const wtWarpModulators = applyParameterModulators( + ac, + warpParam, + t, + endWithRelease, + { + offset: value.warp, + amount: value.warpenv, + defaultAmount: 0.5, + shape: 'linear', + values: warpADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: warprate, + depth: value.warpdepth, + defaultDepth: 0.5, + shape: value.warpshape, + skew: value.warpskew, + dcoffset: value.warpdc ?? 0, + }, + ); + const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const envGain = ac.createGain(); + const node = source.connect(envGain); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); + getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); + const handle = { node, source }; + const timeoutNode = webAudioTimeout( + ac, + () => { + destroyAudioWorkletNode(source); + vibratoOscillator?.stop(); + node.disconnect(); + wtPosModulators?.disconnect(); + wtWarpModulators?.disconnect(); + onended(); + }, + t, + envEnd, + ); + handle.stop = (time) => { + timeoutNode.stop(time); + }; + return handle; +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index a2b7828c2..ccfaf5107 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -6,20 +6,44 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); -const _mod = (n, m) => ((n % m) + m) % m; +const mod = (n, m) => ((n % m) + m) % m; +const lerp = (a, b, n) => n * (b - a) + a; +const pv = (arr, n) => arr[n] ?? arr[0]; +const frac = (x) => x - Math.floor(x); +const ffloor = (x) => x | 0; // fast floor for non-negative +const getUnisonDetune = (unison, detune, voiceIndex) => { + if (unison < 2) { + return 0; + } + return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); +}; +const applySemitoneDetuneToFrequency = (frequency, detune) => { + return frequency * Math.pow(2, detune / 12); +}; + +// Restrict phase to the range [0, maxPhase) via wrapping +function wrapPhase(phase, maxPhase = 1) { + if (phase >= maxPhase) { + phase -= maxPhase; + } else if (phase < 0) { + phase += maxPhase; + } + return phase; +} const blockSize = 128; -// adjust waveshape to remove frequencies above nyquist to prevent aliasing +// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { - // 0 <= phase < 1 + dt = Math.min(dt, 1 - dt); + // Start of cycle if (phase < dt) { phase /= dt; // 2 * (phase - phase^2/2 - 0.5) return phase + phase - phase * phase - 1; } - // -1 < phase < 0 + // End of cycle else if (phase > 1 - dt) { phase = (phase - 1) / dt; // 2 * (phase^2/2 + phase + 0.5) @@ -31,7 +55,7 @@ function polyBlep(phase, dt) { return 0; } } - +// The order is important for dough integration const waveshapes = { tri(phase, skew = 0.5) { const x = 1 - skew; @@ -75,11 +99,18 @@ const waveshapes = { return v - polyBlep(phase, dt); }, }; +function getParamValue(block, param) { + if (param.length > 1) { + return param[block]; + } + return param[0]; +} const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ + { name: 'begin', defaultValue: 0 }, { name: 'time', defaultValue: 0 }, { name: 'end', defaultValue: 0 }, { name: 'frequency', defaultValue: 0.5 }, @@ -87,7 +118,10 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'depth', defaultValue: 1 }, { name: 'phaseoffset', defaultValue: 0 }, { name: 'shape', defaultValue: 0 }, + { name: 'curve', defaultValue: 1 }, { name: 'dcoffset', defaultValue: 0 }, + { name: 'min', defaultValue: 0 }, + { name: 'max', defaultValue: 1 }, ]; } @@ -103,11 +137,14 @@ class LFOProcessor extends AudioWorkletProcessor { } } - process(inputs, outputs, parameters) { - // eslint-disable-next-line no-undef + process(_inputs, outputs, parameters) { + const begin = parameters['begin'][0]; if (currentTime >= parameters.end[0]) { return false; } + if (currentTime <= begin) { + return true; + } const output = outputs[0]; const frequency = parameters['frequency'][0]; @@ -117,20 +154,24 @@ class LFOProcessor extends AudioWorkletProcessor { const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; + const curve = parameters['curve'][0]; + const dcoffset = parameters['dcoffset'][0]; + const min = parameters['min'][0]; + const max = parameters['max'][0]; const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; if (this.phase == null) { - this.phase = _mod(time * frequency + phaseoffset, 1); + this.phase = mod(time * frequency + phaseoffset, 1); } - // eslint-disable-next-line no-undef const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { - const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; - output[i][n] = modval; + let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; + modval = Math.pow(modval, curve); + output[i][n] = clamp(modval, min, max); } this.incrementPhase(dt); } @@ -244,6 +285,73 @@ class ShapeProcessor extends AudioWorkletProcessor { } registerProcessor('shape-processor', ShapeProcessor); +class TwoPoleFilter { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + resonance = clamp(resonance, 0, 1); + cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); + const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); + const r = Math.pow(0.5, (resonance + 0.125) / 0.125); + const mrc = 1 - r * c; + this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf + this.s1 = mrc * this.s1 + c * this.s0; // lpf + return this.s1; // return lpf by default + } +} + +class DJFProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [{ name: 'value', defaultValue: 0.5 }]; + } + + constructor() { + super(); + this.filters = [new TwoPoleFilter(), new TwoPoleFilter()]; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + + const hasInput = !(input[0] === undefined); + this.started = hasInput; + + const value = clamp(parameters.value[0], 0, 1); + let filterType = 'none'; + let cutoff; + let v = 1; + if (value > 0.51) { + filterType = 'hipass'; + v = (value - 0.5) * 2; + } else if (value < 0.49) { + filterType = 'lopass'; + v = value * 2; + } + cutoff = Math.pow(v * 11, 4); + + for (let i = 0; i < input.length; i++) { + for (let n = 0; n < blockSize; n++) { + if (filterType == 'none') { + output[i][n] = input[i][n]; + } else { + this.filters[i].update(input[i][n], cutoff, 0.1); + if (filterType === 'lopass') { + output[i][n] = this.filters[i].s1; + } else if (filterType === 'hipass') { + output[i][n] = input[i][n] - this.filters[i].s1; + } else { + output[i][n] = input[i][n]; + } + } + } + } + return true; + } +} +registerProcessor('djf-processor', DJFProcessor); + function fast_tanh(x) { const x2 = x * x; return (x * (27.0 + x2)) / (27.0 + 9.0 * x2); @@ -286,11 +394,10 @@ class LadderProcessor extends AudioWorkletProcessor { const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); let cutoff = parameters.frequency[0]; - // eslint-disable-next-line no-undef cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; - const k = Math.min(8, resonance * 0.4); + const k = Math.min(8, resonance * 0.13); // drive makeup * resonance volume loss makeup let makeupgain = (1 / drive) * Math.min(1.75, 1 + k); @@ -352,16 +459,6 @@ class DistortProcessor extends AudioWorkletProcessor { registerProcessor('distort-processor', DistortProcessor); // SUPERSAW -function lerp(a, b, n) { - return n * (b - a) + a; -} - -function getUnisonDetune(unison, detune, voiceIndex) { - if (unison < 2) { - return 0; - } - return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); -} class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -413,54 +510,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { }, ]; } - process(input, outputs, params) { - // eslint-disable-next-line no-undef + process(_input, outputs, params) { if (currentTime <= params.begin[0]) { return true; } - // eslint-disable-next-line no-undef if (currentTime >= params.end[0]) { // this.port.postMessage({ type: 'onended' }); return false; } - let frequency = params.frequency[0]; - //apply detune in cents - frequency = frequency * Math.pow(2, params.detune[0] / 1200); const output = outputs[0]; - const voices = params.voices[0]; - const freqspread = params.freqspread[0]; - const panspread = params.panspread[0] * 0.5 + 0.5; - const gain1 = Math.sqrt(1 - panspread); - const gain2 = Math.sqrt(panspread); - for (let n = 0; n < voices; n++) { - const isOdd = (n & 1) == 1; - - //applies unison "spread" detune in semitones - const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12); - let gainL = gain1; - let gainR = gain2; - // invert right and left gain - if (isOdd) { - gainL = gain2; - gainR = gain1; - } - // eslint-disable-next-line no-undef - const dt = freq / sampleRate; - - for (let i = 0; i < output[0].length; i++) { + for (let i = 0; i < output[0].length; i++) { + const detune = pv(params.detune, i); + const voices = pv(params.voices, i); + const freqspread = pv(params.freqspread, i); + const panspread = pv(params.panspread, i) * 0.5 + 0.5; + const gain1 = Math.sqrt(1 - panspread); + const gain2 = Math.sqrt(panspread); + let freq = pv(params.frequency, i); + // Main detuning + freq = applySemitoneDetuneToFrequency(freq, detune / 100); + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + // Individual voice detuning + const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = mod(freqVoice / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); output[0][i] = output[0][i] + v * gainL; output[1][i] = output[1][i] + v * gainR; - this.phase[n] += dt; - - if (this.phase[n] > 1.0) { - this.phase[n] = this.phase[n] - 1; - } + this.phase[n] = wrapPhase(this.phase[n] + dt); } } return true; @@ -469,7 +560,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); -// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file +// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) { @@ -648,3 +739,578 @@ class PhaseVocoderProcessor extends OLAProcessor { } registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); + +// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html +class PulseOscillatorProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.pi = _PI; + this.phi = -this.pi; // phase + this.Y0 = 0; // feedback memories + this.Y1 = 0; + this.PW = this.pi; // pulse width + this.B = 2.3; // feedback coefficient + this.dphif = 0; // filtered phase increment + this.envf = 0; // filtered envelope + } + + static get parameterDescriptors() { + return [ + { + name: 'begin', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + + { + name: 'end', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + + { + name: 'frequency', + defaultValue: 440, + min: Number.EPSILON, + }, + { + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + }, + { + name: 'pulsewidth', + defaultValue: 1, + min: 0, + max: Number.POSITIVE_INFINITY, + }, + ]; + } + + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + if (currentTime <= params.begin[0]) { + return true; + } + if (currentTime >= params.end[0]) { + return false; + } + const output = outputs[0]; + let env = 1, + dphi; + + for (let i = 0; i < (output[0].length ?? 0); i++) { + const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi; + const detune = getParamValue(i, params.detune); + const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); + + dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment + this.dphif += 0.1 * (dphi - this.dphif); + + env *= 0.9998; // exponential decay envelope + this.envf += 0.1 * (env - this.envf); + + // Feedback coefficient control + this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation + if (this.B < 0) this.B = 0; + + // Waveform generation (half-Tomisawa oscillators) + this.phi += this.dphif; // phase increment + if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping + + // First half-Tomisawa generator + let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation + this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter + + // Second half-Tomisawa generator (with phase offset for pulse width) + let out1 = Math.cos(this.phi + this.B * this.Y1 + pw); + this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter + + for (let o = 0; o < output.length; o++) { + // Combination of both oscillators with envelope applied + output[o][i] = 0.15 * (out0 - out1) * this.envf; + } + } + + return true; // keep the audio processing going + } +} + +registerProcessor('pulse-oscillator', PulseOscillatorProcessor); + +/** BYTE BEATS */ +const chyx = { + /*bit*/ bitC: function (x, y, z) { + return x & y ? z : 0; + }, + /*bit reverse*/ br: function (x, size = 8) { + if (size > 32) { + throw new Error('br() Size cannot be greater than 32'); + } else { + let result = 0; + for (let idx = 0; idx < size - 0; idx++) { + result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1))); + } + return result; + } + }, + /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) { + return Math.sin(x / (128 / Math.PI)); + }, + /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) { + return Math.cos(x / (128 / Math.PI)); + }, + /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) { + return Math.tan(x / (128 / Math.PI)); + }, + /*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) { + return X.test(t.toString(2)); + }, +}; + +// Create shortened Math functions +let mathParams, byteBeatHelperFuncs; +function getByteBeatFunc(codetext) { + if ((mathParams || byteBeatHelperFuncs) == null) { + mathParams = Object.getOwnPropertyNames(Math); + byteBeatHelperFuncs = mathParams.map((k) => Math[k]); + const chyxNames = Object.getOwnPropertyNames(chyx); + const chyxFuncs = chyxNames.map((k) => chyx[k]); + mathParams.push('int', 'window', ...chyxNames); + byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs); + } + return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs); +} + +class ByteBeatProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.port.onmessage = (event) => { + let { codeText } = event.data; + const { byteBeatStartTime } = event.data; + if (byteBeatStartTime != null) { + this.t = 0; + this.initialOffset = Math.floor(byteBeatStartTime); + } + + //Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important + //Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%"))) + codeText = codeText + .trim() + .replace( + /^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/, + (match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')), + ); + + this.func = getByteBeatFunc(codeText); + }; + this.initialOffset = null; + this.t = null; + this.func = null; + } + + static get parameterDescriptors() { + return [ + { + name: 'begin', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + { + name: 'frequency', + defaultValue: 440, + min: Number.EPSILON, + }, + { + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + }, + { + name: 'end', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + ]; + } + + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + if (currentTime <= params.begin[0]) { + return true; + } + if (currentTime >= params.end[0]) { + return false; + } + if (this.t == null) { + this.t = params.begin[0] * sampleRate; + } + const output = outputs[0]; + for (let i = 0; i < output[0].length; i++) { + const detune = getParamValue(i, params.detune); + const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); + let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset; + const funcValue = this.func(local_t); + let signal = (funcValue & 255) / 127.5 - 1; + const out = signal * 0.2; + for (let c = 0; c < output.length; c++) { + //prevent speaker blowout via clipping if threshold exceeds + output[c][i] = clamp(out, -0.4, 0.4); + } + this.t = this.t + 1; + } + + return true; // keep the audio processing going + } +} + +registerProcessor('byte-beat-processor', ByteBeatProcessor); + +export const WarpMode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +function hash32(u) { + u = u + 0x7ed55d16 + (u << 12); + u = u ^ 0xc761c23c ^ (u >>> 19); + u = u + 0x165667b1 + (u << 5); + u = (u + 0xd3a2646c) ^ (u << 9); + u = u + 0xfd7046c5 + (u << 3); + u = u ^ 0xb55a4f09 ^ (u >>> 16); + return u >>> 0; +} +const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000; + +function bitReverse(i, n) { + let r = 0; + for (let b = 0; b < n; b++) { + r = (r << 1) | (i & 1); + i >>>= 1; + } + return r; +} + +function noise(x) { + const i = Math.floor(x), + f = x - i; + const a = hash01(i), + b = hash01(i + 1); + return a + (b - a) * f; +} + +function brownian(x, oct = 4) { + let amp = 0.5, + sum = 0, + norm = 0, + freq = 1; + for (let o = 0; o < oct; o++) { + sum += amp * noise(x * freq); + norm += amp; + amp *= 0.5; + freq *= 2; + } + return (sum / norm) * 2 - 1; +} + +const tablesCache = {}; +class WavetableOscillatorProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, + { name: 'detune', defaultValue: 0.18 }, + { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'warpMode', defaultValue: 0 }, + { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, + { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, + { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, + ]; + } + + constructor(options) { + super(options); + this.frameLen = 0; + this.numFrames = 0; + this.phase = []; + this.invSR = 1 / sampleRate; + + this.port.onmessage = (e) => { + const { type, payload } = e.data || {}; + if (type === 'table') { + const key = payload.key; + this.frameLen = payload.frameLen; + if (!tablesCache[key]) { + const tables = [payload.frames]; + let table = tables[0]; + for (let level = 1; level < 1; level++) { + const nextLen = table.length >> 1; + const nextTable = table.map((frame) => { + const avg = new Float32Array(nextLen); + for (let i = 0; i < nextLen; i++) { + avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; + } + return avg; + }); + tables.push(nextTable); + table = nextTable; + if (nextLen <= 32) break; + } + tablesCache[key] = tables; + } + this.tables = tablesCache[key]; + this.numFrames = this.tables[0].length; + } + }; + } + + _mirror(x) { + return 1 - Math.abs(2 * x - 1); + } + + _toBits(amt, min = 2, max = 12) { + const b = max + (min - max) * amt; + return { b, n: Math.round(Math.pow(2, b)) }; + } + + _warpPhase(phase, amt, mode) { + switch (mode) { + case WarpMode.NONE: { + return phase; + } + case WarpMode.ASYM: { + const a = 0.01 + 0.99 * amt; + return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a); + } + case WarpMode.MIRROR: { + // Asym, then mirror + return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM)); + } + case WarpMode.BENDP: { + return Math.pow(phase, 1 + 3 * amt); + } + case WarpMode.BENDM: { + return Math.pow(phase, 1 / (1 + 3 * amt)); + } + case WarpMode.BENDMP: { + return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2); + } + case WarpMode.SYNC: { + const syncRatio = Math.pow(16, amt * amt); + return (phase * syncRatio) % 1; + } + case WarpMode.QUANT: { + const { n } = this._toBits(amt); + return ffloor(phase * n) / n; + } + case WarpMode.FOLD: { + const K = 7; + const k = 1 + Math.max(1, Math.round(K * amt)); + return Math.abs(frac(k * phase) - 0.5) * 2; + } + case WarpMode.PWM: { + const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); + if (phase < w) return (phase / w) * 0.5; + return 0.5 + ((phase - w) / (1 - w)) * 0.5; + } + case WarpMode.ORBIT: { + const depth = 0.5 * amt; + const n = 3; + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.SPIN: { + const depth = 0.5 * amt; + const { n } = this._toBits(amt, 1, 6); + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.CHAOS: { + const r = 3.7 + 0.3 * amt; + const logistic = r * phase * (1 - phase); + return clamp((1 - amt) * phase + amt * logistic, 0, 1); + } + case WarpMode.PRIMES: { + const isPrime = (n) => { + if (n < 2) return false; + if (n % 2 === 0) return n === 2; + for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false; + return true; + }; + let { n } = this._toBits(amt, 3); + while (!isPrime(n)) n++; + return ffloor(phase * n) / n; + } + case WarpMode.BINARY: { + let { b } = this._toBits(amt, 3); + b = Math.round(b); + const n = 1 << b; + const idx = ffloor(phase * n); + const ridx = bitReverse(idx, b); + return ridx / n; + } + case WarpMode.MODULAR: { + const { n } = this._toBits(amt); + const depth = 0.5 * amt; + const jump = frac(phase * n) / n; + return frac(phase + depth * jump); + } + case WarpMode.BROWNIAN: { + const disp = 0.25 * amt * brownian(64 * phase, 4); + return frac(phase + disp); + } + case WarpMode.RECIPROCAL: { + const g = 2 + 4 * amt; + const num = phase * g; + const den = phase + (1 - phase) * g; + const y = den > 1e-12 ? num / den : 0; + return clamp(y, 0, 1); + } + case WarpMode.WORMHOLE: { + const gap = clamp(0.8 * amt, 0, 1); + const a = 0.5 * (1 - gap); + const b = 0.5 * (1 + gap); + if (phase < a) return (phase / a) * 0.5; + if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b)); + return 0.5; + } + case WarpMode.LOGISTIC: { + let x = phase; + const r = 3.6 + 0.4 * amt; + const iters = 1 + Math.round(2 * amt); + for (let i = 0; i < iters; i++) x = r * x * (1 - x); + return clamp(x, 0, 1); + } + case WarpMode.SIGMOID: { + const k = 1 + 10 * amt; + const x = phase - 0.5; + const y = 1 / (1 + Math.exp(-k * x)); + const y0 = 1 / (1 + Math.exp(0.5 * k)); + const y1 = 1 / (1 + Math.exp(-0.5 * k)); + return (y - y0) / (y1 - y0); + } + case WarpMode.FRACTAL: { + const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt; + return frac(phase + d); + } + case WarpMode.FLIP: { + return phase; + } + default: + return phase; + } + } + + _sampleFrame(frame, phase) { + const len = frame.length; + const pos = phase * len; + const i = pos | 0; + const frac = pos - i; + const a = frame[i]; + const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + const b = frame[i1]; + return a + (b - a) * frac; + } + + _chooseMip(dphi) { + const approxHarm = clamp(dphi, 1e-6, 64); + let level = 0; + while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { + level++; + } + return level; + } + + process(_inputs, outputs, parameters) { + if (currentTime >= parameters.end[0]) { + return false; + } + if (currentTime <= parameters.begin[0]) { + return true; + } + const outL = outputs[0][0]; + const outR = outputs[0][1] || outputs[0][0]; + if (!this.tables) { + outL.fill(0); + if (outR !== outL) outR.set(outL); + return true; + } + for (let i = 0; i < outL.length; i++) { + const detune = pv(parameters.detune, i); + const tablePos = clamp(pv(parameters.position, i), 0, 1); + const idx = tablePos * (this.numFrames - 1); + const fIdx = idx | 0; + const frac = idx - fIdx; + const warpAmount = clamp(pv(parameters.warp, i), 0, 1); + const warpMode = pv(parameters.warpMode, i); + const voices = pv(parameters.voices, i); + const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); + const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; + const gain1 = Math.sqrt(0.5 - 0.5 * spread); + const gain2 = Math.sqrt(0.5 + 0.5 * spread); + let f = pv(parameters.frequency, i); + f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune + const normalizer = 1 / Math.sqrt(voices); + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const dPhase = fVoice * this.invSR; + const level = this._chooseMip(dPhase); + const table = this.tables[level]; + + // warp phase then sample + this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; + const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); + const s0 = this._sampleFrame(table[fIdx], ph); + const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); + let s = s0 + (s1 - s0) * frac; + if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { + s = -s; + } + outL[i] += s * gainL * normalizer; + outR[i] += s * gainR * normalizer; + this.phase[n] = wrapPhase(this.phase[n] + dPhase); + } + } + return true; + } +} + +registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore new file mode 100644 index 000000000..d21cbdf3e --- /dev/null +++ b/packages/supradough/.gitignore @@ -0,0 +1 @@ +pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md new file mode 100644 index 000000000..a8cfa84b3 --- /dev/null +++ b/packages/supradough/README.md @@ -0,0 +1,3 @@ +# supradough + +platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs new file mode 100644 index 000000000..bd4b530b3 --- /dev/null +++ b/packages/supradough/dough-export.mjs @@ -0,0 +1,123 @@ +// this is a poc of how a pattern can be rendered as a wav file using node +// run via: node dough-export.mjs +import fs from 'node:fs'; +import WavEncoder from 'wav-encoder'; +import { evalScope } from '@strudel/core'; +import { miniAllStrings } from '@strudel/mini'; +import { Dough } from './dough.mjs'; + +await evalScope( + import('@strudel/core'), + import('@strudel/mini'), + import('@strudel/tonal'), + // import('@strudel/tonal'), +); + +miniAllStrings(); // allows using single quotes for mini notation / skip transpilation + +let sampleRate = 48000, + cps = 0.4; + +/* await doughsamples('github:eddyflux/crate'); +await doughsamples('github:eddyflux/wax'); */ + +let pat = note('c,eb,g,') + .s('sine') + .press() + .add(note(24)) + .fmi(3) + .fmh(5.01) + .dec(0.4) + .delay('.6:<.12 .22>:.8') + .jux(press) + .rarely(add(note('12'))) + .lpf(400) + .lpq(0.2) + .lpd(0.4) + .lpenv(3) + .fmdecay(0.4) + .fmenv(1) + .postgain(0.6) + .stack(s('*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2)) + .stack( + s('[- white@3]*2') + .dec(0.4) + .hpf('<2000!3 <4000 8000>>*4') + .hpq(0.6) + .ply('<1 2>*4') + .postgain(0.5) + .delay(0.5) + .jux(rev) + .lpf(5000), + ) + .stack( + note('*2') + .s('square') + .lpf(sine.range(100, 300).slow(4)) + .lpe(1) + .segment(8) + .lpd(0.3) + .lpq(0.2) + .dec(0.2) + .speed('<1 2>') + .ply('<1 2>') + .postgain(1), + ) + .stack( + chord('') + .voicing() + .s('') + .clip(1) + .rel(0.4) + .vib('4:.2') + .gain(0.7) + .hpf(1200) + .fm(0.5) + .att(1) + .lpa(0.5) + .lpf(200) + .lpenv(4) + .chorus(0.8), + ) + .slow(1 / cps); + +let cycles = 30; +let seconds = cycles + 1; // 1s release tail +const haps = pat.queryArc(0, cycles); + +const dough = new Dough(sampleRate); + +console.log('spawn voices...'); +haps.forEach((hap) => { + hap.value._begin = Number(hap.whole.begin); + hap.value._duration = hap.duration /* / cps */; + dough.scheduleSpawn(hap.value); +}); +console.log(`render ${seconds}s long buffer, each dot is 1 second:`); +const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)]; +let t = performance.now(); +while (dough.t <= buffers[0].length) { + dough.update(); + buffers[0][dough.t] = dough.out[0]; + buffers[1][dough.t] = dough.out[1]; + if (dough.t % sampleRate === 0) { + process.stdout.write('.'); + } +} +const took = (performance.now() - t) / 1000; +const load = (took / seconds) * 100; +const speed = (seconds / took).toFixed(2); +console.log(''); +console.log(`done! +rendered ${seconds}s in ${took.toFixed(2)}s +speed: ${speed}x +load: ${load.toFixed(2)}%`); + +const patternAudio = { + sampleRate, + channelData: buffers, +}; + +WavEncoder.encode(patternAudio).then((buffer) => { + fs.writeFileSync('pattern.wav', new Float32Array(buffer)); +}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs new file mode 100644 index 000000000..18c316b2d --- /dev/null +++ b/packages/supradough/dough-worklet.mjs @@ -0,0 +1,39 @@ +import { Dough } from './dough.mjs'; + +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); + +class DoughProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.dough = new Dough(sampleRate, currentTime); + this.port.onmessage = (event) => { + if (event.data.spawn) { + this.dough.scheduleSpawn(event.data.spawn); + } else if (event.data.sample) { + this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); + } else if (event.data.samples) { + event.data.samples.forEach(([name, channels, sampleRate]) => { + this.dough.loadSample(name, channels, sampleRate); + }); + } else { + console.log('unrecognized event type', event.data); + } + }; + } + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + const output = outputs[0]; + for (let i = 0; i < output[0].length; i++) { + this.dough.update(); + for (let c = 0; c < output.length; c++) { + //prevent speaker blowout via clipping if threshold exceeds + output[c][i] = clamp(this.dough.out[c], -1, 1); + } + } + return true; // keep the audio processing going + } +} + +registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs new file mode 100644 index 000000000..1cb9241ad --- /dev/null +++ b/packages/supradough/dough.mjs @@ -0,0 +1,1116 @@ +// this is dough, the superdough without dependencies +// @ts-check +// @ts-ignore ignore next line because sampleRate is unknown +const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; +const PI_DIV_SR = Math.PI / SAMPLE_RATE; +const ISR = 1 / SAMPLE_RATE; + +let gainCurveFunc = (val) => Math.pow(val, 2); + +function applyGainCurve(val) { + return gainCurveFunc(val); +} + +/** + * Equal Power Crossfade function. + * Smoothly transitions between signals A and B, maintaining consistent perceived loudness. + * + * @param {number} a - Signal A (can be a single value or an array value in buffer processing). + * @param {number} b - Signal B (can be a single value or an array value in buffer processing). + * @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). + * @returns {number} Crossfaded output value. + */ +function crossfade(a, b, m) { + const aGain = Math.sin((1 - m) * 0.5 * Math.PI); + const bGain = Math.sin(m * 0.5 * Math.PI); + return a * aGain + b * bGain; +} + +// function setGainCurve(newGainCurveFunc) { +// gainCurveFunc = newGainCurveFunc; +// } +// https://garten.salat.dev/audio-DSP/oscillators.html +export class SineOsc { + phase = 0; + update(freq) { + const value = Math.sin(this.phase * 2 * Math.PI); + this.phase = (this.phase + freq / SAMPLE_RATE) % 1; + return value; + } +} + +export class ZawOsc { + phase = 0; + update(freq) { + this.phase += ISR * freq; + return (this.phase % 1) * 2 - 1; + } +} + +function polyBlep(t, dt) { + // 0 <= t < 1 + if (t < dt) { + t /= dt; + // 2 * (t - t^2/2 - 0.5) + return t + t - t * t - 1; + } + // -1 < t < 0 + if (t > 1 - dt) { + t = (t - 1) / dt; + // 2 * (t^2/2 + t + 0.5) + return t * t + t + t + 1; + } + // 0 otherwise + return 0; +} + +export class SawOsc { + constructor(props = {}) { + this.phase = props.phase ?? 0; + } + update(freq) { + const dt = freq / SAMPLE_RATE; + let p = polyBlep(this.phase, dt); + let s = 2 * this.phase - 1 - p; + this.phase += dt; + if (this.phase > 1) { + this.phase -= 1; + } + return s; + } +} + +function getUnisonDetune(unison, detune, voiceIndex) { + if (unison < 2) { + return 0; + } + const lerp = (a, b, n) => { + return n * (b - a) + a; + }; + return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); +} +function applySemitoneDetuneToFrequency(frequency, detune) { + return frequency * Math.pow(2, detune / 12); +} +export class SupersawOsc { + constructor(props = {}) { + //TODO: figure out a good way to pass in these params + this.voices = props.voices ?? 5; + this.freqspread = props.freqspread ?? 0.2; + this.panspread = props.panspread ?? 0.4; + this.phase = new Float32Array(this.voices).map(() => Math.random()); + } + update(freq) { + const gain1 = Math.sqrt(1 - this.panspread); + const gain2 = Math.sqrt(this.panspread); + let sl = 0; + let sr = 0; + for (let n = 0; n < this.voices; n++) { + const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); + const dt = freqAdjusted / SAMPLE_RATE; + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + let p = polyBlep(this.phase[n], dt); + let s = 2 * this.phase[n] - 1 - p; + sl = sl + s * gainL; + sr = sr + s * gainL; + + this.phase[n] += dt; + if (this.phase[n] > 1) { + this.phase[n] -= 1; + } + } + + return sl + sr; + //TODO: make stereo + // return [sl, sr]; + } +} + +export class TriOsc { + phase = 0; + update(freq) { + this.phase += ISR * freq; + let phase = this.phase % 1; + let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5); + return value * 2 - 1; + } +} + +export class TwoPoleFilter { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + resonance = Math.max(resonance, 0); + + cutoff = Math.min(cutoff, 20000); + const c = 2 * Math.sin(cutoff * PI_DIV_SR); + + const r = Math.pow(0.5, (resonance + 0.125) / 0.125); + const mrc = 1 - r * c; + + this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf + this.s1 = mrc * this.s1 + c * this.s0; // lpf + return this.s1; // return lpf by default + } +} + +class PulseOsc { + constructor(phase = 0) { + this.phase = phase; + } + saw(offset, dt) { + let phase = (this.phase + offset) % 1; + let p = polyBlep(phase, dt); + return 2 * phase - 1 - p; + } + update(freq, pw = 0.5) { + const dt = freq / SAMPLE_RATE; + let pulse = this.saw(0, dt) - this.saw(pw, dt); + this.phase = (this.phase + dt) % 1; + return pulse + pw * 2 - 1; + } +} + +// non bandlimited (has aliasing) +export class PulzeOsc { + phase = 0; + update(freq, duty = 0.5) { + this.phase += ISR * freq; + let cyclePos = this.phase % 1; + return cyclePos < duty ? 1 : -1; + } +} + +export class Dust { + update = (density) => (Math.random() < density * ISR ? Math.random() : 0); +} + +export class WhiteNoise { + update() { + return Math.random() * 2 - 1; + } +} + +export class BrownNoise { + constructor() { + this.out = 0; + } + update() { + let white = Math.random() * 2 - 1; + this.out = (this.out + 0.02 * white) / 1.02; + return this.out; + } +} + +export class PinkNoise { + constructor() { + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.b3 = 0; + this.b4 = 0; + this.b5 = 0; + this.b6 = 0; + } + + update() { + const white = Math.random() * 2 - 1; + + this.b0 = 0.99886 * this.b0 + white * 0.0555179; + this.b1 = 0.99332 * this.b1 + white * 0.0750759; + this.b2 = 0.969 * this.b2 + white * 0.153852; + this.b3 = 0.8665 * this.b3 + white * 0.3104856; + this.b4 = 0.55 * this.b4 + white * 0.5329522; + this.b5 = -0.7616 * this.b5 - white * 0.016898; + + const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; + this.b6 = white * 0.115926; + + return pink * 0.11; + } +} + +export class Impulse { + phase = 1; + update(freq) { + this.phase += ISR * freq; + let v = this.phase >= 1 ? 1 : 0; + this.phase = this.phase % 1; + return v; + } +} + +export class ClockDiv { + inSgn = true; + outSgn = true; + clockCnt = 0; + update(clock, factor) { + let curSgn = clock > 0; + if (this.inSgn != curSgn) { + this.clockCnt++; + if (this.clockCnt >= factor) { + this.clockCnt = 0; + this.outSgn = !this.outSgn; + } + } + + this.inSgn = curSgn; + return this.outSgn ? 1 : -1; + } +} + +export class Hold { + value = 0; + trigSgn = false; + update(input, trig) { + if (!this.trigSgn && trig > 0) this.value = input; + this.trigSgn = trig > 0; + return this.value; + } +} + +function lerp(x, y0, y1, exponent = 1) { + if (x <= 0) return y0; + if (x >= 1) return y1; + + let curvedX; + + if (exponent === 0) { + curvedX = x; // linear + } else if (exponent > 0) { + curvedX = Math.pow(x, exponent); // ease-in + } else { + curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out + } + + return y0 + (y1 - y0) * curvedX; +} + +export class ADSR { + constructor(props = {}) { + this.state = 'off'; + this.startTime = 0; + this.startVal = 0; + this.decayCurve = props.decayCurve ?? 1; + } + + update(curTime, gate, attack, decay, susVal, release) { + switch (this.state) { + case 'off': { + if (gate > 0) { + this.state = 'attack'; + this.startTime = curTime; + this.startVal = 0; + } + return 0; + } + case 'attack': { + let time = curTime - this.startTime; + if (time > attack) { + this.state = 'decay'; + this.startTime = curTime; + return 1; + } + return lerp(time / attack, this.startVal, 1, 1); + } + case 'decay': { + let time = curTime - this.startTime; + let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); + if (gate <= 0) { + this.state = 'release'; + this.startTime = curTime; + this.startVal = curVal; + return curVal; + } + if (time > decay) { + this.state = 'sustain'; + this.startTime = curTime; + return susVal; + } + return curVal; + } + case 'sustain': { + if (gate <= 0) { + this.state = 'release'; + this.startTime = curTime; + this.startVal = susVal; + } + return susVal; + } + case 'release': { + let time = curTime - this.startTime; + + if (time > release) { + this.state = 'off'; + return 0; + } + let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); + if (gate > 0) { + this.state = 'attack'; + this.startTime = curTime; + this.startVal = curVal; + } + return curVal; + } + } + throw 'invalid envelope state'; + } +} + +/* + impulse(1).ad(.1).mul(sine(200)) +.add(x=>x.delay(.1).mul(.8)) +.out()*/ +const MAX_DELAY_TIME = 10; +export class PitchDelay { + lpf = new TwoPoleFilter(); + constructor(_props = {}) { + this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); + this.writeIdx = 0; + this.readIdx = 0; + this.numSamples = 0; + } + write(s, delayTime) { + // Calculate how far in the past to read + this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); + this.writeIdx = (this.writeIdx + 1) % this.numSamples; + this.buffer[this.writeIdx] = s; + this.readIdx = this.writeIdx - this.numSamples + 1; + + // If past the start of the buffer, wrap around (Q: is this possible?) + if (this.readIdx < 0) this.readIdx += this.numSamples; + } + update(input, delayTime, speed = 1) { + this.write(input, delayTime); + let index = this.readIdx; + if (speed < 0) { + index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples); + } else { + index = Math.floor(this.readIdx * speed) % this.numSamples; + } + const s = this.lpf.update(this.buffer[index], 0.9, 0); + + return s; + } +} + +export class Delay { + writeIdx = 0; + readIdx = 0; + buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0) + write(s, delayTime) { + this.writeIdx = (this.writeIdx + 1) % this.buffer.length; + this.buffer[this.writeIdx] = s; + // Calculate how far in the past to read + let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); + this.readIdx = this.writeIdx - numSamples; + // If past the start of the buffer, wrap around + if (this.readIdx < 0) this.readIdx += this.buffer.length; + } + update(input, delayTime) { + this.write(input, delayTime); + return this.buffer[this.readIdx]; + } +} +//TODO: Figure out why clicking at the start off the buffer +export class Chorus { + delay = new Delay(); + modulator = new TriOsc(); + update(input, mix, delayTime, modulationFreq, modulationDepth) { + const m = this.modulator.update(modulationFreq) * modulationDepth; + const c = this.delay.update(input, delayTime * (1 + m)); + return crossfade(input, c, mix); + } +} + +export class Fold { + update(input = 0, rate = 0) { + if (rate < 0) rate = 0; + rate = rate + 1; + input = input * rate; + return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25); + } +} + +export class Lag { + lagUnit = 4410; + s = 0; + update(input, rate) { + // Remap so the useful range is around [0, 1] + rate = rate * this.lagUnit; + if (rate < 1) rate = 1; + this.s += (1 / rate) * (input - this.s); + return this.s; + } +} + +export class Slew { + last = 0; + update(input, up, dn) { + const upStep = up * ISR; + const downStep = dn * ISR; + let delta = input - this.last; + if (delta > upStep) { + delta = upStep; + } else if (delta < -downStep) { + delta = -downStep; + } + this.last += delta; + return this.last; + } +} + +// overdrive style distortion (adapted from noisecraft) currently unused +export function applyDistortion(x, amount) { + amount = Math.min(Math.max(amount, 0), 1); + amount -= 0.01; + var k = (2 * amount) / (1 - amount); + var y = ((1 + k) * x) / (1 + k * Math.abs(x)); + return y; +} + +export class Sequence { + clockSgn = true; + step = 0; + first = true; + update(clock, ...ins) { + if (!this.clockSgn && clock > 0) { + this.step = (this.step + 1) % ins.length; + this.clockSgn = clock > 0; + return 0; // set first sample to zero to retrigger gates on step change... + } + this.clockSgn = clock > 0; + return ins[this.step]; + } +} + +// sample rate bit crusher +export class Coarse { + hold = 0; + t = 0; + update(input, coarse) { + if (this.t++ % coarse === 0) { + this.t = 0; + this.hold = input; + } + return this.hold; + } +} + +// amplitude bit crusher +export class Crush { + update(input, crush) { + crush = Math.max(1, crush); + const x = Math.pow(2, crush - 1); + return Math.round(input * x) / x; + } +} + +// this is the distort from superdough +export class Distort { + update(input, distort = 0, postgain = 1) { + postgain = Math.max(0.001, Math.min(1, postgain)); + const shape = Math.expm1(distort); + return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; + } +} +// distortion could be expressed as a function, because it's stateless + +export class BufferPlayer { + static samples = new Map(); // string -> { channels, sampleRate } + buffer; // Float32Array + sampleRate; + pos = 0; + sampleFreq = note2freq(); + constructor(buffer, sampleRate, normalize) { + this.buffer = buffer; + this.sampleRate = sampleRate; + this.duration = this.buffer.length / this.sampleRate; + this.speed = SAMPLE_RATE / this.sampleRate; + if (normalize) { + // this will make the buffer last 1s if freq = sampleFreq + // it's useful to loop samples (e.g. fit function) + this.speed *= this.duration; + } + } + update(freq) { + if (this.pos >= this.buffer.length) { + return 0; + } + const speed = (freq / this.sampleFreq) * this.speed; + let s = this.buffer[Math.floor(this.pos)]; + this.pos = this.pos + speed; + return s; + } +} + +export function _rangex(sig, min, max) { + let logmin = Math.log(min); + let range = Math.log(max) - logmin; + const unipolar = (sig + 1) / 2; + return Math.exp(unipolar * range + logmin); +} + +// duplicate +export const getADSR = (params, curve = 'linear', defaultValues) => { + const envmin = curve === 'exponential' ? 0.001 : 0.001; + const releaseMin = 0.01; + const envmax = 1; + const [a, d, s, r] = params; + if (a == null && d == null && s == null && r == null) { + return defaultValues ?? [envmin, envmin, envmax, releaseMin]; + } + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; + return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; +}; + +let shapes = { + sine: SineOsc, + saw: SawOsc, + zaw: ZawOsc, + sawtooth: SawOsc, + zawtooth: ZawOsc, + supersaw: SupersawOsc, + tri: TriOsc, + triangle: TriOsc, + pulse: PulseOsc, + square: PulseOsc, + pulze: PulzeOsc, + dust: Dust, + crackle: Dust, + impulse: Impulse, + white: WhiteNoise, + brown: BrownNoise, + pink: PinkNoise, +}; + +const defaultDefaultValues = { + chorus: 0, + note: 48, + s: 'triangle', + bank: '', + gain: 1, + postgain: 1, + velocity: 1, + density: '.03', + ftype: '12db', + fanchor: 0, + //resonance: 1, // superdough resonance is scaled differently + resonance: 0, + //hresonance: 1, // superdough resonance is scaled differently + hresonance: 0, + // bandq: 1, // superdough resonance is scaled differently + bandq: 0, + channels: [1, 2], + phaserdepth: 0.75, + shapevol: 1, + distortvol: 1, + delay: 0, + byteBeatExpression: '0', + delayfeedback: 0.5, + delayspeed: 1, + delaytime: 0.25, + orbit: 1, + i: 1, + fft: 8, + z: 'triangle', + pan: 0.5, + fmh: 1, + fmenv: 0, // differs from superdough + speed: 1, + pw: 0.5, +}; + +let getDefaultValue = (key) => defaultDefaultValues[key]; + +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 }; +const note2midi = (note, defaultOctave = 3) => { + let [pc, acc = '', oct = ''] = + String(note) + .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) + ?.slice(1) || []; + if (!pc) { + throw new Error('not a note: "' + note + '"'); + } + const chroma = chromas[pc.toLowerCase()]; + const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + const octave = Number(oct || defaultOctave); + return (octave + 1) * 12 + chroma + offset; +}; +const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; +const note2freq = (note) => { + note = note || getDefaultValue('note'); + if (typeof note === 'string') { + note = note2midi(note, 3); // e.g. c3 => 48 + } + return midi2freq(note); +}; + +export class DoughVoice { + /** @type {number} */ + id = 0; + /** @type {number[]} */ + out = [0, 0]; + + /** @type {number | undefined} */ + attack; + /** @type {number | undefined} */ + decay; + /** @type {number | undefined} */ + sustain; + /** @type {number} */ + release; + /** @type {number} */ + _begin; + /** @type {number} */ + _duration; + + /** @type {any} */ + _sound; + /** @type {number} */ + _channels = 1; + /** @type {BufferPlayer[] | undefined} */ + _buffers; + /** @type {string | undefined} */ + unit; + + /** @type {ADSR | undefined} */ + _penv; + /** @type {number | undefined} */ + penv; + /** @type {number | undefined} */ + pattack; + /** @type {number | undefined} */ + pdecay; + /** @type {number | undefined} */ + psustain; + /** @type {number | undefined} */ + prelease; + + /** @type {number | undefined} */ + vib; + + _vib; + /** @type {number | undefined} */ + vibmod; + + /** @type {SineOsc | undefined} */ + _fm; + /** @type {number | undefined} */ + fmh; + /** @type {number | undefined} */ + fmi; + + /** @type {ADSR | undefined} */ + _fmenv; + /** @type {number | undefined} */ + fmattack; + /** @type {number | undefined} */ + fmdecay; + /** @type {number | undefined} */ + fmsustain; + /** @type {number | undefined} */ + fmrelease; + + /** @type {ADSR | undefined} */ + _lpenv; + lpenv; + /** @type {number | undefined} */ + lpattack; + /** @type {number | undefined} */ + lpdecay; + /** @type {number | undefined} */ + lpsustain; + /** @type {number | undefined} */ + lprelease; + + /** @type {ADSR | undefined} */ + _hpenv; + /** @type {number | undefined} */ + hpenv; + /** @type {number | undefined} */ + hpattack; + /** @type {number | undefined} */ + hpdecay; + /** @type {number | undefined} */ + hpsustain; + /** @type {number | undefined} */ + hprelease; + + /** @type {ADSR | undefined} */ + _bpenv; + /** @type {number | undefined} */ + bpenv; + /** @type {number | undefined} */ + bpattack; + /** @type {number | undefined} */ + bpdecay; + /** @type {number | undefined} */ + bpsustain; + /** @type {number | undefined} */ + bprelease; + + /** @type {number | undefined} */ + cutoff; + /** @type {number | undefined} */ + hcutoff; + /** @type {number | undefined} */ + bandf; + /** @type {number | undefined} */ + coarse; + /** @type {number | undefined} */ + crush; + /** @type {number | undefined} */ + distort; + + /** @type {number} */ + freq; + /** @type {string | undefined} */ + note; + + /** @type {TwoPoleFilter[] | null | undefined} */ + _lpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _hpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _bpf; + /** @type {Chorus[] | null | undefined} */ + _chorus; + /** @type {Coarse[] | null | undefined} */ + _coarse; + /** @type {Crush[] | null | undefined} */ + _crush; + /** @type {Distort[] | null | undefined} */ + _distort; + + /** + * @param {DoughVoice} value + */ + constructor(value) { + // mandatory controls + this.freq ??= note2freq(value.note); + this._begin = value._begin; + this._duration = value._duration; + this.release = value.release ?? 0; + // the rest.. we use $ for readability + let $ = this; + Object.assign($, value); + $.s = $.s ?? getDefaultValue('s'); + $.gain = applyGainCurve($.gain ?? getDefaultValue('gain')); + $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity')); + $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain')); + $.density = $.density ?? getDefaultValue('density'); + $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); + $.drive = $.drive ?? 0.69; + $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); + $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); + $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); + $.i = $.i ?? getDefaultValue('i'); + $.chorus = $.chorus ?? getDefaultValue('chorus'); + $.fft = $.fft ?? getDefaultValue('fft'); + $.pan = $.pan ?? getDefaultValue('pan'); + $.orbit = $.orbit ?? getDefaultValue('orbit'); + $.fmenv = $.fmenv ?? getDefaultValue('fmenv'); + $.resonance = $.resonance ?? getDefaultValue('resonance'); + $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); + $.bandq = $.bandq ?? getDefaultValue('bandq'); + $.speed = $.speed ?? getDefaultValue('speed'); + $.pw = $.pw ?? getDefaultValue('pw'); + + [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); + + $._holdEnd = $._begin + $._duration; // needed for gate + $._end = $._holdEnd + $.release + 0.01; // needed for despawn + + if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { + $.s = 'zaw'; // polyblepped saw when fm is applied + } + + if (shapes[$.s]) { + const SourceClass = shapes[$.s]; + $._sound = new SourceClass(); + $._channels = 1; + } else if (BufferPlayer.samples.has($.s)) { + const sample = BufferPlayer.samples.get($.s); + $._buffers = []; + $._channels = sample.channels.length; + for (let i = 0; i < $._channels; i++) { + $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' + } + } else { + console.warn('sound not loaded', $.s); + } + + if ($.penv) { + $._penv = new ADSR({ decayCurve: 4 }); + [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); + } + + if ($.vib) { + $._vib = new SineOsc(); + $.vibmod = $.vibmod ?? getDefaultValue('vibmod'); + } + + if ($.fmi) { + $._fm = new SineOsc(); + $.fmh = $.fmh ?? getDefaultValue('fmh'); + if ($.fmenv) { + $._fmenv = new ADSR({ decayCurve: 2 }); + [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]); + } + } + + // gain envelope + $._adsr = new ADSR({ decayCurve: 2 }); + // delay + $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); + $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); + $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); + $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); + + // filter setup + if ($.lpenv) { + $._lpenv = new ADSR({ decayCurve: 4 }); + [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]); + } + if ($.hpenv) { + $._hpenv = new ADSR({ decayCurve: 4 }); + [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]); + } + if ($.bpenv) { + $._bpenv = new ADSR({ decayCurve: 4 }); + [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); + } + + // channelwise effects setup + $._chorus = $.chorus ? [] : null; + $._lpf = $.cutoff ? [] : null; + $._hpf = $.hcutoff ? [] : null; + $._bpf = $.bandf ? [] : null; + $._coarse = $.coarse ? [] : null; + $._crush = $.crush ? [] : null; + $._distort = $.distort ? [] : null; + for (let i = 0; i < this._channels; i++) { + $._lpf?.push(new TwoPoleFilter()); + $._hpf?.push(new TwoPoleFilter()); + $._bpf?.push(new TwoPoleFilter()); + $._chorus?.push(new Chorus()); + $._coarse?.push(new Coarse()); + $._crush?.push(new Crush()); + $._distort?.push(new Distort()); + } + } + update(t) { + if (!this._sound && !this._buffers) { + return 0; + } + let gate = Number(t >= this._begin && t <= this._holdEnd); + + let freq = this.freq * this.speed; + + // frequency modulation + if (this._fm && this.fmh !== undefined && this.fmi !== undefined) { + let fmi = this.fmi; + if (this._fmenv) { + const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); + fmi = this.fmenv * env * fmi; + } + const modfreq = freq * this.fmh; + const modgain = modfreq * fmi; + freq = freq + this._fm.update(modfreq) * modgain; + } + + // vibrato + if (this._vib && this.vibmod !== undefined) { + freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); + } + + // pitch envelope + if (this._penv && this.penv !== undefined) { + const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); + freq = freq + env * this.penv; + } + + // filters + let lpf = this.cutoff; + if (lpf !== undefined && this._lpenv) { + const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); + lpf = this.lpenv * env * lpf + lpf; + } + let hpf = this.hcutoff; + if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) { + const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); + hpf = 2 ** this.hpenv * env * hpf + hpf; + } + let bpf = this.bandf; + if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) { + const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); + bpf = 2 ** this.bpenv * env * bpf + bpf; + } + // gain envelope + const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); + + // channelwise dsp + for (let i = 0; i < this._channels; i++) { + // sound source + if (this._sound && this.s === 'pulse') { + this.out[i] = this._sound.update(freq, this.pw); + } else if (this._sound) { + this.out[i] = this._sound.update(freq); + } else if (this._buffers) { + this.out[i] = this._buffers[i].update(freq); + } + this.out[i] = this.out[i] * this.gain * this.velocity; + if (this._chorus) { + const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11); + this.out[i] = c + this.out[i]; + } + + if (this._lpf) { + this._lpf[i].update(this.out[i], lpf, this.resonance); + this.out[i] = this._lpf[i].s1; + } + if (this._hpf) { + this._hpf[i].update(this.out[i], hpf, this.hresonance); + this.out[i] = this.out[i] - this._hpf[i].s1; + } + if (this._bpf) { + this._bpf[i].update(this.out[i], bpf, this.bandq); + this.out[i] = this._bpf[i].s0; + } + if (this._coarse) { + this.out[i] = this._coarse[i].update(this.out[i], this.coarse); + } + if (this._crush) { + this.out[i] = this._crush[i].update(this.out[i], this.crush); + } + if (this._distort) { + this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol); + } + this.out[i] = this.out[i] * env; + this.out[i] = this.out[i] * this.postgain; + if (!this._buffers) { + this.out[i] = this.out[i] * 0.2; // turn down waveform + } + } + if (this._channels === 1) { + this.out[1] = this.out[0]; + } + if (this.pan !== 0.5) { + const panpos = (this.pan * Math.PI) / 2; + this.out[0] = this.out[0] * Math.cos(panpos); + this.out[1] = this.out[1] * Math.sin(panpos); + } + } +} + +// this class is the interface to the "outer world" +// it handles spawning and despawning of DoughVoice's +export class Dough { + voices = []; // DoughVoice[] + vid = 0; + q = []; + out = [0, 0]; + delaysend = [0, 0]; + delaytime = getDefaultValue('delaytime'); + delayfeedback = getDefaultValue('delayfeedback'); + delayspeed = getDefaultValue('delayspeed'); + t = 0; + // sampleRate: number, currentTime: number (seconds) + constructor(sampleRate = 48000, currentTime = 0) { + this.sampleRate = sampleRate; + this.t = Math.floor(currentTime * sampleRate); // samples + // console.log('init dough', this.sampleRate, this.t); + this._delayL = new Delay(); + this._delayR = new Delay(); + } + loadSample(name, channels, sampleRate) { + BufferPlayer.samples.set(name, { channels, sampleRate }); + } + scheduleSpawn(value) { + if (value._begin === undefined) { + throw new Error('[dough]: scheduleSpawn expected _begin to be set'); + } + if (value._duration === undefined) { + throw new Error('[dough]: scheduleSpawn expected _duration to be set'); + } + value.sampleRate = this.sampleRate; + // convert seconds to samples + const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs + this.schedule({ time, type: 'spawn', arg: value }); + } + spawn(value) { + value.id = this.vid++; + const voice = new DoughVoice(value); + this.voices.push(voice); + // console.log('spawn', voice.id, 'voices:', this.voices.length); + // schedule removal + const endTime = Math.ceil(voice._end * this.sampleRate); + this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); + } + despawn(vid) { + this.voices = this.voices.filter((v) => v.id !== vid); + // console.log('despawn', vid, 'voices:', this.voices.length); + } + // schedules a function call with a single argument + // msg = {time:number,type:string, arg: any} + // the Dough method "type" will be called with "arg" at "time" + schedule(msg) { + if (!this.q.length) { + // if empty, just push + this.q.push(msg); + return; + } + // not empty + // find index where msg.time fits in + let i = 0; + while (i < this.q.length && this.q[i].time < msg.time) { + i++; + } + // this ensures q stays sorted by time, so we only need to check q[0] + this.q.splice(i, 0, msg); + } + // maybe update should be called once per block instead for perf reasons? + update() { + // go over q + while (this.q.length > 0 && this.q[0].time <= this.t) { + // console.log('schedule', this.q[0]); + // trigger due messages. q is sorted, so we only need to check q[0] + this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method + this.q.shift(); + } + // add active voices + this.out[0] = 0; + this.out[1] = 0; + for (let v = 0; v < this.voices.length; v++) { + this.voices[v].update(this.t / this.sampleRate); + this.out[0] += this.voices[v].out[0]; + this.out[1] += this.voices[v].out[1]; + if (this.voices[v].delay) { + this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay; + this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay; + this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice + this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice + this.delayfeedback = this.voices[v].delayfeedback; + } + } + // todo: how to change delaytime / delayfeedback from a voice? + const delayL = this._delayL.update(this.delaysend[0], this.delaytime); + const delayR = this._delayR.update(this.delaysend[1], this.delaytime); + + this.delaysend[0] = delayL * this.delayfeedback; + this.delaysend[1] = delayR * this.delayfeedback; + this.out[0] += delayL; + this.out[1] += delayR; + this.t++; + } +} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs new file mode 100644 index 000000000..cb132fb3f --- /dev/null +++ b/packages/supradough/index.mjs @@ -0,0 +1,5 @@ +// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build) +import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!) + +export * from './dough.mjs'; +export const workletUrl = _workletUrl; diff --git a/packages/supradough/package.json b/packages/supradough/package.json new file mode 100644 index 000000000..7e465c0a9 --- /dev/null +++ b/packages/supradough/package.json @@ -0,0 +1,37 @@ +{ + "name": "supradough", + "version": "1.2.3", + "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "devDependencies": { + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*", + "wav-encoder": "^1.3.0" + }, + "dependencies": {} +} diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 94507aa9b..960b09001 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -1,11 +1,12 @@ { "name": "@strudel/tidal", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Tidal Code interpreter", "module": "tidal.mjs", "repository": { "type": "git", - "url": "git+https://github.com/felixroos/hs2js.git" + "url": "git+https://codeberg.org/uzu/strudel/src/branch/main/packages/tidal" }, "keywords": [ "haskell", @@ -14,9 +15,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/tree/main/packages/hs2js", + "homepage": "https://codeberg.org/uzu/strudel/src/branch/main/packages/hs2js", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index e642fa4e3..1461bdc8e 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.1.0", + "version": "1.2.4", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@tonaljs/tonal": "^4.10.0", diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..5f61b05a9 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -1,79 +1,136 @@ /* tonal.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ // import { strict as assert } from 'assert'; import '../tonal.mjs'; // need to import this to add prototypes -import { pure, n, seq, note } from '@strudel/core'; +import { pure, n, seq, note, noteToMidi } from '@strudel/core'; import { describe, it, expect } from 'vitest'; import { mini } from '../../mini/mini.mjs'; describe('tonal', () => { - it('Should run tonal functions ', () => { - expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + describe('scaleTranspose', () => { + it('transposes notes by scale degrees', () => { + expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + }); }); - it('scale with plain values', () => { - expect( - seq(0, 1, 2) - .scale('C major') - .note() - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); + describe('scale', () => { + it('converts plain values', () => { + expect( + seq(0, 1, 2) + .scale('C major') + .note() + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values', () => { + expect( + n(seq(0, 1, 2)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale('C:major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (no tonic)', () => { + expect( + n(seq(0, 1, 2)) + .scale('major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (explicit mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale(mini('C:major')) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts decorated n values', () => { + expect( + n(seq('0b', '1#', '-2', '3##', '4bb')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']); + }); + it('produces silence for mixed sharps and flats', () => { + expect( + n(seq('0b#', '1#b', '2#b#')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual([]); + }); + it('snaps notes (upwards) to scale', () => { + const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; + const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; + + expect( + note(seq(inputNotes)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); + it('snaps notes to the correct octave', () => { + const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8']; + const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8']; + + expect( + note(seq(inputNotes)) + .scale('A# minor') // A#, B#, C#, D#, E#, F#, G# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); + it('handles scale names provided with colons', () => { + const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb']; + const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3']; + + expect( + note(seq(inputNotes)) + .scale('F#:pentatonic') // F#, G#, A#, C#, and D# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); }); - it('scale with n values', () => { - expect( - n(0, 1, 2) - .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with colon', () => { - expect( - n(0, 1, 2) - .scale('C:major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with mininotation colon', () => { - expect( - n(0, 1, 2) - .scale(mini('C:major')) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('transposes note numbers with interval numbers', () => { - expect( - note(40, 40, 40) - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 41, 42]); - expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); - }); - it('transposes note numbers with interval strings', () => { - expect( - note(40, 40, 40) - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 42, 43]); - expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); - }); - it('transposes note strings with interval numbers', () => { - expect( - note('c', 'c', 'c') - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'Db', 'D']); - expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); - }); - it('transposes note strings with interval strings', () => { - expect( - note('c', 'c', 'c') - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'D', 'Eb']); - expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + describe('transpose', () => { + it('transposes note numbers with interval numbers', () => { + expect( + note(seq(40, 40, 40)) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 41, 42]); + expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); + }); + it('transposes note numbers with interval strings', () => { + expect( + note(seq(40, 40, 40)) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 42, 43]); + expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); + }); + it('transposes note strings with interval numbers', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'Db', 'D']); + expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); + }); + it('transposes note strings with interval strings', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'D', 'Eb']); + expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + }); }); }); diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index e1a693579..b53a8f405 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -1,6 +1,6 @@ /* tonleiter.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 78183d228..ae75ab690 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -1,24 +1,33 @@ /* tonal.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; -import { stepInNamedScale } from './tonleiter.mjs'; +import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; +import { noteToMidi } from '../core/util.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; -function scaleStep(step, scale) { - scale = scale.replaceAll(':', ' '); - step = Math.ceil(step); - let { intervals, tonic, empty } = Scale.get(scale); - if ((empty && isNote(scale)) || (!empty && !tonic)) { - throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`); +function getScale(scaleName) { + scaleName = scaleName.replaceAll(':', ' '); + const scale = Scale.get(scaleName); + const { tonic, empty } = scale; + if ((empty && isNote(scaleName)) || (empty && !tonic)) { + throw new Error( + `Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`, + ); } else if (empty) { - throw new Error(`invalid scale "${scale}"`); + throw new Error(`Invalid scale name "${scaleName}"`); } + return scale; +} + +function scaleStep(step, scale) { + step = Math.ceil(step); + let { intervals, tonic } = getScale(scale); tonic = tonic || 'C'; const { pc, oct = 3 } = Note.get(tonic); const octaveOffset = Math.floor(step / intervals.length); @@ -30,8 +39,7 @@ function scaleStep(step, scale) { // transpose note inside scale by offset steps // function scaleOffset(scale: string, offset: number, note: string) { function scaleOffset(scale, offset, note) { - let [tonic, scaleName] = Scale.tokenize(scale); - let { notes } = Scale.get(`${tonic} ${scaleName}`); + let { notes } = getScale(scale); notes = notes.map((note) => Note.get(note).pc); // use only pc! offset = Number(offset); if (isNaN(offset)) { @@ -88,13 +96,14 @@ function scaleOffset(scale, offset, note) { * @returns Pattern * @memberof Pattern * @name transpose + * @synonyms trans * @example * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() * @example * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register('transpose', function (intervalOrSemitones, pat) { +export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -119,10 +128,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); + const targetNote = Note.transpose(note, interval); if (typeof hap.value === 'object') { return hap.withValue(() => ({ ...hap.value, note: targetNote })); } @@ -142,6 +148,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern + * @synonyms scaleTrans, strans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') @@ -149,25 +156,79 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * .note() */ -export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) { - return pat.withHap((hap) => { - if (!hap.context.scale) { - throw new Error('can only use scaleTranspose after .scale'); +export const { scaleTranspose, scaleTrans, strans } = register( + ['scaleTranspose', 'scaleTrans', 'strans'], + function (offset /* : number | string */, pat) { + return pat.withHap((hap) => { + if (!hap.context.scale) { + throw new Error('can only use scaleTranspose after .scale'); + } + if (typeof hap.value === 'object') + return hap.withValue(() => ({ + ...hap.value, + note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), + })); + if (typeof hap.value !== 'string') { + throw new Error('can only use scaleTranspose with notes'); + } + return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); + }); + }, +); + +// Converts a step value, which is a number optionally decorated with sharps and flats, +// to a number and an `offset` number of semitones +function _convertStepToNumberAndOffset(step) { + let asNumber = Number(step); + let offset = 0; + if (isNaN(asNumber)) { + step = String(step); + // Check to see if the step matches the expected format: + // - A number (possibly negative) + // - Some number of sharps or flats (but not both) + const match = /^(-?\d+)(#+|b+)?$/.exec(step); + + if (!match) { + throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`); } - if (typeof hap.value === 'object') - return hap.withValue(() => ({ - ...hap.value, - note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), - })); - if (typeof hap.value !== 'string') { - throw new Error('can only use scaleTranspose with notes'); - } - return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); - }); -}); + asNumber = Number(match[1]); + // These decorations will determine the semitone offset based on the number of + // sharps or flats + const decorations = match[2] || ''; + offset = decorations[0] === '#' ? decorations.length : -decorations.length; + } + return [asNumber, offset]; +} + +let scaleToMidisAndNotes = {}; +// Finds the nearest scale note to `note` +function _getNearestScaleNote(scaleName, note, preferHigher = true) { + let noteMidi = typeof note === 'string' ? noteToMidi(note) : note; + if (scaleToMidisAndNotes[scaleName] === undefined) { + const { intervals, tonic } = getScale(scaleName); + const { pc } = Note.get(tonic); + const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping + const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval)); + const sMidi = sNotes.map(noteToMidi); + // Cache + scaleToMidisAndNotes[scaleName] = [sMidi, sNotes]; + } + const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName]; + const rootMidi = scaleMidis[0]; + const octaveDiff = Math.floor((noteMidi - rootMidi) / 12); + const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff); + const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher); + const noteMatch = scaleNotes[noteIdx]; + return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff)); +} /** - * Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. + * Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. + * + * When describing notes via numbers, note that negative numbers can be used to wrap backwards + * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale. + * + * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * * A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts). * @@ -186,6 +247,12 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n * n(rand.range(0,12).segment(8)) * .scale("C:ritusen") * .s("piano") + * @example + * n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4") + * .scale("C:/2") + * .s("piano") + * @example + * note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3) */ export const scale = register( @@ -199,49 +266,35 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { + // The case where the note has been defined via `n` or `pure` + if (!isObject || (isObject && ('n' in value || 'value' in value))) { + const step = isObject ? (value.n ?? value.value) : value; delete value.n; // remove n so it won't cause trouble - } - if (isNote(step)) { - // legacy.. - return pure(step); - } - let asNumber = Number(step); - let semitones = 0; - if (isNaN(asNumber)) { - step = String(step); - if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) { - logger( - `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, - 'error', - ); + if (isNote(step)) { + // legacy.. + return pure(step); + } + try { + const [number, offset] = _convertStepToNumberAndOffset(step); + let note; + if (isObject && value.anchor) { + note = stepInNamedScale(number, scale, value.anchor); + } else { + note = scaleStep(number, scale); + } + if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset)); + value = pure(isObject ? { ...value, note } : note); + } catch (err) { + logger(`[tonal] ${err.message}`, 'error'); return silence; } - const isharp = step.indexOf('#'); - if (isharp >= 0) { - asNumber = Number(step.substring(0, isharp)); - semitones = step.length - isharp; - } else { - const iflat = step.indexOf('b'); - asNumber = Number(step.substring(0, iflat)); - semitones = iflat - step.length; - } + return value; } - try { - let note; - if (isObject && value.anchor) { - note = stepInNamedScale(asNumber, scale, value.anchor); - } else { - note = scaleStep(asNumber, scale); - } - if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones)); - value = pure(isObject ? { ...value, note } : note); - } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); - value = silence; + // The case where the note has been defined via `note` + else { + const note = _getNearestScaleNote(scale, value.note); + return pure(isObject ? { ...value, note } : note); } - return value; }) .outerJoin() // legacy: diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 15288fcc8..233129641 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) { let scaleSteps = {}; // [scaleName]: semitones[] export function stepInNamedScale(step, scale, anchor, preferHigher) { - let [root, scaleName] = Scale.tokenize(scale); + const [root, scaleName] = Scale.tokenize(scale); const rootMidi = x2midi(root); const rootChroma = midi2chroma(rootMidi); if (!scaleSteps[scaleName]) { - let { intervals } = Scale.get(`C ${scaleName}`); + const { intervals } = Scale.get(`C ${scaleName}`); // cache result scaleSteps[scaleName] = intervals.map(step2semitones); } @@ -178,7 +178,7 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' return notes; } -// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs +// https://codeberg.org/uzu/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']; @@ -222,6 +222,7 @@ export const Note = { }; // TODO: support octave numbers +// Example: Note("Bb3").transpose("c3") export function transpose(note, step) { // example: E, 3 const stepNumber = Step.tokenize(step)[1]; // 3 @@ -235,5 +236,3 @@ export function transpose(note, step) { 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") diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 0a08575db..d81911e01 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -1,6 +1,6 @@ /* voicings.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 55140f802..18722bdc2 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.1.0", + "version": "1.2.4", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 988479b4f..02970cf43 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -1,6 +1,6 @@ /* transpiler.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..fea6bad4d 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -8,6 +8,16 @@ export function registerWidgetType(type) { widgetMethods.push(type); } +let languages = new Map(); +// config = { getLocations: (code: string, offset?: number) => number[][] } +// see mondough.mjs for example use +// the language will kick in when the code contains a template literal of type +// example: mondo`...` will use language of type "mondo" +// TODO: refactor tidal.mjs to use this +export function registerLanguage(type, config) { + languages.set(type, config); +} + export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -19,14 +29,33 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { - const leafLocs = getLeafLocations(`"${value}"`, node.start, input); - miniLocations = miniLocations.concat(leafLocs); + const minilang = languages.get('minilang'); + if (minilang) { + const code = `[${value}]`; + const locs = minilang.getLocations(code, node.start); + miniLocations = miniLocations.concat(locs); + } else { + const leafLocs = getLeafLocations(`"${value}"`, node.start, input); + miniLocations = miniLocations.concat(leafLocs); + } }; let widgets = []; walk(ast, { enter(node, parent /* , prop, index */) { - if (isTidalTeplateLiteral(node)) { + if (isLanguageLiteral(node)) { + const { name } = node.tag; + const language = languages.get(name); + const code = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const locs = language.getLocations(code, offset); + miniLocations = miniLocations.concat(locs); + } + this.skip(); + return this.replace(languageWithLocation(name, code, offset)); + } + if (isTemplateLiteral(node, 'tidal')) { const raw = node.quasi.quasis[0].value.raw; const offset = node.quasi.start + 1; if (emitMiniLocations) { @@ -84,8 +113,18 @@ export function transpiler(input, options = {}) { leave(node, parent, prop, index) {}, }); - const { body } = ast; - if (!body?.[body.length - 1]?.expression) { + let { body } = ast; + + if (!body.length) { + console.warn('empty body -> fallback to silence'); + body.push({ + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }); + } else if (!body?.[body.length - 1]?.expression) { throw new Error('unexpected ast format without body expression'); } @@ -120,11 +159,18 @@ function isBackTickString(node, parent) { function miniWithLocation(value, node) { const { start: fromOffset } = node; + + const minilang = languages.get('minilang'); + let name = 'm'; + if (minilang && minilang.name) { + name = minilang.name; // name is expected to be exported from the package of the minilang + } + return { type: 'CallExpression', callee: { type: 'Identifier', - name: 'm', + name, }, arguments: [ { type: 'Literal', value }, @@ -194,7 +240,7 @@ function isLabelStatement(node) { } // converts label expressions to p calls: "x: y" to "y.p('x')" -// see https://github.com/tidalcycles/strudel/issues/990 +// see https://codeberg.org/uzu/strudel/issues/990 function labelToP(node) { return { type: 'ExpressionStatement', @@ -219,12 +265,16 @@ function labelToP(node) { }; } +function isLanguageLiteral(node) { + return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name); +} + // tidal highlighting // this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) // but maybe it's the only way -function isTidalTeplateLiteral(node) { - return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal'; +function isTemplateLiteral(node, value) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === value; } function collectHaskellMiniLocations(haskellCode, offset) { @@ -262,3 +312,18 @@ function tidalWithLocation(value, offset) { optional: false, }; } + +function languageWithLocation(name, value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: name, + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} diff --git a/packages/vite-plugin-bundle-audioworklet/package.json b/packages/vite-plugin-bundle-audioworklet/package.json new file mode 100644 index 000000000..f803ae065 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/package.json @@ -0,0 +1,16 @@ +{ + "name": "vite-plugin-bundle-audioworklet", + "main": "./vite-plugin-bundle-audioworklet.js", + "version": "0.1.1", + "description": "", + "keywords": [ + "vite", + "audioworklet" + ], + "author": "Felix Roos ", + "license": "MIT", + "type": "module", + "devDependencies": { + "vite": "^6.0.11" + } +} diff --git a/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js new file mode 100644 index 000000000..4c5634ba4 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js @@ -0,0 +1,48 @@ +import { createLogger, build } from 'vite'; + +const end = '?audioworklet'; + +function bundleAudioWorkletPlugin() /* : PluginOption */ { + let viteConfig /* : UserConfig */; + + return { + name: 'vite-plugin-bundle-audioworklet', + /* apply: 'build', */ + enforce: 'post', + + config(config) { + viteConfig = config; + }, + + async transform(_code, id) { + if (!id.endsWith(end)) { + return; + } + const entry = id.replace(end, ''); + const quietLogger = createLogger(); + quietLogger.info = () => undefined; + + const output = await build({ + configFile: false, + clearScreen: false, + customLogger: quietLogger, + build: { + lib: { + entry, + name: '_', + formats: ['iife'], + }, + write: false, + }, + }); + if (!(output instanceof Array)) { + throw new Error('Expected output to be Array'); + } + const iife = output[0].output[0].code; + const encoded = Buffer.from(iife, 'utf8').toString('base64'); + return `export default "data:text/javascript;base64,${encoded}";`; + }, + }; +} + +export default bundleAudioWorkletPlugin; diff --git a/packages/web/package.json b/packages/web/package.json index b06e5a489..df21f4055 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.1.0", + "version": "1.2.5", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", @@ -41,6 +41,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js index 99b93956f..671bf2825 100644 --- a/packages/web/vite.config.js +++ b/packages/web/vite.config.js @@ -2,11 +2,12 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'web.mjs'), diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 59672b617..4933b7a01 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -1,10 +1,11 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ export * from './webaudio.mjs'; export * from './scope.mjs'; export * from './spectrum.mjs'; +export * from './supradough.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 81b75c036..49da00f23 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.1.0", + "version": "1.2.5", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -29,13 +29,14 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*" + "superdough": "workspace:*", + "supradough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs new file mode 100644 index 000000000..f97251a07 --- /dev/null +++ b/packages/webaudio/supradough.mjs @@ -0,0 +1,130 @@ +import { Pattern } from '@strudel/core'; +import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; + +let doughWorklet; + +function initDoughWorklet() { + const ac = getAudioContext(); + doughWorklet = getWorklet( + ac, + 'dough-processor', + {}, + { + outputChannelCount: [2], + }, + ); + connectToDestination(doughWorklet); // channels? +} + +const soundMap = new Map(); +const loadedSounds = new Map(); + +Pattern.prototype.supradough = function () { + return this.onTrigger((hap, __, cps, begin) => { + hap.value._begin = begin; + hap.value._duration = hap.duration / cps; + !doughWorklet && initDoughWorklet(); + const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s; + const n = hap.value.n ?? 0; + const soundKey = `${s}:${n}`; + if (soundMap.has(s)) { + hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers) + } + if (soundMap.has(s) && !loadedSounds.has(soundKey)) { + const urls = soundMap.get(s); + const url = urls[n % urls.length]; + console.log(`load ${soundKey} from ${url}`); + const loadSample = fetchSample(url); + loadedSounds.set(soundKey, loadSample); + loadSample.then(({ channels, sampleRate }) => + doughWorklet.port.postMessage({ + sample: soundKey, + channels, + sampleRate, + }), + ); + } + + doughWorklet.port.postMessage({ spawn: hap.value }); + }, 1); +}; + +function githubPath(base, subpath = '') { + if (!base.startsWith('github:')) { + throw new Error('expected "github:" at the start of pseudoUrl'); + } + let [_, path] = base.split('github:'); + path = path.endsWith('/') ? path.slice(0, -1) : path; + if (path.split('/').length === 2) { + // assume main as default branch if none set + path += '/main'; + } + return `https://raw.githubusercontent.com/${path}/${subpath}`; +} +export async function fetchSampleMap(url) { + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (url.startsWith('shabda:')) { + let [_, path] = url.split('shabda:'); + url = `https://shabda.ndre.gr/${path}.json?strudel=1`; + } + if (url.startsWith('shabda/speech')) { + let [_, path] = url.split('shabda/speech'); + path = path.startsWith('/') ? path.substring(1) : path; + let [params, words] = path.split(':'); + let gender = 'f'; + let language = 'en-GB'; + if (params) { + [language, gender] = params.split('/'); + } + url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + const base = url.split('/').slice(0, -1).join('/'); + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + const json = await fetch(url) + .then((res) => res.json()) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); + return [json, json._base || base]; +} + +// for some reason, only piano and flute work.. is it because mp3?? + +async function fetchSample(url) { + const buffer = await fetch(url) + .then((res) => res.arrayBuffer()) + .then((buf) => getAudioContext().decodeAudioData(buf)); + let channels = []; + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + return { channels, sampleRate: buffer.sampleRate }; +} + +export async function doughsamples(sampleMap, baseUrl) { + if (typeof sampleMap === 'string') { + const [json, base] = await fetchSampleMap(sampleMap); + // console.log('json', json, 'base', base); + return doughsamples(json, base); + } + Object.entries(sampleMap).map(async ([key, urls]) => { + if (key !== '_base') { + urls = urls.map((url) => baseUrl + url); + // console.log('set', key, urls); + soundMap.set(key, urls); + } + }); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..383e87f87 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -1,11 +1,16 @@ /* webaudio.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough'; +import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import './supradough.mjs'; +import { workletUrl } from 'supradough'; + +registerWorklet(workletUrl); + const { Pattern, logger, repl } = strudel; setLogger(logger); @@ -15,13 +20,10 @@ const hap2value = (hap) => { return hap.value; }; -export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => - superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); - -Pattern.prototype.webaudio = function () { - return this.onTrigger(webaudioOutputTrigger); +// TODO: refactor output callbacks to eliminate deadline +export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { + return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; export function webaudioRepl(options = {}) { diff --git a/packages/xen/package.json b/packages/xen/package.json index 74c3aba73..0a6736d9c 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.1.0", + "version": "1.2.4", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/xen/test/xen.test.mjs b/packages/xen/test/xen.test.mjs index 977b0f694..a0982208a 100644 --- a/packages/xen/test/xen.test.mjs +++ b/packages/xen/test/xen.test.mjs @@ -1,6 +1,6 @@ /* xen.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index feed38f4f..01303bf52 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -1,6 +1,6 @@ /* tune.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 7b1a804a0..5806cf596 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -1,6 +1,6 @@ /* tunejs.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ @@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) { /* Load a new scale */ -Tune.prototype.loadScale = function(name){ +Tune.prototype.loadScale = function(scale){ /* load the scale */ - var freqs = TuningList[name].frequencies + var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies this.scale = [] for (var i=0;i 0 && arg.every(item => typeof item === 'number' && !isNaN(item)); +} + +/* allow an array of values too */ +Tune.prototype.isValidScale = function(scale) { + return !!TuningList[scale] || isArrayOfNumbers(scale) ; } /* Return a collection of notes as an array */ diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 4077632a5..cc96f4110 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -1,6 +1,6 @@ /* xen.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 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 . */ diff --git a/paper/iclc2023.html b/paper/iclc2023.html deleted file mode 100644 index a83e075a2..000000000 --- a/paper/iclc2023.html +++ /dev/null @@ -1,818 +0,0 @@ - - - - - - - - Strudel: live coding patterns on the Web - - - - - - - -

Abstract

-
-

This paper introduces Strudel, which brings the TidalCycles approach -to live coding algorithmic patterns to native JavaScript and the web. We -begin by giving a little background of the first year of development, -before sharing some detail about its implementation and examples of use. -We go on to outline the wide range of synthesis and other outputs -available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), -WebSerial and CSound, and introduce Strudel’s REPL live editor, -including its built-in visualisations. We then compare Strudel with -Tidal, the trade-offs involved between JavaScript and Haskell, and the -unique capabilities offered by Strudel for aligning patterns.

-
- -

1 Introduction

-

In the following paper, we introduce Strudel, an alternative -implementation of the TidalCycles (or ‘Tidal’ for short) live coding -system, using the JavaScript programming language. Strudel is an attempt -to make live coding more accessible, by creating a system that runs -entirely in the browser, while opening Tidal’s approach to algorithmic -patterns (Mclean 2020) up to -modern audio/visual web technologies. The Strudel REPL is a live code -editor dedicated to manipulating patterns while they play, with builtin -visual feedback. While Strudel is written in JavaScript, the API is -optimized for simplicity and readability by applying code -transformations on the syntax tree level, allowing language operations -that would otherwise be impossible. The application supports multiple -ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open -Sound Control) messages, Web Serial, Web MIDI and Csound. The project is -split into multiple packages, allowing granular reuse in other -applications. Apart from TidalCycles, Strudel draws inspiration from -many prior existing projects like TidalVortex (McLean et al. 2022), -Gibber (Roberts and Kuchera-morin -2012), Estuary (Ogborn et al. -2017), Hydra (Jack [2022] 2022), Ocarina (Solomon -[2021] 2022) and Feedforward (McLean 2020). This paper -expands the Strudel Demo paper for the Web Audio Conference 2022 (Roos and McLean -2022).

-

The first tentative commit to the Strudel project was on 22nd January -2022 by Alex McLean, with the core representation implemented over the -following few days. Although this was his first attempt at a -JavaScript-based application, by 27th January, Alex had managed to -upload the initial version to the ‘npm’ javascript package database, -sharing with the wider community for comment. By 4th February, Felix -Roos had discovered Strudel and contributed a ‘REPL’ user interface to -it, and then contributed a scheduler the next day, so that Strudel could -already make sound. At this point, Alex and Felix shared ownership to -the repository, and the project has since proved to be a productive -confluence of Felix’s own work into music representation and -visualisation, with Alex’s experience with making Tidal. Felix has since -become the primary contributor to Strudel, with Alex continuing to jump -between developing both Strudel and Tidal. Aspects of Strudel’s -development have therefore fed back into TidalCycles, and both systems -have maintained a shared conceptual underpinning. We plan to continue -working towards feature parity between these systems, although within -the syntactical trade-offs and library ecosystems of JavaScript and -Haskell, some divergence is inevitable and healthy.

-

Over the first year of its life, Strudel is now a fully-fledged live -coding environment, porting Tidal’s core represention of patterns, -pattern transformations, and mininotation for polymetric sequences, -combined with a wealth of features for synthesising and visualising -those patterns.

-

2 From Tidal to Strudel and -back

-

As mentioned above, the original Tidal is implemented as a domain -specific language (DSL) embedded in the Haskell pure functional -programming language, and takes advantage of Haskell’s terse syntax and -advanced, ‘strong’ type system. JavaScript on the other hand, is a -multi-paradigm programming language, with a dynamic type system. Because -Tidal leans heavily on many of Haskell’s more unique features, it was -not always clear that it could meaningfully be ported to a -multi-paradigm scripting language. However, this possibility was already -demonstrated with an earlier port to Python [TidalVortex; McLean et al. -(2022)], and we have now successfully implemented Tidal’s pure -functional representation of patterns in Strudel, including partial -application, currying, and the functor, applicative and monadic -structures that underlie Tidal’s expressive pattern transformations. The -result is a terse and highly composable system, where everything is -either a pattern, or a function for combining and manipulating patterns, -offering a rich creative ground for exploration.

-

This development process has been far from a one-way port, however. -The process of porting Tidal’s concepts has also opened up new -possibilities, some just from revisiting every design decision, and some -from the particular affordances and constraints offered by JavaScript. -This has lead to new features (and indeed bugfixes) that have found -their way back to Tidal where appropriate, and ongoing work that we will -return to in the conclusion of this paper.

-

3 Representing Patterns

-

Patterns are the essence of Tidal. Its patterns are abstract entities -that represent flows of time as functions, adapting a technique called -pure functional reactive programming. Taking a time span as its input, a -Pattern can output a set of events that happen within that time span. It -depends on the structure of the Pattern how the events are located in -time. From now on, this process of generating events from a time span -will be called querying. Example:

-
const pattern = sequence(c3, [e3, g3])
-const events = pattern.queryArc(0, 1)
-console.log(events.map(e => e.show()))
-

In this example, we create a pattern using the sequence -function and query it for the time span from -0 to 1. Those numbers represent units of time -called cycles. The length of one cycle depends on the -tempo, which defaults to one cycle per second. The resulting events -are:

-
[{ value: 'c3', begin: 0, end: 1/2 },
-{ value: 'e3', begin: 1/2, end: 3/4 },
-{ value: 'g3', begin: 3/4, end: 1 }]
-

Each event has a value, a begin time and an end time, where time is -represented as a fraction. In the above case, the events are placed in -sequential order, where c3 takes the first half, and e3 and g3 together -take the second half. This temporal placement is the result of the -sequence function, which divides its arguments equally over -one cycle. If an argument is an array, the same rule applies to that -part of the cycle. In the example, e3 and g3 are divided equally over -the second half of the whole cycle.

-

The above examples do not represent how Strudel is used in practice. -In the live coding editor, the user only has to type in the pattern -itself, the querying will be handled by the scheduler. The scheduler -will repeatedly query the pattern for events, which are then scheduled -as sound synthesis or other event triggers. Also, the above event data -structure has been simplified for readability.

-
- - -
-

4 Making Patterns

-

In practice, the end-user live coder will not deal with constructing -patterns directly, but will rather build patterns using Strudel’s -extensive combinator library to create, combine and transform -patterns.

-

The live coder will rarely use the sequence function as -seen above, as sequencing is implicit in many functions. For example in -the following, the note function constructs a pattern of -notes, sequencing its arguments in the same manner as the previous -example.

-
note(c3, [e3, g3])
-

Perhaps more often, they will use the mini-notation for even terser -notation of rhythmic sequences: [^This last example is also valid Tidal -code, albeit the parenthesis is not required in its Haskell syntax in -this case. Tidal does not support passing sequences as lists directly to -the note function, however.].

-
note("c3 [e3 g3]")
-

Such sequences are often treated only a starting point for -manipulation, where they then undergo pattern transformations such as -repetition, symmetry, interference/combination or randomisation, -potentially at multiple timescales. Because Strudel patterns are -represented as pure functions of time rather than as data structures, -very long and complex generative results can be represented and -manipulated without having to store the resulting sequences in -memory.

-

5 Pattern Example

-

The following example showcases how patterns can be utilized to -create musical complexity from simple parts, using repetition and -interference:

-
"<0 2 [4 6](3,4,1) 3>"
-.off(1/4, add(2))
-.off(1/2, add(6))
-.scale('D minor')
-.legato(.25)
-.note().s("sawtooth square")
-.delay(.8).delaytime(.125)
-

The pattern starts with a rhythm of numbers in mini notation, which -are later interpreted inside the scale of D minor. The first line could -also be expressed without mini notation:

-
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
-

These numbers then undergo various pattern transformations. Here is a -short description of all the functions used:

-
    -
  • cat: play elements sequentially, where each lasts one -cycle
  • -
  • brackets: elements inside brackets are divided equally -over the time of their parent
  • -
  • .euclid(p, s, o): place p pulses evenly over s steps, -with offset o (Toussaint -2005)
  • -
  • .off(n, f): layers a pattern on top of itself, with the -new layer offset by n cycles, and with function f applied
  • -
  • .legato(n): multiply the duration of all events in a -pattern by a factor of n
  • -
  • .echo(t, n, v): copy each event t times, with n cycles -in between each copy, decreasing velocity by v
  • -
  • .note(): interpretes values as notes
  • -
  • .s(name): play back each event with the given -sound
  • -
  • .delay(wet): add delay
  • -
  • .delaytime(t): set delay time
  • -
-

Much of the above will be familiar to Tidal users.

- -

6 Ways to make Sound (and other -events)

-

To generate sound, Strudel supports bindings for different -outputs:

-
    -
  • Tone.js (deprecated)
  • -
  • Web Audio API
  • -
  • WebDirt, a js recreation of Tidal’s Dirt sample engine -(deprecated)
  • -
  • OSC via osc-js, compatible with superdirt
  • -
  • Csound via the Csound WebAssembly build
  • -
  • MIDI via WebMIDI
  • -
  • Serial via WebSerial
  • -
-

At first, we used Tone.js as sound output, but it proved to be -limited for the use case of Strudel, where each individual event could -potentially have a completely different audio graph. While the Web Audio -API takes a fire-and-forget approach, creating a lot of Tone.js -instruments and effects causes performance issues quickly. For that -reason, we chose to search for alternatives.

-

Strudel’s new default output uses the Web Audio API to create a new -audio graph for each event. It currently supports basic oscillators, -sample playback, various effects and an experimental support for -soundfonts.

-

WebDirt (Ogborn [2016] 2022) was -created as part of the Estuary Live Coding System (Ogborn et al. -2017), and proved to be a solid choice for handling samples in -Strudel as well. We are however focused on working more directly with -the Web Audio API to be able to integrate new features more tightly.

-

Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy -server, it is possible to send network messages to trigger events. This -is mainly used to render sound using SuperDirt (SuperDirt [2015] 2022), -which is the well-developed Supercollider-based synthesis framework that -Tidal live coders generally use as standard.

-

Recently, the experimental integration of Csound proved to bring a -new dimension of sound design capabilities to Strudel. Thanks to the -WebAssembly distribution of this classic system (Yi, Lazzarini, and Costello -2018), Csound ‘orchestra’ synthesisers can be embedded in and -then patterned with Strudel code.

-

MIDI output can also be used to send MIDI messages to either external -instruments or to other programs on the same device. Unlike OSC, Strudel -is able to send MIDI directly without requiring additional proxy -software, but only from web browsers that support it (at the time of -writing, this means Chromium-based browsers).

-

Finally, Strudel supports Serial output, for example to trigger -events via microcontrollers. This has already been explored for robot -choreography by Kate Sicchio and Alex McLean, via a performance -presented at the International Conference on Live Interfaces 2022.

-

7 The Strudel REPL

-

While Strudel can be used as a library in any JavaScript codebase, -its main, reference user interface is the Strudel REPL[^REPL stands for -read, evaluate, print/play, loop. It is friendly jargon for an -interactive programming interface from computing heritage, usually for a -commandline interface but also applied to live coding editors.], which -is a browser-based live coding environment. This live code editor is -dedicated to manipulating Strudel patterns while they play. The REPL -features built-in visual feedback, which highlights which elements in -the patterned (mini-notation) sequences are influencing the event that -is currently being played. This feedback is designed to support both -learning and live use of Strudel.

-

Besides a UI for playback control and meta information, the main part -of the REPL interface is the code editor powered by CodeMirror. In it, -the user can edit and evaluate pattern code live, using one of the -available synthesis outputs to create music and/or sound art. The -control flow of the REPL follows 3 basic steps:

-
    -
  1. The user writes and updates code. Each update transpiles and -evaluates it to create a Pattern instance
  2. -
  3. While the REPL is running, the Scheduler queries the -active Pattern by a regular interval, generating -Events (also known as Haps in Strudel) for the -next time span.
  4. -
  5. For each scheduling tick, all generated Events are -triggered by calling their onTrigger method, which is set -by the output.
  6. -
-
-REPL control flow - -
-

7.1 User Code

-

To create a Pattern from the user code, two steps are -needed:

-
    -
  1. Transpile the JS input code to make it functional
  2. -
  3. Evaluate the transpiled code
  4. -
-

7.1.1 Transpilation & -Evaluation

-

In the JavaScript world, using transpilation is a common practise to -be able to use language features that are not supported by the base -language. Tools like babel will transpile code that -contains unsupported language features into a version of the code -without those features.

-

In the same tradition, Strudel can add a transpilation step to -simplify the user code in the context of live coding. For example, the -Strudel REPL lets the user create mini notation patterns using just -double quoted strings, while single quoted strings remain what they -are:

-
"c3 [e3 g3]*2"
-

is transpiled to:

-
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
-

Here, the string is wrapped in mini, which will create a -pattern from a mini notation string. Additionally, the -withMiniLocation method passes the original source code -location of the string to the pattern, which enables highlighting active -events.

-

Other convenient features like pseudo variables, operator overloading -and top level await are possible with transpilation.

-

After the transpilation, the code is ready to be evaluated into a -Pattern.

-

Behind the scenes, the user code string is parsed with -acorn, turning it into an Abstract Syntax Tree (AST). The -AST allows changing the structure of the code before generating the -transpiled version using escodegen.

-

7.1.2 Mini Notation

-

While the transpilation allows JavaScript to express Patterns in a -less verbose way, it is still preferable to use the Mini Notation as a -more compact way to express rhythm. Strudel aims to provide the same -Mini Notation features and syntax as used in Tidal.

-

The Mini Notation parser is implemented using peggy, -which allows generating performant parsers for Domain Specific Languages -(DSLs) using a concise grammar notation. The generated parser turns the -Mini Notation string into an AST which is used to call the respective -Strudel functions with the given structure. For example, -"c3 [e3 g3]*2" will result in the following calls:

-
seq(
-  reify('c3').withLocation([1,1,1], [1,4,4]),
-  seq(
-    reify('e3').withLocation([1,5,5], [1,8,8]),
-    reify('g3').withLocation([1,8,8], [1,10,10]),
-  ).fast(2)
-)
-

7.1.3 Highlighting Locations

-

As seen in the examples above, both the JS and the Mini Notation -parser add source code locations using withMiniLocation and -withLocation methods. While the JS parser adds locations -relative to the user code as a whole, the Mini Notation adds locations -relative to the position of the mini notation string. The absolute -location of elements within Mini Notation can be calculated by simply -adding both locations together. This absolute location can be used to -highlight active events in real time.

-

7.2 Scheduling Events

-

After an instance of Pattern is obtained from the user -code, it is used by the scheduler to get queried for events. Once -started, the scheduler runs at a fixed interval to query active pattern -for events withing the current interval’s time span. A simplified -implementation looks like this:

-
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
-let interval = 0.5; // query interval in seconds
-let time = 0; // beginning of current time span
-let minLatency = .1; // min time before a hap should trigger
-setInterval(() => {
-  const haps = pattern.queryArc(time, time + interval);
-  time += interval; // increment time
-  haps.forEach((hap) => {
-    const deadline = hap.whole.begin - time + minLatency;
-    onTrigger(hap, deadline, duration);
-  });
-}, interval * 1000); // query each "interval" seconds
-

Note that the above code is simplified for illustrative purposes. The -actual implementation has to work around imprecise callbacks of -setInterval. More about the implementation details can be -read in this -blog post.

-

The fact that Pattern.queryArc is a pure function that -maps a time span to a set of events allows us to choose any interval we -like without changing the resulting output. It also means that when the -pattern is changed from outside, the next scheduling callback will work -with the new pattern, keeping its clock running.

-

The latency between the time the pattern is evaluated and the change -is heard is between minLatency and -interval + minLatency, in our example between 100ms and -600ms. In Strudel, the current query interval is 50ms with a minLatency -of 100ms, meaning the latency is between 50ms and 150ms.

-

7.3 Output

-

The last step is to trigger each event in the chosen output. This is -where the given time and value of each event is used to generate audio -or any other form of time based output. The default output of the -Strudel REPL is the WebAudio output. To understand what an output does, -we first have to understand what control parameters are.

-

7.3.1 Control Parameters

-

To be able to manipulate multiple aspects of sound in parallel, so -called control parameters are used to shape the value of each event. -Example:

-
note("c3 e3").cutoff(1000).s('sawtooth')
-  .queryArc(0, 1).map(hap => hap.value)
-/* [
-  { note: 'c3', cutoff: 1000, s: 'sawtooth' }
-  { note: 'e3', cutoff: 1000, s: 'sawtooth' }
-] */
-

Here, the control parameter functions note, -cutoff and s are used, where each controls a -different property in the value object. Each control parameter function -accepts a primitive value, a list of values to be sequenced into a -Pattern, or a Pattern. In the example, -note gets a Pattern from a Mini Notation -expression (double quoted), while cutoff and s -are given a Number and a (single quoted) -String respectively.

-

Strudel comes with a large default set of control parameter functions -that are based on the ones used by Tidal and SuperDirt, focusing on -music and audio terminology. It is however possible to create custom -control paramters for any purpose:

-
const { x, y } = createParams('x', 'y')
-x(sine.range(0, 200)).y(cosine.range(0,200))
-

This example creates the custom control parameters x and -y which are then used to form a pattern that descibes the -coordinates of a circle.

-

7.3.2 Outputs

-

Now that we know how the value of an event is manipulated using -control parameters, we can look at how outputs can use that value to -generate anything. The scheduler above was calling the -onTrigger function which is used to implement the output. A -very simple version of the web audio output could look like this:

-
function onTrigger(hap, deadline, duration) {
-  const { note } = hap.value;
-  const time = getAudioContext().currentTime + deadline;
-  const o = getAudioContext().createOscillator();
-  o.frequency.value = getFreq(note);
-  o.start(time);
-  o.stop(time + event.duration);
-  o.connect(getAudioContext().destination);
-}
-

The above example will create an OscillatorNode for each -event, where the frequency is controlled by the note param. -In essence, this is how the WebAudio API output of Strudel works, only -with many more parameters to control synths, samples and effects.

-

8 Pattern alignment and -combination

-

One core aspect of Strudel, inherited from Tidal, is the flexible way -that patterns can be combined, irrespective of their structure. Its -declarative approach means a live coder does not have to think about the -details of how this is done, only what is to be -done.

-

As a simple example, consider two number patterns -"0 [1 2] 3", and "10 20". The first has three -contiguous steps of equal lengths, with the second step broken down into -two substeps, giving four events in total. There are a very large number -of ways in which the structure of these two patterns could be combined, -but the default method in both Strudel and Tidal is to line up the -cycles of the two patterns, and then take events from the first pattern -and match them with those in the second pattern. Therefore, the -following two lines are equivalent:

-
"0 [1 2] 3".add("10 20")
-"10 [11 22] 23"
-

Where the events only partially overlap, they are treated as -fragments of the event in the first pattern. This is a little difficult -to conceptualise, but lets start by comparing the two patterns in the -following example:

-
"0 1 2".add("10 20")
-"10 [11 21] 20"
-

They are similar to the previous example in that the number -1 is split in two, with its two halves added to -10 and 20 respectively. However, the -11 ‘remembers’ that it is a fragment of that original -1 event, and so is treated as having a duration of a third -of a cycle, despite only being active for a sixth of a cycle. Likewise, -the 21 is also a fragment of that original 1 -event, but a fragment of its second half. Because the start of its event -is missing, it wouldn’t actually trigger a sound (unless it underwent -further pattern transformations/combinations).

-

In practice, the effect of this default, implicit method for -combining two patterns is that the second pattern is added in -to the first one, and indeed this can be made explicit:

-
"0 1 2".add.in("10 20")
-

This makes way for other ways to align the pattern, and several are -already defined, in particular:

-
    -
  • in - as explained above, aligns cycles, and applies -values from the pattern on the right in to the pattern on the -left.
  • -
  • out - as with in, but values are applied -out of the pattern on the left (i.e. in to the one on -the right).
  • -
  • mix - structures from both patterns are combined, so -that the new events are not fragments but are created at intersections -of events from both sides.
  • -
  • squeeze - cycles from the pattern on the right are -squeezed into events on the left. So that -e.g. "0 1 2".add.squeeze("10 20") is equivalent to -"[10 20] [11 21] [12 22]".
  • -
  • squeezeout - as with squeeze, but cycles -from the left are squeezed into events on the right. So, -"0 1 2".add.squeezeout("10 20") is equivalent to -[10 11 12] [20 21 22].
  • -
  • trig is similar to squeezeout in that -cycles from the right are aligned with events on the left. However those -cycles are not ‘squeezed’, rather they are truncated to fit the event. -So "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be -equivalent to 10 11 12 13 20 21 30 31. In effect, events on -the right ‘trigger’ cycles on the left.
  • -
  • trigzero is similar to trig, but the -pattern is ‘triggered’ from its very first cycle, rather than from the -current cycle. trig and trigzero therefore -only give different results where the leftmost pattern differs from one -cycle to the next.
  • -
-

We will save going deeper into the background, design and -practicalities of these alignment functions for future publications. -However in the next section, we take them as a case study for looking at -the different design affordances offered by Haskell to Tidal, and -JavaScript to Strudel.

-

9 Comparing Strudel and Haskell in -use

-

Unlike Haskell, JavaScript lacks the ability to define custom infix -operators, or change the meaning of existing ones. So the above Strudel -example of "0 1 2".add.out("10 20") is equivalent to the -Tidal expression "0 1 2" +| "10 20", where the vertical bar -in the operator +| stands for out (where -a |+ b would be equivalent of -a.add.in(b)).

-

From this we can already see that Tidal tends towards brevity through -mixing infix operators with functions, and Strudel tends towards -spelling out operations which are joined together with the -. operator. This then is the design trade-off of Tidal’s -tersity, versus Strudel’s simplicity.

-

To demonstrate this, consider the following Tidal pattern:

-
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
-

This can be directly translated to the Strudel equivalent:

-
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
-

Although for a more canonical Strudel expression, we would reorder it -as:

-
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
-

The Strudel example uses the . method call operator for -all operations and combinations, whereas the Tidal example has -# for the default method for combining patterns and uses -infix operators for other methods. The lack of parenthesis in the Tidal -example is partly due to the way that arguments are applied to Haskell’s -functions, and partly due to the use of the $ operator as -an alternative way to establish precedence and control the order of -evaluation.

-

Considering the above, we argue that the Haskell syntax is a little -cleaner, but that the Strudel syntax is easier to learn. Our informal -observation is that while Haskell’s dollar $ operator is -very useful in making code easier to work with, it is one of the most -difficult aspects of Tidal use for beginners to learn. On the other -hand, the deeper levels of parenthesis in Strudel code can be difficult -to keep track of, especially while coding under pressure of live musical -performance. However this difficulty can be largely be mitigated by -reordering expressions, and further mitigated by supporting editor -features.

-

With Strudel, we have little choice but to embrace the affordances -and constraints offered by JavaScript, and while designing a -domain-specific language entirely based on method calls is a challenge, -through creative adoption of functional programming techniques like -partial application, we are so far very happy with the results. Tidal’s -functional reactive approach to pattern-making has in general translated -well to JavaScript, and opportunities and constraints have overall -traded off to create a very approachable and useable live coding -environment.

-

9.1 The trade-off of flexible -typing

-

We have identified one problem with porting Tidal to JavaScript where -we have missed Haskell’s strict typing and type inference. In both Tidal -and Strudel, time is rational, where any point in time is represented as -the ratio of two integers. This allows representation of musical ratios -such that are impossible to represent accurately using the more common -floating point numbers. However while libraries are available that -support rational numbers in JavaScript, the lack of strict typing means -that it is easy to implement pattern methods where computationally -expensive conversion from floating point to rational numbers are -performed late, and therefore often enough to overload the CPUs, due to -the large number of iterative calculations required to estimate a ratio -for a given floating point number. To mitigate this problem, we might -consider moving to TypeScript in the future.

-

10 Future Outlook

-

The project is still young, with many features on the horizon. As -general guiding principles, Strudel aims to be

-
    -
  1. accessible
  2. -
  3. consistent with Tidal’s approach to pattern
  4. -
  5. modular and extensible
  6. -
-

While Haskell’s type system makes it a great language for the ongoing -development of Tidal’s inner representation of pattern, JavaScript’s -vibrant ecosystem, flexibility and accessibility makes it a great host -for more ad-hoc experiments, including interface design. For the future, -it is planned to integrate additional alternative sound engines such as -Glicol (Lan -[2020] 2022) and Faust (Faust - Programming -Language for Audio Applications and Plugins [2016] 2022). -Strudel is already approaching feature parity with Tidal, but there are -more Tidal functions to be ported, and work to be done to improve -compatibility with Tidal’s mininotation. Tidal version 2.0 is under -development, which brings a new representation for sequences to its -patterns, which will then be brought to Strudel. Besides sound, other -ways to render events are being explored, such as graphical, and -choreographic output. We are also looking into alternative ways of -editing patterns, including multi-user editing for network music, -parsing a novel syntax to escape the constraints of javascript, and -developing hardware/e-textile interfaces. In summary, there is a lot of -fun ahead.

-

11 Links

-

The Strudel REPL is available at https://strudel.cc, including an -interactive tutorial. The repository is at https://github.com/tidalcycles/strudel, all the code is -open source under the AGPL-3.0 License.

-

12 Acknowledgments

-

Thanks to the Strudel and wider Tidal, live coding, WebAudio and -free/open source software communities for inspiration and support. Alex -McLean’s work on this project is supported by a UKRI Future Leaders -Fellowship [grant number MR/V025260/1].

-

References

-
-
-Faust - Programming Language for Audio Applications and -Plugins. (2016) 2022. C++. GRAME. https://github.com/grame-cncm/faust. -
-
-Jack, Olivia. (2022) 2022. Hydra. https://github.com/ojack/hydra. -
-
-Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. https://github.com/chaosprint/glicol. -
-
-Mclean, Alex. 2020. “Algorithmic Pattern.” In -Proceedings of the International Conference on New Interfaces for -Musical Expression, 265--270. Birmingham, UK. https://zenodo.org/record/4813352. -
-
-McLean, Alex. 2020. “Feedforward.” In Proceedings of -New Interfaces for Musical Expression. Birmingham. https://zenodo.org/record/6353969. -
-
-McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. -2022. “TidalVortex Zero.” In Proceedings of the 7th -International Conference on Live Coding. Limerick, Ireland: Zenodo. -https://doi.org/10.5281/zenodo.6456380. -
-
-Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. https://github.com/dktr0/WebDirt. -
-
-Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, -and Alex McLean. 2017. “Estuary: Browser-Based Collaborative -Projectional Live Coding of Musical Patterns.” In Proceedings -of the International Conference on Live Coding, 11. Morelia. -
-
-Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live -Coding Audio in the Browser.” In In Proceedings of the 2012 -International Computer Music Conference. -
-
-Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns -for the Web.” In. Zenodo. https://doi.org/10.5281/zenodo.6768844. -
-
-Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. https://github.com/mikesol/purescript-ocarina. -
-
-SuperDirt. (2015) 2022. SuperCollider. musikinformatik. https://github.com/musikinformatik/SuperDirt. -
-
-Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates -Traditional Musical Rhythms.” In In Proceedings of BRIDGES: -Mathematical Connections in Art, Music and Science, 47–56. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. -
-
-Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. -“WebAssembly AudioWorklet Csound.” In. Berlin, Germany. https://mural.maynoothuniversity.ie/16018/. -
-
- - diff --git a/paper/pandoc/iclc.html b/paper/pandoc/iclc.html deleted file mode 100755 index 90406b16e..000000000 --- a/paper/pandoc/iclc.html +++ /dev/null @@ -1,80 +0,0 @@ -$if(false)$ - -This is a pandoc template and should not be edited. - -$endif$ - - - - - - -$for(author-meta)$ - -$endfor$ -$if(date-meta)$ - -$endif$ - $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ - -$if(quotes)$ - -$endif$ -$if(highlighting-css)$ - -$endif$ - -$for(css)$ - -$endfor$ -$if(math)$ - $math$ -$endif$ -$for(header-includes)$ - $header-includes$ -$endfor$ - - -$for(include-before)$ -$include-before$ -$endfor$ -$if(title)$ -
-

$title$

-$if(subtitle)$ -

$subtitle$

-$endif$ -
    -$for(author)$ -
  • $author$
  • -$endfor$ -
-$if(date)$ -

$date$

-$endif$ -
-$endif$ -$if(toc)$ -
-$toc$ -
-$endif$ - -

Abstract

-
-$if(abstract)$ -$abstract$ -$else$ -Please provide an abstract in the metadata block at the top of your -markdown document. Refer to template.txt for details. -$endif$ -
- -$body$ -$for(include-after)$ -$include-after$ -$endfor$ - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cdd501cc..c225057f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 2.2.7 '@vitest/coverage-v8': specifier: 3.0.4 - version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)) + version: 3.0.4(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -75,6 +75,9 @@ importers: prettier: specifier: ^3.4.2 version: 3.4.2 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:packages/vite-plugin-bundle-audioworklet vitest: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) @@ -198,8 +201,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vim': - specifier: ^6.2.1 - version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) + specifier: ^6.3.0 + version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vscode-keymap': specifier: ^6.0.2 version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) @@ -270,6 +273,16 @@ importers: packages/embed: {} + packages/gamepad: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/hs2js: dependencies: web-tree-sitter: @@ -334,6 +347,37 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mondo: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + + packages/mondough: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler + mondolang: + specifier: workspace:* + version: link:../mondo + devDependencies: + mondo: + specifier: '*' + version: 0.4.4 + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: dependencies: '@strudel/core': @@ -418,6 +462,9 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet packages/sampler: dependencies: @@ -466,6 +513,21 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet + + packages/supradough: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet + wav-encoder: + specifier: ^1.3.0 + version: 1.3.0 packages/tidal: dependencies: @@ -530,6 +592,12 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/vite-plugin-bundle-audioworklet: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/web: dependencies: '@strudel/core': @@ -554,6 +622,9 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet packages/webaudio: dependencies: @@ -566,6 +637,9 @@ importers: superdough: specifier: workspace:* version: link:../superdough + supradough: + specifier: workspace:* + version: link:../supradough devDependencies: vite: specifier: ^6.0.11 @@ -643,6 +717,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../packages/draw + '@strudel/gamepad': + specifier: workspace:* + version: link:../packages/gamepad '@strudel/hydra': specifier: workspace:* version: link:../packages/hydra @@ -652,6 +729,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/mondo': + specifier: workspace:* + version: link:../packages/mondough '@strudel/motion': specifier: workspace:* version: link:../packages/motion @@ -767,6 +847,9 @@ importers: sharp: specifier: ^0.33.5 version: 0.33.5 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../packages/vite-plugin-bundle-audioworklet workbox-window: specifier: ^7.3.0 version: 7.3.0 @@ -2164,14 +2247,14 @@ packages: '@codemirror/state': ^6.0.1 '@codemirror/view': ^6.3.0 - '@replit/codemirror-vim@6.2.1': - resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==} + '@replit/codemirror-vim@6.3.0': + resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==} peerDependencies: - '@codemirror/commands': ^6.0.0 - '@codemirror/language': ^6.1.0 - '@codemirror/search': ^6.2.0 - '@codemirror/state': ^6.0.1 - '@codemirror/view': ^6.0.3 + '@codemirror/commands': 6.x.x + '@codemirror/language': 6.x.x + '@codemirror/search': 6.x.x + '@codemirror/state': 6.x.x + '@codemirror/view': 6.x.x '@replit/codemirror-vscode-keymap@6.0.2': resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==} @@ -2941,6 +3024,10 @@ packages: resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==} engines: {node: '>= 14.0.0'} + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3069,6 +3156,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -3303,6 +3393,9 @@ packages: claviature@0.1.0: resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + cldr-plurals@1.0.0: + resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4344,6 +4437,9 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -4405,6 +4501,11 @@ packages: h3@1.14.0: resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==} + handlebars@1.1.2: + resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==} + engines: {node: '>=0.4.7'} + hasBin: true + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -5552,6 +5653,10 @@ packages: engines: {node: '>=18'} hasBin: true + mondo@0.4.4: + resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -5624,6 +5729,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -5793,6 +5899,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + optimist@0.3.7: + resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6691,6 +6800,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -6706,6 +6819,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -7121,6 +7235,11 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@2.3.6: + resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==} + engines: {node: '>=0.4.0'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -7139,6 +7258,9 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + underscore@1.5.2: + resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7430,6 +7552,9 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + wav-encoder@1.3.0: + resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==} + wav@1.0.2: resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==} @@ -7521,6 +7646,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -9487,7 +9616,7 @@ snapshots: '@codemirror/state': 6.5.1 '@codemirror/view': 6.36.2 - '@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': + '@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': dependencies: '@codemirror/commands': 6.8.0 '@codemirror/language': 6.10.8 @@ -10235,7 +10364,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -10405,6 +10534,9 @@ snapshots: '@algolia/requester-fetch': 5.20.0 '@algolia/requester-node-http': 5.20.0 + amdefine@1.0.1: + optional: true + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -10614,6 +10746,9 @@ snapshots: async-function@1.0.0: {} + async@0.2.10: + optional: true + async@3.2.6: {} asynckit@0.4.0: {} @@ -10871,6 +11006,8 @@ snapshots: claviature@0.1.0: {} + cldr-plurals@1.0.0: {} + clean-stack@2.2.0: {} cli-boxes@3.0.0: {} @@ -12013,6 +12150,8 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globalize@0.1.1: {} + globals@11.12.0: {} globals@14.0.0: {} @@ -12079,6 +12218,12 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 + handlebars@1.1.2: + dependencies: + optimist: 0.3.7 + optionalDependencies: + uglify-js: 2.3.6 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -13662,6 +13807,13 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + mondo@0.4.4: + dependencies: + cldr-plurals: 1.0.0 + globalize: 0.1.1 + handlebars: 1.1.2 + underscore: 1.5.2 + mrmime@2.0.0: {} ms@2.0.0: {} @@ -13954,6 +14106,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optimist@0.3.7: + dependencies: + wordwrap: 0.0.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15061,6 +15217,11 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + optional: true + source-map@0.5.7: {} source-map@0.6.1: {} @@ -15528,6 +15689,13 @@ snapshots: ufo@1.5.4: {} + uglify-js@2.3.6: + dependencies: + async: 0.2.10 + optimist: 0.3.7 + source-map: 0.1.43 + optional: true + uglify-js@3.19.3: optional: true @@ -15544,6 +15712,8 @@ snapshots: underscore@1.13.7: {} + underscore@1.5.2: {} + undici-types@6.20.0: {} unenv@1.10.0: @@ -15806,6 +15976,8 @@ snapshots: walk-up-path@3.0.1: {} + wav-encoder@1.3.0: {} + wav@1.0.2: dependencies: buffer-alloc: 1.2.0 @@ -15924,6 +16096,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@0.0.3: {} + wordwrap@1.0.0: {} workbox-background-sync@7.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ef8cc99d5..a4fca9ff7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,12 @@ packages: - # all packages in direct subdirs of packages/ - - "packages/*" - - "examples/*" - - "tools/dbpatch" - - "website/" + - packages/* + - examples/* + - tools/dbpatch + - website/ + +onlyBuiltDependencies: + - esbuild + - nx + - sharp + - tree-sitter + - tree-sitter-haskell diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index c100d566d..73278947d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -933,6 +933,39 @@ exports[`runs examples > example "as" example index 0 1`] = ` ] `; +exports[`runs examples > example "as" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | begin:0 s:sax_vib clip:1 ]", + "[ 1/4 → 3/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/8 → 1/2 | begin:0 s:sax_vib clip:1 ]", + "[ 1/2 → 5/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 5/8 → 3/4 | begin:0.3 s:sax_vib clip:1 ]", + "[ 3/4 → 7/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ (7/8 → 1/1) ⇝ 9/8 | begin:0 s:sax_vib clip:1 ]", + "[ 7/8 ⇜ (1/1 → 9/8) | begin:0 s:sax_vib clip:1 ]", + "[ 9/8 → 5/4 | begin:0.25 s:sax_vib clip:1 ]", + "[ 5/4 → 11/8 | begin:0 s:sax_vib clip:1 ]", + "[ 11/8 → 3/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 3/2 → 13/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 13/8 → 7/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/4 → 2/1 | begin:0 s:sax_vib clip:1 ]", + "[ 2/1 → 17/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 17/8 → 9/4 | begin:0 s:sax_vib clip:1 ]", + "[ 9/4 → 19/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 19/8 → 5/2 | begin:0.3 s:sax_vib clip:1 ]", + "[ 5/2 → 21/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 21/8 → 23/8 | begin:0 s:sax_vib clip:1 ]", + "[ 23/8 → 3/1 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/1 → 25/8 | begin:0 s:sax_vib clip:1 ]", + "[ 25/8 → 13/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 13/4 → 27/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 27/8 → 7/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/2 → 15/4 | begin:0 s:sax_vib clip:1 ]", + "[ 15/4 → 31/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 31/8 → 4/1 | begin:0 s:sax_vib clip:1 ]", +] +`; + exports[`runs examples > example "attack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 attack:0 ]", @@ -1018,6 +1051,75 @@ exports[`runs examples > example "begin" example index 0 1`] = ` ] `; +exports[`runs examples > example "berlin" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 ]", + "[ 1/16 → 1/8 | note:E3 ]", + "[ 1/8 → 3/16 | note:F3 ]", + "[ 3/16 → 1/4 | note:G3 ]", + "[ 1/4 → 5/16 | note:A3 ]", + "[ 5/16 → 3/8 | note:C4 ]", + "[ 3/8 → 7/16 | note:D4 ]", + "[ 7/16 → 1/2 | note:F4 ]", + "[ 1/2 → 9/16 | note:D4 ]", + "[ 9/16 → 5/8 | note:E4 ]", + "[ 5/8 → 11/16 | note:E4 ]", + "[ 11/16 → 3/4 | note:E4 ]", + "[ 3/4 → 13/16 | note:F3 ]", + "[ 13/16 → 7/8 | note:F3 ]", + "[ 7/8 → 15/16 | note:F3 ]", + "[ 15/16 → 1/1 | note:F3 ]", + "[ 1/1 → 17/16 | note:E3 ]", + "[ 17/16 → 9/8 | note:E3 ]", + "[ 9/8 → 19/16 | note:E3 ]", + "[ 19/16 → 5/4 | note:F3 ]", + "[ 5/4 → 21/16 | note:E3 ]", + "[ 21/16 → 11/8 | note:F3 ]", + "[ 11/8 → 23/16 | note:G3 ]", + "[ 23/16 → 3/2 | note:A3 ]", + "[ 3/2 → 25/16 | note:A3 ]", + "[ 25/16 → 13/8 | note:Bb3 ]", + "[ 13/8 → 27/16 | note:Bb3 ]", + "[ 27/16 → 7/4 | note:Bb3 ]", + "[ 7/4 → 29/16 | note:F3 ]", + "[ 29/16 → 15/8 | note:G3 ]", + "[ 15/8 → 31/16 | note:Bb3 ]", + "[ 31/16 → 2/1 | note:C4 ]", + "[ 2/1 → 33/16 | note:C4 ]", + "[ 33/16 → 17/8 | note:D4 ]", + "[ 17/8 → 35/16 | note:F4 ]", + "[ 35/16 → 9/4 | note:G4 ]", + "[ 9/4 → 37/16 | note:Bb3 ]", + "[ 37/16 → 19/8 | note:Bb3 ]", + "[ 19/8 → 39/16 | note:Bb3 ]", + "[ 39/16 → 5/2 | note:C4 ]", + "[ 5/2 → 41/16 | note:F3 ]", + "[ 41/16 → 21/8 | note:F3 ]", + "[ 21/8 → 43/16 | note:G3 ]", + "[ 43/16 → 11/4 | note:A3 ]", + "[ 11/4 → 45/16 | note:A3 ]", + "[ 45/16 → 23/8 | note:A3 ]", + "[ 23/8 → 47/16 | note:A3 ]", + "[ 47/16 → 3/1 | note:A3 ]", + "[ 3/1 → 49/16 | note:E3 ]", + "[ 49/16 → 25/8 | note:G3 ]", + "[ 25/8 → 51/16 | note:Bb3 ]", + "[ 51/16 → 13/4 | note:C4 ]", + "[ 13/4 → 53/16 | note:D4 ]", + "[ 53/16 → 27/8 | note:E4 ]", + "[ 27/8 → 55/16 | note:G4 ]", + "[ 55/16 → 7/2 | note:A4 ]", + "[ 7/2 → 57/16 | note:Bb3 ]", + "[ 57/16 → 29/8 | note:Bb3 ]", + "[ 29/8 → 59/16 | note:C4 ]", + "[ 59/16 → 15/4 | note:C4 ]", + "[ 15/4 → 61/16 | note:F3 ]", + "[ 61/16 → 31/8 | note:G3 ]", + "[ 31/8 → 63/16 | note:G3 ]", + "[ 63/16 → 4/1 | note:A3 ]", +] +`; + exports[`runs examples > example "binary" example index 0 1`] = ` [ "[ 0/1 → 1/3 | s:hh ]", @@ -1483,6 +1585,52 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "byteBeatExpression" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 1/1 → 2/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 2/1 → 3/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 3/1 → 4/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", +] +`; + +exports[`runs examples > example "byteBeatStartTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/8 → 1/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/4 → 3/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/8 → 1/2 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/2 → 5/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 5/8 → 3/4 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/4 → 7/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 7/8 → 1/1 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/1 → 9/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 9/8 → 5/4 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 5/4 → 11/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 11/8 → 3/2 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 3/2 → 13/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 13/8 → 7/4 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 7/4 → 15/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 15/8 → 2/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 2/1 → 17/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 17/8 → 9/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 9/4 → 19/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 19/8 → 5/2 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 5/2 → 21/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 21/8 → 11/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 11/4 → 23/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 23/8 → 3/1 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/1 → 25/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 25/8 → 13/4 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 13/4 → 27/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 27/8 → 7/2 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 7/2 → 29/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 29/8 → 15/4 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 15/4 → 31/8 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 31/8 → 4/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", +] +`; + exports[`runs examples > example "cat" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:e5 ]", @@ -1699,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = ` ] `; +exports[`runs examples > example "chorus" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]", +] +`; + exports[`runs examples > example "chunk" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:A4 ]", @@ -1741,6 +1910,86 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` ] `; +exports[`runs examples > example "chunkBackInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + +exports[`runs examples > example "chunkInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + exports[`runs examples > example "clip" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano clip:0.5 ]", @@ -2289,6 +2538,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` ] `; +exports[`runs examples > example "degradeBy" example index 2 1`] = ` +[ + "[ 1/8 → 3/16 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 5/16 → 3/8 | s:bd ]", + "[ 1/2 → 9/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 21/16 → 11/8 | s:bd ]", + "[ 3/2 → 25/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 37/16 → 19/8 | s:bd ]", + "[ 5/2 → 41/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 53/16 → 27/8 | s:bd ]", + "[ 7/2 → 57/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "delay" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0 ]", @@ -2324,16 +2606,83 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; -exports[`runs examples > example "delaytime" example index 0 1`] = ` +exports[`runs examples > example "delayfeedback" example index 0 2`] = ` [ - "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", - "[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]", - "[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]", - "[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]", - "[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]", - "[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]", - "[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]", - "[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]", + "[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]", + "[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]", + "[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]", + "[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]", +] +`; + +exports[`runs examples > example "delayspeed" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", +] +`; + +exports[`runs examples > example "delaysync" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/2 → 1/1 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/1 → 3/2 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 3/2 → 2/1 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 2/1 → 5/2 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 5/2 → 3/1 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 3/1 → 7/2 | s:bd delay:0.25 delaysync:0.625 ]", + "[ 7/2 → 4/1 | s:bd delay:0.25 delaysync:0.625 ]", +] +`; + +exports[`runs examples > example "density" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:crackle density:0.01 ]", + "[ 1/4 → 1/2 | s:crackle density:0.01 ]", + "[ 1/2 → 3/4 | s:crackle density:0.01 ]", + "[ 3/4 → 1/1 | s:crackle density:0.01 ]", + "[ 1/1 → 5/4 | s:crackle density:0.01 ]", + "[ 5/4 → 3/2 | s:crackle density:0.01 ]", + "[ 3/2 → 7/4 | s:crackle density:0.01 ]", + "[ 7/4 → 2/1 | s:crackle density:0.01 ]", + "[ 2/1 → 9/4 | s:crackle density:0.01 ]", + "[ 9/4 → 5/2 | s:crackle density:0.01 ]", + "[ 5/2 → 11/4 | s:crackle density:0.01 ]", + "[ 11/4 → 3/1 | s:crackle density:0.01 ]", + "[ 3/1 → 13/4 | s:crackle density:0.01 ]", + "[ 13/4 → 7/2 | s:crackle density:0.01 ]", + "[ 7/2 → 15/4 | s:crackle density:0.01 ]", + "[ 15/4 → 4/1 | s:crackle density:0.01 ]", ] `; @@ -2482,26 +2831,38 @@ exports[`runs examples > example "distort" example index 1 1`] = ` exports[`runs examples > example "djf" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 1/1 → 5/4 | n:0 s:superzow octave:3 djf:0.25 ]", - "[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]", - "[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]", - "[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 9/4 → 5/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]", - "[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]", - "[ 7/2 → 15/4 | n:7 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]", + "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", + "[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]", + "[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]", + "[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]", + "[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]", + "[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]", + "[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]", + "[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]", + "[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]", + "[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]", + "[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]", + "[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]", + "[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]", + "[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]", + "[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]", + "[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]", + "[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]", + "[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]", + "[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]", + "[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]", + "[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]", + "[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]", + "[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]", + "[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]", + "[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]", + "[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]", + "[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]", ] `; @@ -2741,6 +3102,251 @@ exports[`runs examples > example "dry" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckattack" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", +] +`; + +exports[`runs examples > example "duckattack" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", +] +`; + +exports[`runs examples > example "duckdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", +] +`; + +exports[`runs examples > example "duckdepth" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", +] +`; + +exports[`runs examples > example "duckonset" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", +] +`; + +exports[`runs examples > example "duckorbit" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", +] +`; + +exports[`runs examples > example "duckorbit" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", +] +`; + exports[`runs examples > example "duration" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", @@ -2943,18 +3549,19 @@ exports[`runs examples > example "euclidLegato" example index 0 1`] = ` exports[`runs examples > example "euclidLegatoRot" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c3 ]", - "[ 1/4 → 3/4 | note:c3 ]", - "[ 3/4 → 1/1 | note:c3 ]", - "[ 1/1 → 5/4 | note:c3 ]", - "[ 5/4 → 7/4 | note:c3 ]", - "[ 7/4 → 2/1 | note:c3 ]", - "[ 2/1 → 9/4 | note:c3 ]", - "[ 9/4 → 11/4 | note:c3 ]", - "[ 11/4 → 3/1 | note:c3 ]", - "[ 3/1 → 13/4 | note:c3 ]", - "[ 13/4 → 15/4 | note:c3 ]", - "[ 15/4 → 4/1 | note:c3 ]", + "[ -1/5 ⇜ (0/1 → 1/5) | note:c3 ]", + "[ 1/5 → 2/5 | note:c3 ]", + "[ 2/5 → 4/5 | note:c3 ]", + "[ 4/5 → 6/5 | note:c3 ]", + "[ 6/5 → 7/5 | note:c3 ]", + "[ 7/5 → 9/5 | note:c3 ]", + "[ 9/5 → 11/5 | note:c3 ]", + "[ 11/5 → 12/5 | note:c3 ]", + "[ 12/5 → 14/5 | note:c3 ]", + "[ 14/5 → 16/5 | note:c3 ]", + "[ 16/5 → 17/5 | note:c3 ]", + "[ 17/5 → 19/5 | note:c3 ]", + "[ (19/5 → 4/1) ⇝ 21/5 | note:c3 ]", ] `; @@ -2975,6 +3582,39 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` ] `; +exports[`runs examples > example "euclidish" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | s:hh pan:0.5 ]", + "[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]", + "[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]", + "[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]", + "[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]", + "[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]", + "[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]", + "[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]", + "[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]", + "[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]", + "[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]", + "[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]", + "[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]", + "[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]", + "[ 2/1 → 25/12 | s:hh pan:1 ]", + "[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]", + "[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]", + "[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]", + "[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]", + "[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]", + "[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]", + "[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]", + "[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]", + "[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]", + "[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]", + "[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]", + "[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]", + "[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]", +] +`; + exports[`runs examples > example "every" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", @@ -3122,6 +3762,8 @@ exports[`runs examples > example "fast" example index 0 1`] = ` exports[`runs examples > example "fastChunk" example index 0 1`] = ` [ + "[ 0/1 → 1/4 | color:red note:0 ]", + "[ 1/4 → 1/2 | color:red note:1 ]", "[ 1/2 → 3/4 | note:E2 ]", "[ 3/4 → 1/1 | note:F2 ]", "[ 1/1 → 5/4 | note:G2 ]", @@ -3130,6 +3772,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = ` "[ 7/4 → 2/1 | note:C3 ]", "[ 2/1 → 9/4 | note:D3 ]", "[ 9/4 → 5/2 | note:D2 ]", + "[ 5/2 → 11/4 | color:red note:2 ]", + "[ 11/4 → 3/1 | color:red note:3 ]", "[ 3/1 → 13/4 | note:G2 ]", "[ 13/4 → 7/2 | note:A2 ]", "[ 7/2 → 15/4 | note:B2 ]", @@ -3410,6 +4054,144 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/16 → 1/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/8 → 3/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/16 → 1/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/4 → 5/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/16 → 3/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/8 → 7/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/16 → 1/2 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/2 → 9/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 9/16 → 5/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/8 → 11/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 11/16 → 3/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/4 → 13/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 13/16 → 7/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/8 → 15/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 15/16 → 1/1 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/1 → 17/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 17/16 → 9/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 9/8 → 19/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 19/16 → 5/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 5/4 → 21/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 21/16 → 11/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 11/8 → 23/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 23/16 → 3/2 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 3/2 → 25/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 25/16 → 13/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 13/8 → 27/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 27/16 → 7/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 7/4 → 29/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 29/16 → 15/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 15/8 → 31/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 31/16 → 2/1 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 2/1 → 33/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 33/16 → 17/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 17/8 → 35/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 35/16 → 9/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 9/4 → 37/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 37/16 → 19/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 19/8 → 39/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 39/16 → 5/2 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 5/2 → 41/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 41/16 → 21/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 21/8 → 43/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 43/16 → 11/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 11/4 → 45/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 45/16 → 23/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 23/8 → 47/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 47/16 → 3/1 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 3/1 → 49/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 49/16 → 25/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 25/8 → 51/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 51/16 → 13/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 13/4 → 53/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 53/16 → 27/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 27/8 → 55/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 55/16 → 7/2 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 7/2 → 57/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 57/16 → 29/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 29/8 → 59/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 59/16 → 15/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 15/4 → 61/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 61/16 → 31/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 31/8 → 63/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 63/16 → 4/1 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", +] +`; + +exports[`runs examples > example "fmwave" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/8 → 3/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/16 → 1/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/4 → 5/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/16 → 3/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/8 → 7/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/16 → 1/2 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/2 → 9/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/16 → 5/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/8 → 11/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/16 → 3/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/4 → 13/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/16 → 7/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/8 → 15/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/16 → 1/1 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/1 → 17/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/16 → 9/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/8 → 19/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/16 → 5/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/4 → 21/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/16 → 11/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/8 → 23/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/16 → 3/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/2 → 25/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/16 → 13/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/8 → 27/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/16 → 7/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/4 → 29/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/16 → 15/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/8 → 31/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/16 → 2/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 2/1 → 33/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 33/16 → 17/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/8 → 35/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 35/16 → 9/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/4 → 37/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 37/16 → 19/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/8 → 39/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 39/16 → 5/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/2 → 41/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 41/16 → 21/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/8 → 43/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 43/16 → 11/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/4 → 45/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 45/16 → 23/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/8 → 47/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 47/16 → 3/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/1 → 49/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 49/16 → 25/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/8 → 51/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 51/16 → 13/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/4 → 53/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 53/16 → 27/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/8 → 55/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 55/16 → 7/2 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/2 → 57/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 57/16 → 29/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/8 → 59/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 59/16 → 15/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/4 → 61/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 61/16 → 31/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/8 → 63/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 63/16 → 4/1 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", +] +`; + exports[`runs examples > example "focus" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:sd ]", @@ -4188,6 +4970,35 @@ exports[`runs examples > example "inside" example index 0 1`] = ` ] `; +exports[`runs examples > example "into" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd speed:2 ]", + "[ 1/8 → 1/4 | s:sd speed:2 ]", + "[ 1/4 → 3/8 | s:bd speed:2 ]", + "[ 3/8 → 1/2 | s:sd speed:2 ]", + "[ 1/2 → 3/4 | s:ht ]", + "[ 3/4 → 1/1 | s:lt ]", + "[ 1/1 → 9/8 | s:bd speed:2 ]", + "[ 9/8 → 5/4 | s:sd speed:2 ]", + "[ 5/4 → 11/8 | s:bd speed:2 ]", + "[ 11/8 → 3/2 | s:sd speed:2 ]", + "[ 3/2 → 7/4 | s:ht ]", + "[ 7/4 → 2/1 | s:lt ]", + "[ 2/1 → 17/8 | s:bd speed:2 ]", + "[ 17/8 → 9/4 | s:sd speed:2 ]", + "[ 9/4 → 19/8 | s:bd speed:2 ]", + "[ 19/8 → 5/2 | s:sd speed:2 ]", + "[ 5/2 → 11/4 | s:ht ]", + "[ 11/4 → 3/1 | s:lt ]", + "[ 3/1 → 25/8 | s:bd speed:2 ]", + "[ 25/8 → 13/4 | s:sd speed:2 ]", + "[ 13/4 → 27/8 | s:bd speed:2 ]", + "[ 27/8 → 7/2 | s:sd speed:2 ]", + "[ 7/2 → 15/4 | s:ht ]", + "[ 15/4 → 4/1 | s:lt ]", +] +`; + exports[`runs examples > example "invert" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", @@ -4240,6 +5051,43 @@ exports[`runs examples > example "irand" example index 0 1`] = ` ] `; +exports[`runs examples > example "irbegin" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", +] +`; + exports[`runs examples > example "iresponse" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]", @@ -4261,6 +5109,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = ` ] `; +exports[`runs examples > example "irspeed" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", +] +`; + exports[`runs examples > example "isaw" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 clip:1 ]", @@ -4813,6 +5698,49 @@ exports[`runs examples > example "linger" example index 0 1`] = ` ] `; +exports[`runs examples > example "lock" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | delay:{s:sd} lock:1 ]", + "[ 1/1 → 2/1 | delay:{s:sd} lock:1 ]", + "[ 2/1 → 3/1 | delay:{s:sd} lock:1 ]", + "[ 3/1 → 4/1 | delay:{s:sd} lock:1 ]", +] +`; + +exports[`runs examples > example "log" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:bd ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + +exports[`runs examples > example "logValues" example index 0 1`] = ` +[ + "[ (0/1 → 1/3) ⇝ 1/2 | s:bd gain:0.25 n:2 ]", + "[ 0/1 ⇜ (1/3 → 1/2) | s:bd gain:0.5 n:1 ]", + "[ (1/2 → 2/3) ⇝ 1/1 | s:sd gain:0.5 n:1 ]", + "[ 1/2 ⇜ (2/3 → 1/1) | s:sd gain:1 n:0 ]", + "[ (1/1 → 4/3) ⇝ 3/2 | s:bd gain:0.25 n:2 ]", + "[ 1/1 ⇜ (4/3 → 3/2) | s:bd gain:0.5 n:1 ]", + "[ (3/2 → 5/3) ⇝ 2/1 | s:sd gain:0.5 n:1 ]", + "[ 3/2 ⇜ (5/3 → 2/1) | s:sd gain:1 n:0 ]", + "[ (2/1 → 7/3) ⇝ 5/2 | s:bd gain:0.25 n:2 ]", + "[ 2/1 ⇜ (7/3 → 5/2) | s:bd gain:0.5 n:1 ]", + "[ (5/2 → 8/3) ⇝ 3/1 | s:sd gain:0.5 n:1 ]", + "[ 5/2 ⇜ (8/3 → 3/1) | s:sd gain:1 n:0 ]", + "[ (3/1 → 10/3) ⇝ 7/2 | s:bd gain:0.25 n:2 ]", + "[ 3/1 ⇜ (10/3 → 7/2) | s:bd gain:0.5 n:1 ]", + "[ (7/2 → 11/3) ⇝ 4/1 | s:sd gain:0.5 n:1 ]", + "[ 7/2 ⇜ (11/3 → 4/1) | s:sd gain:1 n:0 ]", +] +`; + exports[`runs examples > example "loop" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:casio loop:1 ]", @@ -5206,6 +6134,334 @@ exports[`runs examples > example "mask" example index 0 1`] = ` ] `; +exports[`runs examples > example "midi" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + +exports[`runs examples > example "midi" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + +exports[`runs examples > example "midibend" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midibend:0 ]", + "[ 1/1 → 2/1 | note:c4 midibend:0.4 ]", + "[ 2/1 → 3/1 | note:c4 midibend:1.1102230246251565e-16 ]", + "[ 3/1 → 4/1 | note:c4 midibend:-0.4 ]", +] +`; + +exports[`runs examples > example "midichan" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + +exports[`runs examples > example "midicmd" example index 0 1`] = ` +[ + "[ 0/1 → 1/48 | midicmd:clock ]", + "[ 0/1 → 2/1 | midicmd:start ]", + "[ 1/48 → 1/24 | midicmd:clock ]", + "[ 1/24 → 1/16 | midicmd:clock ]", + "[ 1/16 → 1/12 | midicmd:clock ]", + "[ 1/12 → 5/48 | midicmd:clock ]", + "[ 5/48 → 1/8 | midicmd:clock ]", + "[ 1/8 → 7/48 | midicmd:clock ]", + "[ 7/48 → 1/6 | midicmd:clock ]", + "[ 1/6 → 3/16 | midicmd:clock ]", + "[ 3/16 → 5/24 | midicmd:clock ]", + "[ 5/24 → 11/48 | midicmd:clock ]", + "[ 11/48 → 1/4 | midicmd:clock ]", + "[ 1/4 → 13/48 | midicmd:clock ]", + "[ 13/48 → 7/24 | midicmd:clock ]", + "[ 7/24 → 5/16 | midicmd:clock ]", + "[ 5/16 → 1/3 | midicmd:clock ]", + "[ 1/3 → 17/48 | midicmd:clock ]", + "[ 17/48 → 3/8 | midicmd:clock ]", + "[ 3/8 → 19/48 | midicmd:clock ]", + "[ 19/48 → 5/12 | midicmd:clock ]", + "[ 5/12 → 7/16 | midicmd:clock ]", + "[ 7/16 → 11/24 | midicmd:clock ]", + "[ 11/24 → 23/48 | midicmd:clock ]", + "[ 23/48 → 1/2 | midicmd:clock ]", + "[ 1/2 → 25/48 | midicmd:clock ]", + "[ 25/48 → 13/24 | midicmd:clock ]", + "[ 13/24 → 9/16 | midicmd:clock ]", + "[ 9/16 → 7/12 | midicmd:clock ]", + "[ 7/12 → 29/48 | midicmd:clock ]", + "[ 29/48 → 5/8 | midicmd:clock ]", + "[ 5/8 → 31/48 | midicmd:clock ]", + "[ 31/48 → 2/3 | midicmd:clock ]", + "[ 2/3 → 11/16 | midicmd:clock ]", + "[ 11/16 → 17/24 | midicmd:clock ]", + "[ 17/24 → 35/48 | midicmd:clock ]", + "[ 35/48 → 3/4 | midicmd:clock ]", + "[ 3/4 → 37/48 | midicmd:clock ]", + "[ 37/48 → 19/24 | midicmd:clock ]", + "[ 19/24 → 13/16 | midicmd:clock ]", + "[ 13/16 → 5/6 | midicmd:clock ]", + "[ 5/6 → 41/48 | midicmd:clock ]", + "[ 41/48 → 7/8 | midicmd:clock ]", + "[ 7/8 → 43/48 | midicmd:clock ]", + "[ 43/48 → 11/12 | midicmd:clock ]", + "[ 11/12 → 15/16 | midicmd:clock ]", + "[ 15/16 → 23/24 | midicmd:clock ]", + "[ 23/24 → 47/48 | midicmd:clock ]", + "[ 47/48 → 1/1 | midicmd:clock ]", + "[ 1/1 → 49/48 | midicmd:clock ]", + "[ 49/48 → 25/24 | midicmd:clock ]", + "[ 25/24 → 17/16 | midicmd:clock ]", + "[ 17/16 → 13/12 | midicmd:clock ]", + "[ 13/12 → 53/48 | midicmd:clock ]", + "[ 53/48 → 9/8 | midicmd:clock ]", + "[ 9/8 → 55/48 | midicmd:clock ]", + "[ 55/48 → 7/6 | midicmd:clock ]", + "[ 7/6 → 19/16 | midicmd:clock ]", + "[ 19/16 → 29/24 | midicmd:clock ]", + "[ 29/24 → 59/48 | midicmd:clock ]", + "[ 59/48 → 5/4 | midicmd:clock ]", + "[ 5/4 → 61/48 | midicmd:clock ]", + "[ 61/48 → 31/24 | midicmd:clock ]", + "[ 31/24 → 21/16 | midicmd:clock ]", + "[ 21/16 → 4/3 | midicmd:clock ]", + "[ 4/3 → 65/48 | midicmd:clock ]", + "[ 65/48 → 11/8 | midicmd:clock ]", + "[ 11/8 → 67/48 | midicmd:clock ]", + "[ 67/48 → 17/12 | midicmd:clock ]", + "[ 17/12 → 23/16 | midicmd:clock ]", + "[ 23/16 → 35/24 | midicmd:clock ]", + "[ 35/24 → 71/48 | midicmd:clock ]", + "[ 71/48 → 3/2 | midicmd:clock ]", + "[ 3/2 → 73/48 | midicmd:clock ]", + "[ 73/48 → 37/24 | midicmd:clock ]", + "[ 37/24 → 25/16 | midicmd:clock ]", + "[ 25/16 → 19/12 | midicmd:clock ]", + "[ 19/12 → 77/48 | midicmd:clock ]", + "[ 77/48 → 13/8 | midicmd:clock ]", + "[ 13/8 → 79/48 | midicmd:clock ]", + "[ 79/48 → 5/3 | midicmd:clock ]", + "[ 5/3 → 27/16 | midicmd:clock ]", + "[ 27/16 → 41/24 | midicmd:clock ]", + "[ 41/24 → 83/48 | midicmd:clock ]", + "[ 83/48 → 7/4 | midicmd:clock ]", + "[ 7/4 → 85/48 | midicmd:clock ]", + "[ 85/48 → 43/24 | midicmd:clock ]", + "[ 43/24 → 29/16 | midicmd:clock ]", + "[ 29/16 → 11/6 | midicmd:clock ]", + "[ 11/6 → 89/48 | midicmd:clock ]", + "[ 89/48 → 15/8 | midicmd:clock ]", + "[ 15/8 → 91/48 | midicmd:clock ]", + "[ 91/48 → 23/12 | midicmd:clock ]", + "[ 23/12 → 31/16 | midicmd:clock ]", + "[ 31/16 → 47/24 | midicmd:clock ]", + "[ 47/24 → 95/48 | midicmd:clock ]", + "[ 95/48 → 2/1 | midicmd:clock ]", + "[ 2/1 → 97/48 | midicmd:clock ]", + "[ 2/1 → 4/1 | midicmd:stop ]", + "[ 97/48 → 49/24 | midicmd:clock ]", + "[ 49/24 → 33/16 | midicmd:clock ]", + "[ 33/16 → 25/12 | midicmd:clock ]", + "[ 25/12 → 101/48 | midicmd:clock ]", + "[ 101/48 → 17/8 | midicmd:clock ]", + "[ 17/8 → 103/48 | midicmd:clock ]", + "[ 103/48 → 13/6 | midicmd:clock ]", + "[ 13/6 → 35/16 | midicmd:clock ]", + "[ 35/16 → 53/24 | midicmd:clock ]", + "[ 53/24 → 107/48 | midicmd:clock ]", + "[ 107/48 → 9/4 | midicmd:clock ]", + "[ 9/4 → 109/48 | midicmd:clock ]", + "[ 109/48 → 55/24 | midicmd:clock ]", + "[ 55/24 → 37/16 | midicmd:clock ]", + "[ 37/16 → 7/3 | midicmd:clock ]", + "[ 7/3 → 113/48 | midicmd:clock ]", + "[ 113/48 → 19/8 | midicmd:clock ]", + "[ 19/8 → 115/48 | midicmd:clock ]", + "[ 115/48 → 29/12 | midicmd:clock ]", + "[ 29/12 → 39/16 | midicmd:clock ]", + "[ 39/16 → 59/24 | midicmd:clock ]", + "[ 59/24 → 119/48 | midicmd:clock ]", + "[ 119/48 → 5/2 | midicmd:clock ]", + "[ 5/2 → 121/48 | midicmd:clock ]", + "[ 121/48 → 61/24 | midicmd:clock ]", + "[ 61/24 → 41/16 | midicmd:clock ]", + "[ 41/16 → 31/12 | midicmd:clock ]", + "[ 31/12 → 125/48 | midicmd:clock ]", + "[ 125/48 → 21/8 | midicmd:clock ]", + "[ 21/8 → 127/48 | midicmd:clock ]", + "[ 127/48 → 8/3 | midicmd:clock ]", + "[ 8/3 → 43/16 | midicmd:clock ]", + "[ 43/16 → 65/24 | midicmd:clock ]", + "[ 65/24 → 131/48 | midicmd:clock ]", + "[ 131/48 → 11/4 | midicmd:clock ]", + "[ 11/4 → 133/48 | midicmd:clock ]", + "[ 133/48 → 67/24 | midicmd:clock ]", + "[ 67/24 → 45/16 | midicmd:clock ]", + "[ 45/16 → 17/6 | midicmd:clock ]", + "[ 17/6 → 137/48 | midicmd:clock ]", + "[ 137/48 → 23/8 | midicmd:clock ]", + "[ 23/8 → 139/48 | midicmd:clock ]", + "[ 139/48 → 35/12 | midicmd:clock ]", + "[ 35/12 → 47/16 | midicmd:clock ]", + "[ 47/16 → 71/24 | midicmd:clock ]", + "[ 71/24 → 143/48 | midicmd:clock ]", + "[ 143/48 → 3/1 | midicmd:clock ]", + "[ 3/1 → 145/48 | midicmd:clock ]", + "[ 145/48 → 73/24 | midicmd:clock ]", + "[ 73/24 → 49/16 | midicmd:clock ]", + "[ 49/16 → 37/12 | midicmd:clock ]", + "[ 37/12 → 149/48 | midicmd:clock ]", + "[ 149/48 → 25/8 | midicmd:clock ]", + "[ 25/8 → 151/48 | midicmd:clock ]", + "[ 151/48 → 19/6 | midicmd:clock ]", + "[ 19/6 → 51/16 | midicmd:clock ]", + "[ 51/16 → 77/24 | midicmd:clock ]", + "[ 77/24 → 155/48 | midicmd:clock ]", + "[ 155/48 → 13/4 | midicmd:clock ]", + "[ 13/4 → 157/48 | midicmd:clock ]", + "[ 157/48 → 79/24 | midicmd:clock ]", + "[ 79/24 → 53/16 | midicmd:clock ]", + "[ 53/16 → 10/3 | midicmd:clock ]", + "[ 10/3 → 161/48 | midicmd:clock ]", + "[ 161/48 → 27/8 | midicmd:clock ]", + "[ 27/8 → 163/48 | midicmd:clock ]", + "[ 163/48 → 41/12 | midicmd:clock ]", + "[ 41/12 → 55/16 | midicmd:clock ]", + "[ 55/16 → 83/24 | midicmd:clock ]", + "[ 83/24 → 167/48 | midicmd:clock ]", + "[ 167/48 → 7/2 | midicmd:clock ]", + "[ 7/2 → 169/48 | midicmd:clock ]", + "[ 169/48 → 85/24 | midicmd:clock ]", + "[ 85/24 → 57/16 | midicmd:clock ]", + "[ 57/16 → 43/12 | midicmd:clock ]", + "[ 43/12 → 173/48 | midicmd:clock ]", + "[ 173/48 → 29/8 | midicmd:clock ]", + "[ 29/8 → 175/48 | midicmd:clock ]", + "[ 175/48 → 11/3 | midicmd:clock ]", + "[ 11/3 → 59/16 | midicmd:clock ]", + "[ 59/16 → 89/24 | midicmd:clock ]", + "[ 89/24 → 179/48 | midicmd:clock ]", + "[ 179/48 → 15/4 | midicmd:clock ]", + "[ 15/4 → 181/48 | midicmd:clock ]", + "[ 181/48 → 91/24 | midicmd:clock ]", + "[ 91/24 → 61/16 | midicmd:clock ]", + "[ 61/16 → 23/6 | midicmd:clock ]", + "[ 23/6 → 185/48 | midicmd:clock ]", + "[ 185/48 → 31/8 | midicmd:clock ]", + "[ 31/8 → 187/48 | midicmd:clock ]", + "[ 187/48 → 47/12 | midicmd:clock ]", + "[ 47/12 → 63/16 | midicmd:clock ]", + "[ 63/16 → 95/24 | midicmd:clock ]", + "[ 95/24 → 191/48 | midicmd:clock ]", + "[ 191/48 → 4/1 | midicmd:clock ]", +] +`; + +exports[`runs examples > example "midin" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/4 → 1/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/2 → 3/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/4 → 1/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/1 → 5/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 5/4 → 3/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/2 → 7/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 7/4 → 2/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 2/1 → 9/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 9/4 → 5/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 5/2 → 11/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 11/4 → 3/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/1 → 13/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 13/4 → 7/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 7/2 → 15/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 15/4 → 4/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", +] +`; + +exports[`runs examples > example "midiport" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c midiport:0 ]", + "[ 1/4 → 1/2 | note:a midiport:0 ]", + "[ 1/2 → 3/4 | note:f midiport:0 ]", + "[ 3/4 → 1/1 | note:e midiport:0 ]", + "[ 1/1 → 5/4 | note:c midiport:1 ]", + "[ 5/4 → 3/2 | note:a midiport:1 ]", + "[ 3/2 → 7/4 | note:f midiport:1 ]", + "[ 7/4 → 2/1 | note:e midiport:1 ]", + "[ 2/1 → 9/4 | note:c midiport:2 ]", + "[ 9/4 → 5/2 | note:a midiport:2 ]", + "[ 5/2 → 11/4 | note:f midiport:2 ]", + "[ 11/4 → 3/1 | note:e midiport:2 ]", + "[ 3/1 → 13/4 | note:c midiport:3 ]", + "[ 13/4 → 7/2 | note:a midiport:3 ]", + "[ 7/2 → 15/4 | note:f midiport:3 ]", + "[ 15/4 → 4/1 | note:e midiport:3 ]", +] +`; + +exports[`runs examples > example "miditouch" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]", + "[ 1/1 → 2/1 | note:c4 miditouch:1 ]", + "[ 2/1 → 3/1 | note:c4 miditouch:0.5000000000000001 ]", + "[ 3/1 → 4/1 | note:c4 miditouch:0 ]", +] +`; + +exports[`runs examples > example "morph" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 25/112 → 39/112 | s:hh ]", + "[ 27/56 → 17/28 | s:hh ]", + "[ 83/112 → 97/112 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 137/112 → 151/112 | s:hh ]", + "[ 83/56 → 45/28 | s:hh ]", + "[ 195/112 → 209/112 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 249/112 → 263/112 | s:hh ]", + "[ 139/56 → 73/28 | s:hh ]", + "[ 307/112 → 321/112 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 361/112 → 375/112 | s:hh ]", + "[ 195/56 → 101/28 | s:hh ]", + "[ 419/112 → 433/112 | s:hh ]", +] +`; + +exports[`runs examples > example "morph" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 11/56 → 9/28 | s:hh ]", + "[ 13/28 → 33/56 | s:hh ]", + "[ 41/56 → 6/7 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 303934523/262340932 → 673454279/524681864 | s:hh ]", + "[ 188758485/131170466 → 820619173/524681864 | s:hh ]", + "[ 451099417/262340932 → 967784067/524681864 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 15/7 → 127/56 | s:hh ]", + "[ 17/7 → 143/56 | s:hh ]", + "[ 19/7 → 159/56 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 828616387/262340932 → 1722818007/524681864 | s:hh ]", + "[ 451099417/131170466 → 1869982901/524681864 | s:hh ]", + "[ 975781281/262340932 → 2017147795/524681864 | s:hh ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", @@ -5426,6 +6682,45 @@ exports[`runs examples > example "note" example index 2 1`] = ` ] `; +exports[`runs examples > example "note" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:fbb1 s:saw ]", + "[ 1/4 → 1/2 | note:a#0 s:saw ]", + "[ 1/2 → 3/4 | note:cbbb-1 s:saw ]", + "[ 3/4 → 1/1 | note:e##-2 s:saw ]", + "[ 1/1 → 5/4 | note:fbb1 s:saw ]", + "[ 5/4 → 3/2 | note:a#0 s:saw ]", + "[ 3/2 → 7/4 | note:cbbb-1 s:saw ]", + "[ 7/4 → 2/1 | note:e##-2 s:saw ]", + "[ 2/1 → 9/4 | note:fbb1 s:saw ]", + "[ 9/4 → 5/2 | note:a#0 s:saw ]", + "[ 5/2 → 11/4 | note:cbbb-1 s:saw ]", + "[ 11/4 → 3/1 | note:e##-2 s:saw ]", + "[ 3/1 → 13/4 | note:fbb1 s:saw ]", + "[ 13/4 → 7/2 | note:a#0 s:saw ]", + "[ 7/2 → 15/4 | note:cbbb-1 s:saw ]", + "[ 15/4 → 4/1 | note:e##-2 s:saw ]", +] +`; + +exports[`runs examples > example "nrpnn" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", +] +`; + +exports[`runs examples > example "nrpv" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", +] +`; + exports[`runs examples > example "octave" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", @@ -5513,6 +6808,43 @@ exports[`runs examples > example "often" example index 0 1`] = ` ] `; +exports[`runs examples > example "onTriggerTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 1/4 → 3/8 | s:bd ]", + "[ 3/8 → 1/2 | s:bd ]", + "[ 1/2 → 5/8 | s:bd ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 7/8 → 1/1 | s:bd ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 9/8 → 5/4 | s:bd ]", + "[ 5/4 → 11/8 | s:bd ]", + "[ 11/8 → 3/2 | s:bd ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 15/8 → 2/1 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 17/8 → 9/4 | s:bd ]", + "[ 9/4 → 19/8 | s:bd ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 21/8 → 11/4 | s:bd ]", + "[ 11/4 → 23/8 | s:bd ]", + "[ 23/8 → 3/1 | s:bd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 25/8 → 13/4 | s:bd ]", + "[ 13/4 → 27/8 | s:bd ]", + "[ 27/8 → 7/2 | s:bd ]", + "[ 7/2 → 29/8 | s:bd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", + "[ 31/8 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "orbit" example index 0 1`] = ` [ "[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]", @@ -6176,69 +7508,114 @@ exports[`runs examples > example "ply" example index 0 1`] = ` ] `; -exports[`runs examples > example "polymeter" example index 0 1`] = ` +exports[`runs examples > example "plyForEach" example index 0 1`] = ` [ - "[ 0/1 → 1/3 | note:c ]", - "[ 0/1 → 1/3 | note:c2 ]", - "[ 1/3 → 2/3 | note:eb ]", - "[ 1/3 → 2/3 | note:g2 ]", - "[ 2/3 → 1/1 | note:g ]", - "[ 2/3 → 1/1 | note:c2 ]", - "[ 1/1 → 4/3 | note:c ]", - "[ 1/1 → 4/3 | note:g2 ]", - "[ 4/3 → 5/3 | note:eb ]", - "[ 4/3 → 5/3 | note:c2 ]", - "[ 5/3 → 2/1 | note:g ]", - "[ 5/3 → 2/1 | note:g2 ]", - "[ 2/1 → 7/3 | note:c ]", - "[ 2/1 → 7/3 | note:c2 ]", - "[ 7/3 → 8/3 | note:eb ]", - "[ 7/3 → 8/3 | note:g2 ]", - "[ 8/3 → 3/1 | note:g ]", - "[ 8/3 → 3/1 | note:c2 ]", - "[ 3/1 → 10/3 | note:c ]", - "[ 3/1 → 10/3 | note:g2 ]", - "[ 10/3 → 11/3 | note:eb ]", - "[ 10/3 → 11/3 | note:c2 ]", - "[ 11/3 → 4/1 | note:g ]", - "[ 11/3 → 4/1 | note:g2 ]", + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", ] `; -exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` +exports[`runs examples > example "plyWith" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c ]", - "[ 0/1 → 1/4 | note:e ]", - "[ 1/4 → 1/2 | note:d ]", - "[ 1/4 → 1/2 | note:f ]", - "[ 1/2 → 3/4 | note:c ]", - "[ 1/2 → 3/4 | note:g ]", - "[ 3/4 → 1/1 | note:d ]", - "[ 3/4 → 1/1 | note:e ]", - "[ 1/1 → 5/4 | note:c ]", - "[ 1/1 → 5/4 | note:f ]", - "[ 5/4 → 3/2 | note:d ]", - "[ 5/4 → 3/2 | note:g ]", - "[ 3/2 → 7/4 | note:c ]", - "[ 3/2 → 7/4 | note:e ]", - "[ 7/4 → 2/1 | note:d ]", - "[ 7/4 → 2/1 | note:f ]", - "[ 2/1 → 9/4 | note:c ]", - "[ 2/1 → 9/4 | note:g ]", - "[ 9/4 → 5/2 | note:d ]", - "[ 9/4 → 5/2 | note:e ]", - "[ 5/2 → 11/4 | note:c ]", - "[ 5/2 → 11/4 | note:f ]", - "[ 11/4 → 3/1 | note:d ]", - "[ 11/4 → 3/1 | note:g ]", - "[ 3/1 → 13/4 | note:c ]", - "[ 3/1 → 13/4 | note:e ]", - "[ 13/4 → 7/2 | note:d ]", - "[ 13/4 → 7/2 | note:f ]", - "[ 7/2 → 15/4 | note:c ]", - "[ 7/2 → 15/4 | note:g ]", - "[ 15/4 → 4/1 | note:d ]", - "[ 15/4 → 4/1 | note:e ]", + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "polymeter" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:c ]", + "[ 0/1 → 1/6 | note:c2 ]", + "[ 1/6 → 1/3 | note:eb ]", + "[ 1/6 → 1/3 | note:g2 ]", + "[ 1/3 → 1/2 | note:g ]", + "[ 1/3 → 1/2 | note:c2 ]", + "[ 1/2 → 2/3 | note:c ]", + "[ 1/2 → 2/3 | note:g2 ]", + "[ 2/3 → 5/6 | note:eb ]", + "[ 2/3 → 5/6 | note:c2 ]", + "[ 5/6 → 1/1 | note:g ]", + "[ 5/6 → 1/1 | note:g2 ]", + "[ 1/1 → 7/6 | note:c ]", + "[ 1/1 → 7/6 | note:c2 ]", + "[ 7/6 → 4/3 | note:eb ]", + "[ 7/6 → 4/3 | note:g2 ]", + "[ 4/3 → 3/2 | note:g ]", + "[ 4/3 → 3/2 | note:c2 ]", + "[ 3/2 → 5/3 | note:c ]", + "[ 3/2 → 5/3 | note:g2 ]", + "[ 5/3 → 11/6 | note:eb ]", + "[ 5/3 → 11/6 | note:c2 ]", + "[ 11/6 → 2/1 | note:g ]", + "[ 11/6 → 2/1 | note:g2 ]", + "[ 2/1 → 13/6 | note:c ]", + "[ 2/1 → 13/6 | note:c2 ]", + "[ 13/6 → 7/3 | note:eb ]", + "[ 13/6 → 7/3 | note:g2 ]", + "[ 7/3 → 5/2 | note:g ]", + "[ 7/3 → 5/2 | note:c2 ]", + "[ 5/2 → 8/3 | note:c ]", + "[ 5/2 → 8/3 | note:g2 ]", + "[ 8/3 → 17/6 | note:eb ]", + "[ 8/3 → 17/6 | note:c2 ]", + "[ 17/6 → 3/1 | note:g ]", + "[ 17/6 → 3/1 | note:g2 ]", + "[ 3/1 → 19/6 | note:c ]", + "[ 3/1 → 19/6 | note:c2 ]", + "[ 19/6 → 10/3 | note:eb ]", + "[ 19/6 → 10/3 | note:g2 ]", + "[ 10/3 → 7/2 | note:g ]", + "[ 10/3 → 7/2 | note:c2 ]", + "[ 7/2 → 11/3 | note:c ]", + "[ 7/2 → 11/3 | note:g2 ]", + "[ 11/3 → 23/6 | note:eb ]", + "[ 11/3 → 23/6 | note:c2 ]", + "[ 23/6 → 4/1 | note:g ]", + "[ 23/6 → 4/1 | note:g2 ]", ] `; @@ -6346,6 +7723,15 @@ exports[`runs examples > example "pressBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "progNum" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 progNum:10 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 progNum:10 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 progNum:10 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 progNum:10 midichan:1 ]", +] +`; + exports[`runs examples > example "pure" example index 0 1`] = ` [ "[ 0/1 → 1/1 | e4 ]", @@ -6355,6 +7741,186 @@ exports[`runs examples > example "pure" example index 0 1`] = ` ] `; +exports[`runs examples > example "pw" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/16 → 1/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/8 → 3/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/16 → 1/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/4 → 5/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/16 → 3/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/8 → 7/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/16 → 1/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/2 → 9/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/16 → 5/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/8 → 11/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/16 → 3/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/4 → 13/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/16 → 7/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/8 → 15/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/16 → 1/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/1 → 17/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 17/16 → 9/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/8 → 19/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 19/16 → 5/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/4 → 21/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 21/16 → 11/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/8 → 23/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 23/16 → 3/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/2 → 25/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 25/16 → 13/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/8 → 27/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 27/16 → 7/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/4 → 29/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 29/16 → 15/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/8 → 31/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 31/16 → 2/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 2/1 → 33/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 33/16 → 17/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 17/8 → 35/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 35/16 → 9/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/4 → 37/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 37/16 → 19/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 19/8 → 39/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 39/16 → 5/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/2 → 41/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 41/16 → 21/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 21/8 → 43/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 43/16 → 11/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/4 → 45/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 45/16 → 23/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 23/8 → 47/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 47/16 → 3/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/1 → 49/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 49/16 → 25/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 25/8 → 51/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 51/16 → 13/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/4 → 53/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 53/16 → 27/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 27/8 → 55/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 55/16 → 7/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/2 → 57/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 57/16 → 29/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 29/8 → 59/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 59/16 → 15/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/4 → 61/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 61/16 → 31/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 31/8 → 63/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 63/16 → 4/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", +] +`; + +exports[`runs examples > example "pw" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.75 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:1 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:1 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.75 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:1 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:1 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.75 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:1 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:1 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.75 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:1 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:1 ]", +] +`; + +exports[`runs examples > example "pwrate" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", +] +`; + +exports[`runs examples > example "pwsweep" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", +] +`; + exports[`runs examples > example "queryArc" example index 0 1`] = `[]`; exports[`runs examples > example "rand" example index 0 1`] = ` @@ -6790,35 +8356,76 @@ exports[`runs examples > example "rev" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 0 1`] = ` [ - "[ 0/1 → 1/2 | note:d ]", - "[ 1/2 → 1/1 | note:e ]", - "[ 1/1 → 3/2 | note:d ]", - "[ 3/2 → 2/1 | note:e ]", - "[ 2/1 → 5/2 | note:d ]", - "[ 5/2 → 3/1 | note:e ]", - "[ 3/1 → 7/2 | note:d ]", - "[ 7/2 → 4/1 | note:e ]", + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", ] `; exports[`runs examples > example "ribbon" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | note:G3 ]", + "[ 0/1 → 1/4 | note:A3 ]", "[ 1/4 → 1/2 | note:C3 ]", "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:F3 ]", - "[ 1/1 → 5/4 | note:Eb3 ]", - "[ 5/4 → 3/2 | note:Ab3 ]", - "[ 3/2 → 7/4 | note:G3 ]", - "[ 7/4 → 2/1 | note:C4 ]", - "[ 2/1 → 9/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", "[ 9/4 → 5/2 | note:C3 ]", "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:F3 ]", - "[ 3/1 → 13/4 | note:Eb3 ]", - "[ 13/4 → 7/2 | note:Ab3 ]", - "[ 7/2 → 15/4 | note:G3 ]", - "[ 15/4 → 4/1 | note:C4 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "ribbon" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", ] `; @@ -7384,6 +8991,108 @@ exports[`runs examples > example "scale" example index 2 1`] = ` ] `; +exports[`runs examples > example "scale" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 s:piano ]", + "[ 0/1 → 1/4 | note:B3 s:piano ]", + "[ 1/4 → 3/8 | note:Gb2 s:piano ]", + "[ 3/8 → 1/2 | note:F2 s:piano ]", + "[ 1/2 → 3/4 | note:A2 s:piano ]", + "[ 1/2 → 3/4 | note:D4 s:piano ]", + "[ 3/4 → 1/1 | note:G3 s:piano ]", + "[ 1/1 → 5/4 | note:C3 s:piano ]", + "[ 1/1 → 5/4 | note:C4 s:piano ]", + "[ 5/4 → 11/8 | note:Gb2 s:piano ]", + "[ 11/8 → 3/2 | note:E2 s:piano ]", + "[ 3/2 → 7/4 | note:A2 s:piano ]", + "[ 3/2 → 7/4 | note:Eb4 s:piano ]", + "[ 7/4 → 2/1 | note:F#3 s:piano ]", + "[ 2/1 → 9/4 | note:C3 s:piano ]", + "[ 2/1 → 9/4 | note:B3 s:piano ]", + "[ 9/4 → 19/8 | note:Gb2 s:piano ]", + "[ 19/8 → 5/2 | note:F2 s:piano ]", + "[ 5/2 → 11/4 | note:Ab2 s:piano ]", + "[ 5/2 → 11/4 | note:D4 s:piano ]", + "[ 11/4 → 3/1 | note:G3 s:piano ]", + "[ 3/1 → 13/4 | note:C3 s:piano ]", + "[ 3/1 → 13/4 | note:C4 s:piano ]", + "[ 13/4 → 27/8 | note:Gb2 s:piano ]", + "[ 27/8 → 7/2 | note:E2 s:piano ]", + "[ 7/2 → 15/4 | note:Ab2 s:piano ]", + "[ 7/2 → 15/4 | note:Eb4 s:piano ]", + "[ 15/4 → 4/1 | note:F#3 s:piano ]", +] +`; + +exports[`runs examples > example "scale" example index 4 1`] = ` +[ + "[ 0/1 → 1/16 | note:Gb1 ]", + "[ 1/16 → 1/8 | note:Gb1 ]", + "[ 1/8 → 3/16 | note:Gb1 ]", + "[ 3/16 → 1/4 | note:Gb1 ]", + "[ 1/4 → 5/16 | note:Gb1 ]", + "[ 5/16 → 3/8 | note:Gb1 ]", + "[ 3/8 → 7/16 | note:Gb1 ]", + "[ 7/16 → 1/2 | note:Gb1 ]", + "[ 1/2 → 9/16 | note:Gb1 ]", + "[ 9/16 → 5/8 | note:Gb1 ]", + "[ 5/8 → 11/16 | note:Gb1 ]", + "[ 11/16 → 3/4 | note:Gb1 ]", + "[ 3/4 → 13/16 | note:Gb1 ]", + "[ 13/16 → 7/8 | note:Gb1 ]", + "[ 7/8 → 15/16 | note:Gb1 ]", + "[ 15/16 → 1/1 | note:Gb1 ]", + "[ 1/1 → 17/16 | note:Cb3 ]", + "[ 17/16 → 9/8 | note:Cb3 ]", + "[ 9/8 → 19/16 | note:Cb3 ]", + "[ 19/16 → 5/4 | note:Cb3 ]", + "[ 5/4 → 21/16 | note:Cb3 ]", + "[ 21/16 → 11/8 | note:Cb3 ]", + "[ 11/8 → 23/16 | note:Cb3 ]", + "[ 23/16 → 3/2 | note:Cb3 ]", + "[ 3/2 → 25/16 | note:Cb3 ]", + "[ 25/16 → 13/8 | note:Cb3 ]", + "[ 13/8 → 27/16 | note:Cb3 ]", + "[ 27/16 → 7/4 | note:Cb3 ]", + "[ 7/4 → 29/16 | note:Cb3 ]", + "[ 29/16 → 15/8 | note:Cb3 ]", + "[ 15/8 → 31/16 | note:Cb3 ]", + "[ 31/16 → 2/1 | note:Cb3 ]", + "[ 2/1 → 33/16 | note:Eb4 ]", + "[ 33/16 → 17/8 | note:Eb4 ]", + "[ 17/8 → 35/16 | note:Eb4 ]", + "[ 35/16 → 9/4 | note:Eb4 ]", + "[ 9/4 → 37/16 | note:Eb4 ]", + "[ 37/16 → 19/8 | note:Eb4 ]", + "[ 19/8 → 39/16 | note:Eb4 ]", + "[ 39/16 → 5/2 | note:Eb4 ]", + "[ 5/2 → 41/16 | note:Eb4 ]", + "[ 41/16 → 21/8 | note:Eb4 ]", + "[ 21/8 → 43/16 | note:Eb4 ]", + "[ 43/16 → 11/4 | note:Eb4 ]", + "[ 11/4 → 45/16 | note:Eb4 ]", + "[ 45/16 → 23/8 | note:Eb4 ]", + "[ 23/8 → 47/16 | note:Eb4 ]", + "[ 47/16 → 3/1 | note:Eb4 ]", + "[ 3/1 → 49/16 | note:Db2 ]", + "[ 49/16 → 25/8 | note:Db2 ]", + "[ 25/8 → 51/16 | note:Db2 ]", + "[ 51/16 → 13/4 | note:Db2 ]", + "[ 13/4 → 53/16 | note:Db2 ]", + "[ 53/16 → 27/8 | note:Db2 ]", + "[ 27/8 → 55/16 | note:Db2 ]", + "[ 55/16 → 7/2 | note:Db2 ]", + "[ 7/2 → 57/16 | note:Db2 ]", + "[ 57/16 → 29/8 | note:Db2 ]", + "[ 29/8 → 59/16 | note:Db2 ]", + "[ 59/16 → 15/4 | note:Db2 ]", + "[ 15/4 → 61/16 | note:Db2 ]", + "[ 61/16 → 31/8 | note:Db2 ]", + "[ 31/8 → 63/16 | note:Db2 ]", + "[ 63/16 → 4/1 | note:Db2 ]", +] +`; + exports[`runs examples > example "scaleTranspose" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]", @@ -7414,9 +9123,7 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7437,9 +9144,7 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", @@ -7464,6 +9169,52 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "scrub" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", +] +`; + +exports[`runs examples > example "scrub" example index 1 1`] = ` +[ + "[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40 ]", @@ -7647,6 +9398,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "setcpm" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:sd bank:tr707 ]", + "[ 1/2 → 3/4 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:sd bank:tr707 ]", + "[ 1/1 → 5/4 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:sd bank:tr707 ]", + "[ 3/2 → 7/4 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:sd bank:tr707 ]", + "[ 2/1 → 9/4 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:sd bank:tr707 ]", + "[ 5/2 → 11/4 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:sd bank:tr707 ]", + "[ 3/1 → 13/4 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:sd bank:tr707 ]", + "[ 7/2 → 15/4 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:sd bank:tr707 ]", +] +`; + exports[`runs examples > example "shape" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh shape:0 ]", @@ -7848,52 +9628,48 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 0 1`] = ` +exports[`runs examples > example "shuffle" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 0/1 → 1/4 | note:e s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", - "[ 1/2 → 3/4 | note:e s:piano ]", - "[ 3/4 → 1/1 | note:f s:piano ]", - "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:d s:piano ]", - "[ 3/2 → 7/4 | note:e s:piano ]", - "[ 7/4 → 2/1 | note:f s:piano ]", - "[ 2/1 → 9/4 | note:c s:piano ]", - "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:f s:piano ]", + "[ 3/4 → 1/1 | note:c s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:c s:piano ]", + "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 7/4 → 2/1 | note:d s:piano ]", + "[ 2/1 → 9/4 | note:d s:piano ]", + "[ 9/4 → 5/2 | note:c s:piano ]", "[ 5/2 → 11/4 | note:e s:piano ]", "[ 11/4 → 3/1 | note:f s:piano ]", "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:d s:piano ]", - "[ 7/2 → 15/4 | note:e s:piano ]", - "[ 15/4 → 4/1 | note:f s:piano ]", + "[ 13/4 → 7/2 | note:e s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:d s:piano ]", ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 1 1`] = ` +exports[`runs examples > example "shuffle" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:c s:piano ]", + "[ 0/1 → 1/8 | note:e s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", - "[ 1/4 → 3/8 | note:e s:piano ]", - "[ 3/8 → 1/2 | note:f s:piano ]", + "[ 1/4 → 3/8 | note:f s:piano ]", + "[ 3/8 → 1/2 | note:c s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:d s:piano ]", - "[ 5/4 → 11/8 | note:e s:piano ]", - "[ 11/8 → 3/2 | note:f s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:f s:piano ]", + "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:c s:piano ]", - "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 2/1 → 17/8 | note:d s:piano ]", + "[ 17/8 → 9/4 | note:c s:piano ]", "[ 9/4 → 19/8 | note:e s:piano ]", "[ 19/8 → 5/2 | note:f s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:d s:piano ]", - "[ 13/4 → 27/8 | note:e s:piano ]", - "[ 27/8 → 7/2 | note:f s:piano ]", + "[ 25/8 → 13/4 | note:e s:piano ]", + "[ 13/4 → 27/8 | note:f s:piano ]", + "[ 27/8 → 7/2 | note:d s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -8970,6 +10746,33 @@ exports[`runs examples > example "swingBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "sysex" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + +exports[`runs examples > example "sysexdata" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + +exports[`runs examples > example "sysexid" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + exports[`runs examples > example "take" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd ]", @@ -9124,6 +10927,420 @@ exports[`runs examples > example "transpose" example index 1 1`] = ` ] `; +exports[`runs examples > example "tremolo" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolodepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/16 → 1/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/8 → 3/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/16 → 1/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/4 → 5/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/16 → 3/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/8 → 7/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/16 → 1/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/2 → 9/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 9/16 → 5/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/8 → 11/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 11/16 → 3/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/4 → 13/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/16 → 7/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/8 → 15/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/16 → 1/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/1 → 17/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 17/16 → 9/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 9/8 → 19/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 19/16 → 5/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 5/4 → 21/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 21/16 → 11/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 11/8 → 23/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 23/16 → 3/2 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 3/2 → 25/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 25/16 → 13/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 13/8 → 27/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 27/16 → 7/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 7/4 → 29/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 29/16 → 15/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 15/8 → 31/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 31/16 → 2/1 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 2/1 → 33/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 33/16 → 17/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 17/8 → 35/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 35/16 → 9/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 9/4 → 37/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 37/16 → 19/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 19/8 → 39/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 39/16 → 5/2 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 5/2 → 41/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 41/16 → 21/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 21/8 → 43/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 43/16 → 11/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 11/4 → 45/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 45/16 → 23/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 23/8 → 47/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 47/16 → 3/1 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 3/1 → 49/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 49/16 → 25/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 25/8 → 51/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 51/16 → 13/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/4 → 53/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 53/16 → 27/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 27/8 → 55/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 55/16 → 7/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/2 → 57/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 57/16 → 29/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 29/8 → 59/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 59/16 → 15/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/4 → 61/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 61/16 → 31/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 31/8 → 63/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 63/16 → 4/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", +] +`; + +exports[`runs examples > example "tremolophase" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", +] +`; + +exports[`runs examples > example "tremoloshape" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/16 → 1/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/8 → 3/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/16 → 1/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/4 → 5/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/16 → 3/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/8 → 7/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/16 → 1/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/2 → 9/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 9/16 → 5/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/8 → 11/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 11/16 → 3/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/4 → 13/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/16 → 7/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/8 → 15/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/16 → 1/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/1 → 17/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 17/16 → 9/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 9/8 → 19/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 19/16 → 5/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 5/4 → 21/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 21/16 → 11/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 11/8 → 23/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 23/16 → 3/2 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 3/2 → 25/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 25/16 → 13/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 13/8 → 27/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 27/16 → 7/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 7/4 → 29/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 29/16 → 15/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 15/8 → 31/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 31/16 → 2/1 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 2/1 → 33/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 33/16 → 17/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 17/8 → 35/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 35/16 → 9/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 9/4 → 37/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 37/16 → 19/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 19/8 → 39/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 39/16 → 5/2 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 5/2 → 41/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 41/16 → 21/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 21/8 → 43/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 43/16 → 11/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 11/4 → 45/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 45/16 → 23/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 23/8 → 47/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 47/16 → 3/1 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 3/1 → 49/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 49/16 → 25/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 25/8 → 51/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 51/16 → 13/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/4 → 53/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 53/16 → 27/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 27/8 → 55/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 55/16 → 7/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/2 → 57/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 57/16 → 29/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 29/8 → 59/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 59/16 → 15/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/4 → 61/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 61/16 → 31/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 31/8 → 63/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 63/16 → 4/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", +] +`; + +exports[`runs examples > example "tremoloskew" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolosync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", +] +`; + exports[`runs examples > example "tri" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:C3 ]", @@ -9572,6 +11789,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = ` ] `; +exports[`runs examples > example "warp" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", +] +`; + +exports[`runs examples > example "warpmode" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", + "[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", +] +`; + exports[`runs examples > example "wchoose" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c2 s:sine ]", @@ -9776,6 +12099,128 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; +exports[`runs examples > example "wt" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", +] +`; + +exports[`runs examples > example "wtphaserand" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 06267b8f9..4a0ddd539 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7318,12 +7318,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` [ "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.5989903202280402 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.8369929669424891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.9213038925081491 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.8426077850162983 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.7006962578743696 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.6507943943142891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", @@ -7335,12 +7335,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.8758113365620375 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.7597710825502872 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.7743164440616965 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.8362447572872043 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.5914018759503961 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932272 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", @@ -7352,12 +7352,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.7347871446982026 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.9797635599970818 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.6662392104044557 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.9516951469704509 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.9182533202692866 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.5711571052670479 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", @@ -7369,12 +7369,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.9456470254808664 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.6086445553228259 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.5062594395130873 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.6716219391673803 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.7043459005653858 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.8878388572484255 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index b0a329a47..2a29de3f5 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio'; import { mini, m } from '@strudel/mini/mini.mjs'; // import * as voicingHelpers from '@strudel/tonal/voicings.mjs'; // import euclid from '@strudel/core/euclid.mjs'; -// import '@strudel/midi/midi.mjs'; +//import '@strudel/midi/midi.mjs'; import * as tonalHelpers from '@strudel/tonal'; import '@strudel/xen/xen.mjs'; // import '@strudel/xen/tune.mjs'; @@ -21,6 +21,9 @@ import '@strudel/xen/xen.mjs'; // import '@strudel/webaudio/webaudio.mjs'; // import '@strudel/serial/serial.mjs'; import '../website/src/repl/piano'; +//import * as motionHelpers from '../packages/motion/index.mjs'; +//import * as geolocationHelpers from '../packages/geolocation/index.mjs'; +import * as gamepadHelpers from '../packages/gamepad/index.mjs'; class MockedNode { chain() { @@ -94,6 +97,7 @@ const toneHelpersMocked = { '_pianoroll', '_spectrum', 'markcss', + 'p', ].forEach((mock) => { strudel.Pattern.prototype[mock] = function () { return this; @@ -123,6 +127,12 @@ const loadCsound = () => {}; const loadCSound = () => {}; const loadcsound = () => {}; +const midin = () => { + return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0 +}; + +const sysex = ([id, data]) => {}; + // TODO: refactor to evalScope evalScope( // Tone, @@ -131,6 +141,7 @@ evalScope( uiHelpersMocked, webaudio, tonalHelpers, + gamepadHelpers, /* toneHelpers, voicingHelpers, @@ -138,6 +149,8 @@ evalScope( uiHelpers, */ { + midin, + sysex, // gist, // euclid, csound: id, @@ -151,6 +164,7 @@ evalScope( loadCsound, loadcsound, setcps: id, + setcpm: id, Clock: {}, // whatever }, ); diff --git a/test/testtunes.mjs b/test/testtunes.mjs index fc887610e..3690d315b 100644 --- a/test/testtunes.mjs +++ b/test/testtunes.mjs @@ -359,28 +359,6 @@ stack( "[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25) ).note().piano().slow(2)`; -/* -export const customTrigger = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos -stack( - freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))), - freq("440(5,8)".clip(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8)) -).s("/2") - .onTrigger((t,hap,ct)=>{ - const ac = Tone.getContext().rawContext; - t = ac.currentTime + t - ct; - const { freq, s, gain = 1 } = hap.value; - const master = ac.createGain(); - master.gain.value = 0.1 * gain; - master.connect(ac.destination); - const o = ac.createOscillator(); - o.type = s || 'triangle'; - o.frequency.value = Number(freq); - o.connect(master); - o.start(t); - o.stop(t + hap.duration); -}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`; */ - export const swimmingWithSoundfonts = `// Koji Kondo - Swimming (Super Mario World) stack( n( diff --git a/undocumented.json b/undocumented.json index 8a38f205f..3f5a3be30 100644 --- a/undocumented.json +++ b/undocumented.json @@ -647,7 +647,6 @@ "evaluate" ], "/packages/webaudio/webaudio.mjs": [ - "webaudioOutputTrigger", "webaudioOutput", "webaudioRepl" ], diff --git a/vitest.config.ts b/vitest.config.mjs similarity index 71% rename from vitest.config.ts rename to vitest.config.mjs index 026f0b7c6..dcdde1133 100644 --- a/vitest.config.ts +++ b/vitest.config.mjs @@ -1,7 +1,9 @@ -import { configDefaults, defineConfig } from 'vitest/config'; +import { defineConfig } from 'vitest/config'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; /// export default defineConfig({ + plugins: [bundleAudioWorkletPlugin()], test: { reporters: 'verbose', isolate: false, diff --git a/website/agpl-header.txt b/website/agpl-header.txt index 6fd0c0fc9..8b7b6d631 100644 --- a/website/agpl-header.txt +++ b/website/agpl-header.txt @@ -1,10 +1,10 @@ /* Strudel - javascript-based environment for live coding algorithmic (musical) patterns -https://strudel.cc / https://github.com/tidalcycles/strudel/ +https://strudel.cc / https://codeberg.org/uzu/strudel/ Copyright (C) Strudel contributors -https://github.com/tidalcycles/strudel/graphs/contributors +https://codeberg.org/uzu/strudel/activity/contributors 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 diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 631584220..151c48fc1 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,6 +5,7 @@ import remarkToc from 'remark-toc'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeUrls from 'rehype-urls'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; @@ -134,6 +135,7 @@ export default defineConfig({ site, base, vite: { + plugins: [bundleAudioWorkletPlugin()], ssr: { // Example: Force a broken package to skip SSR processing, if needed // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 diff --git a/website/package.json b/website/package.json index 411581f8d..87f4025e4 100644 --- a/website/package.json +++ b/website/package.json @@ -8,8 +8,7 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview --port 3009 --host 0.0.0.0", - "astro": "astro", - "postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public" + "astro": "astro" }, "dependencies": { "@algolia/client-search": "^5.20.0", @@ -29,6 +28,7 @@ "@strudel/csound": "workspace:*", "@strudel/desktopbridge": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/gamepad": "workspace:*", "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", @@ -41,6 +41,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", + "@strudel/mondo": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", @@ -72,6 +73,7 @@ "@vite-pwa/astro": "^0.5.0", "html-escaper": "^3.0.3", "sharp": "^0.33.5", - "workbox-window": "^7.3.0" + "workbox-window": "^7.3.0", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/website/public/EmuSP12.json b/website/public/EmuSP12.json deleted file mode 100644 index b129ee57a..000000000 --- a/website/public/EmuSP12.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/", -"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"], -"cb": ["emusp12-cb/Cowbell.wav"], -"cp": ["emusp12-cp/Clap.wav"], -"cr": ["emusp12-cr/Crash.wav"], -"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"], -"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"], -"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"], -"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"], -"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"], -"oh": ["emusp12-oh/Hhopen1.wav"], -"perc": ["emusp12-perc/Blow1.wav"], -"rd": ["emusp12-rd/Ride.wav"], -"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"], -"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"] -} diff --git a/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf b/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf new file mode 100644 index 000000000..2ff9d6cfe Binary files /dev/null and b/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf differ diff --git a/website/public/fonts/CutiePi/LICENSE.txt b/website/public/fonts/CutiePi/LICENSE.txt new file mode 100644 index 000000000..dbc024ae8 --- /dev/null +++ b/website/public/fonts/CutiePi/LICENSE.txt @@ -0,0 +1,7 @@ + +100% free for personal and commercial use. +However it's limited on basic latin only, +contact riedjal@gmail.com for full glyph (based on ANSI encoding) +and OTF features (alternates). + +src: https://www.dafont.com/cute-aurora.font?text=%24%3A+s%28%22bd%285%2C8%29%22%29.superimpose%28x+%3D%3E+x.note%28%22c2%22%29.midi%28device%29%29 \ No newline at end of file diff --git a/website/public/fonts/tic80/license.txt b/website/public/fonts/tic80/license.txt new file mode 100644 index 000000000..e42ba4eb9 --- /dev/null +++ b/website/public/fonts/tic80/license.txt @@ -0,0 +1,5 @@ +The FontStruction “TIC-80 wide font” +(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed +under a Creative Commons CC0 Public Domain Dedication license +(http://creativecommons.org/publicdomain/zero/1.0/). +[ancestry] \ No newline at end of file diff --git a/website/public/fonts/tic80/readme.txt b/website/public/fonts/tic80/readme.txt new file mode 100644 index 000000000..b3aaff55c --- /dev/null +++ b/website/public/fonts/tic80/readme.txt @@ -0,0 +1,16 @@ +The font file in this archive was created using Fontstruct the free, online +font-building tool. +This font was created by “nesbox”. +This font has a homepage where this archive and other versions may be found: +https://fontstruct.com/fontstructions/show/1388526 +[ancestry] +Try Fontstruct at https://fontstruct.com +It’s easy and it’s fun. + +Fontstruct is copyright ©2017-2025 Rob Meek + +LEGAL NOTICE: +In using this font you must comply with the licensing terms described in the +file “license.txt” included with this archive. +If you redistribute the font file in this archive, it must be accompanied by all +the other files from this archive, including this one. diff --git a/website/public/fonts/tic80/tic-80-wide-font.otf b/website/public/fonts/tic80/tic-80-wide-font.otf new file mode 100644 index 000000000..ba1c1caf4 Binary files /dev/null and b/website/public/fonts/tic80/tic-80-wide-font.otf differ diff --git a/website/public/img/strudel-signal-flow.png b/website/public/img/strudel-signal-flow.png new file mode 100644 index 000000000..c5099c917 Binary files /dev/null and b/website/public/img/strudel-signal-flow.png differ diff --git a/website/public/uzu-drumkit.json b/website/public/uzu-drumkit.json new file mode 100644 index 000000000..b9c7250d5 --- /dev/null +++ b/website/public/uzu-drumkit.json @@ -0,0 +1,76 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", + "bd": [ + "bd/10_bd_switchangel.wav", + "bd/11_bd_mot4i.wav", + "bd/12_bd_mot4i.wav", + "bd/13_bd_mot4i.wav", + "bd/14_bd_switchangel.wav", + "bd/15_bd_switchangel.wav", + "bd/16_bd_switchangel.wav", + "bd/17_bd_switchangel.wav" + ], + "brk": [ + "brk/10_break_amen_pprocessed.wav" + ], + "cb": [ + "cb/10_perc_switchangel.wav" + ], + "cp": [ + "cp/10_cp_switchangel.wav", + "cp/11_cp_mot4i.wav" + ], + "cr": [ + "cr/10_cr_switchangel.wav", + "cr/11_cr_mot4i.wav" + ], + "hh": [ + "hh/10_hh_switchangel.wav", + "hh/11_hh_mot4i.wav", + "hh/12_hh_switchangel.wav", + "hh/13_hh_switchangel.wav", + "hh/14_hh_mot4i.wav" + ], + "ht": [ + "ht/10_ht_mot4i.wav" + ], + "lt": [ + "lt/10_lt_mot4i.wav" + ], + "misc": [ + "misc/10_misc_switchangel_ludens.wav", + "misc/11_misc_switchangel_ludens.wav", + "misc/12_misc_switchangel_ludens.wav", + "misc/13_misc_switchangel_ludens.wav", + "misc/14_misc_switchangel_ludens.wav" + ], + "mt": [ + "mt/10_mt_mot4i.wav" + ], + "oh": [ + "oh/10_oh_switchangel.wav", + "oh/11_oh_switchangel.wav", + "oh/12_oh_switchangel.wav", + "oh/13_oh_switchangel.wav" + ], + "rd": [ + "rd/10_rd_switchangel.wav" + ], + "rim": [ + "rim/10_rim_switchangel.wav", + "rim/11_rim_switch_angel.wav" + ], + "sd": [ + "sd/10_sd_switchangel-bounce-2.wav", + "sd/11_sd_switchangel_3.wav", + "sd/12_sd_switchangel_2.wav", + "sd/13_sd_switchangel_2.wav", + "sd/14_sd.wav" + ], + "sh": [ + "sh/10_sh_switchangel.wav" + ], + "tb": [ + "tb/10_tb.wav" + ] + } \ No newline at end of file diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json new file mode 100644 index 000000000..375b1fad3 --- /dev/null +++ b/website/public/uzu-wavetables.json @@ -0,0 +1,28 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/", + "wt_digital": [ + "wt_digital/wt_bad_day.wav", + "wt_digital/wt_basique.wav", + "wt_digital/wt_crickets.wav", + "wt_digital/wt_curses.wav", + "wt_digital/wt_echoes.wav" + ], + "wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"], + "wt_digital_basique": ["wt_digital/wt_basique.wav"], + "wt_digital_crickets": ["wt_digital/wt_crickets.wav"], + "wt_digital_curses": ["wt_digital/wt_curses.wav"], + "wt_digital_echoes": ["wt_digital/wt_echoes.wav"], + "wt_vgame": [ + "wt_vgame/wt_vgame10.wav", + "wt_vgame/wt_vgame11.wav", + "wt_vgame/wt_vgame12.wav", + "wt_vgame/wt_vgame13.wav", + "wt_vgame/wt_vgame14.wav", + "wt_vgame/wt_vgame15.wav", + "wt_vgame/wt_vgame16.wav", + "wt_vgame/wt_vgame17.wav", + "wt_vgame/wt_vgame18.wav", + "wt_vgame/wt_vgame19.wav", + "wt_vgame/wt_vgame20.wav" + ] +} \ No newline at end of file diff --git a/website/src/components/Footer/AvatarList.astro b/website/src/components/Footer/AvatarList.astro deleted file mode 100644 index 86bbcc875..000000000 --- a/website/src/components/Footer/AvatarList.astro +++ /dev/null @@ -1,169 +0,0 @@ ---- -// fetch all commits for just this page's path -type Props = { - path: string; -}; -const { path } = Astro.props as Props; -const resolvedPath = `website/src/pages${path}.mdx`; -const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`; -const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`; - -type Commit = { - author: { - id: string; - login: string; - }; -}; - -async function getCommits(url: string) { - try { - const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello'; - if (!token) { - throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'); - } - - const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`; - - const res = await fetch(url, { - method: 'GET', - headers: { - Authorization: auth, - 'User-Agent': 'astro-docs/1.0', - }, - }); - - const data = await res.json(); - - if (!res.ok) { - throw new Error( - `Request to fetch commits failed. Reason: ${res.statusText} - Message: ${data.message}`, - ); - } - - return data as Commit[]; - } catch (e) { - console.warn(`[error] /src/components/AvatarList.astro - ${(e as any)?.message ?? e}`); - return [] as Commit[]; - } -} - -function removeDups(arr: Commit[]) { - const map = new Map(); - for (let item of arr) { - const author = item.author; - // Deduplicate based on author.id - //map.set(author.id, { login: author.login, id: author.id }); - author && map.set(author.id, { login: author.login, id: author.id }); - } - - return [...map.values()]; -} - -const data = await getCommits(url); -const unique = removeDups(data); -const recentContributors = unique.slice(0, 3); // only show avatars for the 3 most recent contributors -const additionalContributors = unique.length - recentContributors.length; // list the rest of them as # of extra contributors ---- - - -
-
    - { - recentContributors.map((item) => ( -
  • - - {`Contributor - -
  • - )) - } -
- { - additionalContributors > 0 && ( - - {`and ${additionalContributors} additional contributor${ - additionalContributors > 1 ? 's' : '' - }.`} - - ) - } - {unique.length === 0 && Contributors} -
- - diff --git a/website/src/components/Header/Search.tsx b/website/src/components/Header/Search.tsx index c982d80b6..f200dc79f 100644 --- a/website/src/components/Header/Search.tsx +++ b/website/src/components/Header/Search.tsx @@ -82,7 +82,7 @@ export default function Search() { appId={ALGOLIA.appId} apiKey={ALGOLIA.apiKey} getMissingResultsUrl={({ query }) => { - return `https://github.com/tidalcycles/strudel/issues/new?title=Missing doc for ${query}`; + return `https://codeberg.org/uzu/strudel/issues/new?title=Missing%20doc%20for${encodeURIComponent(query)}`; }} transformItems={(items) => { return items.map((item) => { diff --git a/website/src/components/RightSidebar/RightSidebar.astro b/website/src/components/RightSidebar/RightSidebar.astro index cd501b1a2..28c713022 100644 --- a/website/src/components/RightSidebar/RightSidebar.astro +++ b/website/src/components/RightSidebar/RightSidebar.astro @@ -18,5 +18,4 @@ currentPage = currentPage.endsWith('/') ? currentPage.slice(0, -1) : currentPage diff --git a/website/src/components/Udels/UdelsEditor.jsx b/website/src/components/Udels/UdelsEditor.jsx index fe7a1ae48..f58ac5fc7 100644 --- a/website/src/components/Udels/UdelsEditor.jsx +++ b/website/src/components/Udels/UdelsEditor.jsx @@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage' // } export default function UdelsEditor(Props) { - const { context } = Props; + const { context, ...editorProps } = Props; const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context; return ( -
+
diff --git a/website/src/components/Udels/UdelsHeader.jsx b/website/src/components/Udels/UdelsHeader.jsx index d56f3a1dd..754716937 100644 --- a/website/src/components/Udels/UdelsHeader.jsx +++ b/website/src/components/Udels/UdelsHeader.jsx @@ -4,7 +4,7 @@ export default function UdelsHeader(Props) { const { numWindows, setNumWindows } = Props; return ( -