Compare commits

..

3 Commits

Author SHA1 Message Date
Felix Roos 7766b8003f supabase: add hello-world function 2025-06-14 00:35:13 +02:00
Felix Roos 466265f4d5 Merge remote-tracking branch 'origin/main' into rate-limiting 2025-06-13 13:56:14 +02:00
Felix Roos f51ec50691 init supabase 2025-06-13 13:55:04 +02:00
305 changed files with 11010 additions and 28894 deletions
-40
View File
@@ -1,40 +0,0 @@
name: Build and Deploy to beta (warm.strudel.cc)
on:
push:
branches:
- main
# 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/warm.strudel.cc
-37
View File
@@ -1,37 +0,0 @@
name: Build and Deploy to live (strudel.cc)
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/strudel.cc
+49
View File
@@ -0,0 +1,49 @@
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
@@ -11,15 +11,13 @@ jobs:
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
-15
View File
@@ -45,20 +45,6 @@ tidal-drum-machines
webaudiofontdata
src-tauri/target
### BEGIN Visual Studio Code ###
# Blanket, recursive exclude for .vscode directory and files
.vscode/**/*
# Unexclude specific files and directories within .vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### END Visual Studio Code ###
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
@@ -144,4 +130,3 @@ fabric.properties
samples/*
!samples/README.md
.idea/
-1
View File
@@ -1 +0,0 @@
22
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["denoland.vscode-deno"]
}
+31 -9
View File
@@ -1,10 +1,32 @@
{
"cSpell.words": [
"subspan",
"vals"
],
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
},
"testing.automaticallyOpenPeekView": "never"
}
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"cSpell.words": [
"subspan",
"vals"
],
"deno.enablePaths": [
"supabase/functions"
],
"deno.lint": true,
"deno.unstable": [
"bare-node-builtins",
"byonm",
"sloppy-imports",
"unsafe-proto",
"webgpu",
"broadcast-channel",
"worker-options",
"cron",
"kv",
"ffi",
"fs",
"http",
"net"
],
"testing.automaticallyOpenPeekView": "never",
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
}
}
-1081
View File
File diff suppressed because it is too large Load Diff
+17 -98
View File
@@ -4,13 +4,20 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi
## Move to codeberg
Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github.
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 community, either
To get in touch with the contributors, either
- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel)
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
## Ask a Question
@@ -29,35 +36,22 @@ Use one of the Communication Channels listed above and drop us a line or two!
## Share Music
If you made some music with strudel, you can give back some love and share what you've done!
Your creation could also be part of the random selection in the REPL if you want.
Use one of the Communication Channels listed above.
(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse)
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place.
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar)
## Propose a Feature
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature!
## Contribute a feature
Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that.
At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion.
### AI/LLM policy
Strudel is a project handmade by humans, with thought and nuance.
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels.
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above.
Maybe you even want to help with the implementation of that feature!
## Report a Bug
If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues).
Please check that it has not been reported before.
## Fix a Bug
@@ -124,7 +118,8 @@ There are also eslint extensions / plugins for most editors.
## Running all CI Checks
When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`.
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
You can run the same check with `pnpm check`
## Package Workflow
@@ -155,7 +150,6 @@ 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
@@ -166,81 +160,6 @@ 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!
-21
View File
@@ -1,21 +0,0 @@
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"]
+11 -8
View File
@@ -1,22 +1,21 @@
# strudel
Live coding patterns on the web
https://strudel.cc/
Development is moving to https://codeberg.org/uzu/strudel
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Source: https://codeberg.org/uzu/strudel/
* Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**.
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/) 18 or newer
1. Install [Node.js](https://nodejs.org/)
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
@@ -33,13 +32,19 @@ 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). 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.md). 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). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors).
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
<a href="https://codeberg.org/uzu/strudel/activity/contributors">
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## Community
@@ -48,5 +53,3 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
-954
View File
@@ -1,954 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="generator" content="pandoc" />
<meta name="date" content="2022-12-14" />
<title>Strudel: live coding patterns on the Web</title>
<style type="text/css">
code {
white-space: pre;
}
</style>
<style type="text/css">
pre > code.sourceCode {
white-space: pre;
position: relative;
}
pre > code.sourceCode > span {
display: inline-block;
line-height: 1.25;
}
pre > code.sourceCode > span:empty {
height: 1.2em;
}
.sourceCode {
overflow: visible;
}
code.sourceCode > span {
color: inherit;
text-decoration: inherit;
}
div.sourceCode {
margin: 1em 0;
}
pre.sourceCode {
margin: 0;
}
@media screen {
div.sourceCode {
overflow: auto;
}
}
@media print {
pre > code.sourceCode {
white-space: pre-wrap;
}
pre > code.sourceCode > span {
text-indent: -5em;
padding-left: 5em;
}
}
pre.numberSource code {
counter-reset: source-line 0;
}
pre.numberSource code > span {
position: relative;
left: -4em;
counter-increment: source-line;
}
pre.numberSource code > span > a:first-child::before {
content: counter(source-line);
position: relative;
left: -1em;
text-align: right;
vertical-align: baseline;
border: none;
display: inline-block;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
padding: 0 4px;
width: 4em;
color: #aaaaaa;
}
pre.numberSource {
margin-left: 3em;
border-left: 1px solid #aaaaaa;
padding-left: 4px;
}
div.sourceCode {
}
@media screen {
pre > code.sourceCode > span > a:first-child::before {
text-decoration: underline;
}
}
code span.al {
color: #ff0000;
font-weight: bold;
} /* Alert */
code span.an {
color: #60a0b0;
font-weight: bold;
font-style: italic;
} /* Annotation */
code span.at {
color: #7d9029;
} /* Attribute */
code span.bn {
color: #40a070;
} /* BaseN */
code span.bu {
color: #008000;
} /* BuiltIn */
code span.cf {
color: #007020;
font-weight: bold;
} /* ControlFlow */
code span.ch {
color: #4070a0;
} /* Char */
code span.cn {
color: #880000;
} /* Constant */
code span.co {
color: #60a0b0;
font-style: italic;
} /* Comment */
code span.cv {
color: #60a0b0;
font-weight: bold;
font-style: italic;
} /* CommentVar */
code span.do {
color: #ba2121;
font-style: italic;
} /* Documentation */
code span.dt {
color: #902000;
} /* DataType */
code span.dv {
color: #40a070;
} /* DecVal */
code span.er {
color: #ff0000;
font-weight: bold;
} /* Error */
code span.ex {
} /* Extension */
code span.fl {
color: #40a070;
} /* Float */
code span.fu {
color: #06287e;
} /* Function */
code span.im {
color: #008000;
font-weight: bold;
} /* Import */
code span.in {
color: #60a0b0;
font-weight: bold;
font-style: italic;
} /* Information */
code span.kw {
color: #007020;
font-weight: bold;
} /* Keyword */
code span.op {
color: #666666;
} /* Operator */
code span.ot {
color: #007020;
} /* Other */
code span.pp {
color: #bc7a00;
} /* Preprocessor */
code span.sc {
color: #4070a0;
} /* SpecialChar */
code span.ss {
color: #bb6688;
} /* SpecialString */
code span.st {
color: #4070a0;
} /* String */
code span.va {
color: #19177c;
} /* Variable */
code span.vs {
color: #4070a0;
} /* VerbatimString */
code span.wa {
color: #60a0b0;
font-weight: bold;
font-style: italic;
} /* Warning */
</style>
<link rel="stylesheet" href="css/iclc.css" />
</head>
<body>
<div id="header">
<h1 class="title">Strudel: live coding patterns on the Web</h1>
<ul id="authorlist">
<li>true</li>
<li>true</li>
</ul>
<h3 class="date">2022-12-14</h3>
</div>
<h2 class="abstract">Abstract</h2>
<div id="abstract">
<p>
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 Strudels 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.
</p>
</div>
<h1 data-number="1" id="introduction"><span class="header-section-number">1</span> Introduction</h1>
<p>
In the following paper, we introduce <em>Strudel</em>, 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 Tidals
approach to algorithmic patterns
<span class="citation" data-cites="mcleanAlgorithmicPattern2020a">(Mclean 2020)</span> 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
<span class="citation" data-cites="mcleanTidalVortexZero2022">(McLean et al. 2022)</span>, Gibber
<span class="citation" data-cites="robertsGibberLiveCoding2012">(Roberts and Kuchera-morin 2012)</span>, Estuary
<span class="citation" data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al. 2017)</span>, Hydra
<span class="citation" data-cites="jackHydra2022">(Jack [2022] 2022)</span>, Ocarina
<span class="citation" data-cites="solomonPurescriptocarina2022">(Solomon [2021] 2022)</span> and Feedforward
<span class="citation" data-cites="mcleanFeedforward2020">(McLean 2020)</span>. This paper expands the Strudel
Demo paper for the Web Audio Conference 2022
<span class="citation" data-cites="StrudelWAC2022">(Roos and McLean 2022)</span>.
</p>
<p>
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 Felixs own work into music representation and visualisation, with Alexs
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 Strudels 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.
</p>
<p>
Over the first year of its life, Strudel is now a fully-fledged live coding environment, porting Tidals core
represention of patterns, pattern transformations, and mininotation for polymetric sequences, combined with a
wealth of features for synthesising and visualising those patterns.
</p>
<h1 data-number="2" id="from-tidal-to-strudel-and-back">
<span class="header-section-number">2</span> From Tidal to Strudel and back
</h1>
<p>
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 Haskells 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 Haskells 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;
<span class="citation" data-cites="mcleanTidalVortexZero2022">McLean et al. (2022)</span>], and we have now
successfully implemented Tidals pure functional representation of patterns in Strudel, including partial
application, currying, and the functor, applicative and monadic structures that underlie Tidals 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.
</p>
<p>
This development process has been far from a one-way port, however. The process of porting Tidals 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.
</p>
<h1 data-number="3" id="representing-patterns">
<span class="header-section-number">3</span> Representing Patterns
</h1>
<p>
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
<strong>querying</strong>. Example:
</p>
<div class="sourceCode" id="cb1">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> pattern <span class="op">=</span> <span class="fu">sequence</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> events <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(events<span class="op">.</span><span class="fu">map</span>(e <span class="kw">=&gt;</span> e<span class="op">.</span><span class="fu">show</span>()))</span></code></pre>
</div>
<p>
In this example, we create a pattern using the <code>sequence</code> function and <strong>query</strong> it for
the time span from <code>0</code> to <code>1</code>. Those numbers represent units of time called
<strong>cycles</strong>. The length of one cycle depends on the tempo, which defaults to one cycle per second. The
resulting events are:
</p>
<div class="sourceCode" id="cb2">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>[{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;c3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span> }<span class="op">,</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;e3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span> }<span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;g3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span> }]</span></code></pre>
</div>
<p>
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 <code>sequence</code> 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.
</p>
<p>
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.
</p>
<figure>
<img
src="images/strudel-screenshot2.png"
style="width: 60%"
alt="Screenshot of the Strudel REPL live coding editor, including piano-roll visualisation."
/>
<figcaption aria-hidden="true">
Screenshot of the Strudel REPL live coding editor, including piano-roll visualisation.
</figcaption>
</figure>
<h1 data-number="4" id="making-patterns"><span class="header-section-number">4</span> Making Patterns</h1>
<p>
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build
patterns using Strudels extensive combinator library to create, combine and transform patterns.
</p>
<p>
The live coder will rarely use the <code>sequence</code> function as seen above, as sequencing is implicit in many
functions. For example in the following, the <code>note</code> function constructs a pattern of notes, sequencing
its arguments in the same manner as the previous example.
</p>
<div class="sourceCode" id="cb3">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span></code></pre>
</div>
<p>
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 <code>note</code> function, however.].
</p>
<div class="sourceCode" id="cb4">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">&quot;c3 [e3 g3]&quot;</span>)</span></code></pre>
</div>
<p>
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.
</p>
<h1 data-number="5" id="pattern-example"><span class="header-section-number">5</span> Pattern Example</h1>
<p>
The following example showcases how patterns can be utilized to create musical complexity from simple parts, using
repetition and interference:
</p>
<div class="sourceCode" id="cb5">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;&lt;0 2 [4 6](3,4,1) 3&gt;&quot;</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">2</span>))</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">6</span>))</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">scale</span>(<span class="st">&#39;D minor&#39;</span>)</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">legato</span>(<span class="op">.</span><span class="dv">25</span>)</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">note</span>()<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;sawtooth square&quot;</span>)</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">delay</span>(<span class="op">.</span><span class="dv">8</span>)<span class="op">.</span><span class="fu">delaytime</span>(<span class="op">.</span><span class="dv">125</span>)</span></code></pre>
</div>
<p>
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:
</p>
<div class="sourceCode" id="cb6">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">cat</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">2</span><span class="op">,</span> [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">.</span><span class="fu">euclid</span>(<span class="dv">3</span><span class="op">,</span> <span class="dv">4</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">,</span> <span class="dv">3</span>)</span></code></pre>
</div>
<p>
These numbers then undergo various pattern transformations. Here is a short description of all the functions used:
</p>
<ul>
<li><code>cat</code>: play elements sequentially, where each lasts one cycle</li>
<li><code>brackets</code>: elements inside brackets are divided equally over the time of their parent</li>
<li>
<code>.euclid(p, s, o)</code>: place p pulses evenly over s steps, with offset o
<span class="citation" data-cites="toussaintEuclideanAlgorithmGenerates2005">(Toussaint 2005)</span>
</li>
<li>
<code>.off(n, f)</code>: layers a pattern on top of itself, with the new layer offset by n cycles, and with
function f applied
</li>
<li><code>.legato(n)</code>: multiply the duration of all events in a pattern by a factor of n</li>
<li>
<code>.echo(t, n, v)</code>: copy each event t times, with n cycles in between each copy, decreasing velocity by
v
</li>
<li><code>.note()</code>: interpretes values as notes</li>
<li><code>.s(name)</code>: play back each event with the given sound</li>
<li><code>.delay(wet)</code>: add delay</li>
<li><code>.delaytime(t)</code>: set delay time</li>
</ul>
<p>Much of the above will be familiar to Tidal users.</p>
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
<h1 data-number="6" id="ways-to-make-sound-and-other-events">
<span class="header-section-number">6</span> Ways to make Sound (and other events)
</h1>
<p>To generate sound, Strudel supports bindings for different outputs:</p>
<ul>
<li>Tone.js (deprecated)</li>
<li>Web Audio API</li>
<li>WebDirt, a js recreation of Tidals <em>Dirt</em> sample engine (deprecated)</li>
<li>OSC via osc-js, compatible with superdirt</li>
<li>Csound via the Csound WebAssembly build</li>
<li>MIDI via WebMIDI</li>
<li>Serial via WebSerial</li>
</ul>
<p>
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
<em>fire-and-forget</em> approach, creating a lot of Tone.js instruments and effects causes performance issues
quickly. For that reason, we chose to search for alternatives.
</p>
<p>
Strudels 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.
</p>
<p>
WebDirt <span class="citation" data-cites="ogbornDktr0WebDirt2022">(Ogborn [2016] 2022)</span> was created as part
of the Estuary Live Coding System
<span class="citation" data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al. 2017)</span>, 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.
</p>
<p>
Using the OSC protocol via Strudels 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
<span class="citation" data-cites="SuperDirt2022">(<em>SuperDirt</em> [2015] 2022)</span>, which is the
well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
</p>
<p>
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
<span class="citation" data-cites="CsoundWebAssembly">(Yi, Lazzarini, and Costello 2018)</span>, Csound
orchestra synthesisers can be embedded in and then patterned with Strudel code.
</p>
<p>
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).
</p>
<p>
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.
</p>
<h1 data-number="7" id="the-strudel-repl"><span class="header-section-number">7</span> The Strudel REPL</h1>
<p>
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.
</p>
<p>
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:
</p>
<ol type="1">
<li>
The user writes and updates code. Each update transpiles and evaluates it to create a
<code>Pattern</code> instance
</li>
<li>
While the REPL is running, the <code>Scheduler</code> queries the active <code>Pattern</code> by a regular
interval, generating <code>Events</code> (also known as <code>Haps</code> in Strudel) for the next time span.
</li>
<li>
For each scheduling tick, all generated <code>Events</code> are triggered by calling their
<code>onTrigger</code> method, which is set by the output.
</li>
</ol>
<figure>
<img
src="https://github.com/tidalcycles/strudel/raw/talk/talk/public/strudelflow.png?raw=true"
style="width: 43%"
alt="REPL control flow"
/>
<figcaption aria-hidden="true">REPL control flow</figcaption>
</figure>
<h2 data-number="7.1" id="user-code"><span class="header-section-number">7.1</span> User Code</h2>
<p>To create a <code>Pattern</code> from the user code, two steps are needed:</p>
<ol type="1">
<li>Transpile the JS input code to make it functional</li>
<li>Evaluate the transpiled code</li>
</ol>
<h3 data-number="7.1.1" id="transpilation-evaluation">
<span class="header-section-number">7.1.1</span> Transpilation &amp; Evaluation
</h3>
<p>
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 <code>babel</code> will transpile code that contains unsupported
language features into a version of the code without those features.
</p>
<p>
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:
</p>
<div class="sourceCode" id="cb7">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;c3 [e3 g3]*2&quot;</span></span></code></pre>
</div>
<p>is transpiled to:</p>
<div class="sourceCode" id="cb8">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mini</span>(<span class="st">&quot;c3 [e3 g3]*2&quot;</span>)<span class="op">.</span><span class="fu">withMiniLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">14</span><span class="op">,</span><span class="dv">14</span>])</span></code></pre>
</div>
<p>
Here, the string is wrapped in <code>mini</code>, which will create a pattern from a mini notation string.
Additionally, the <code>withMiniLocation</code> method passes the original source code location of the string to
the pattern, which enables highlighting active events.
</p>
<p>
Other convenient features like pseudo variables, operator overloading and top level await are possible with
transpilation.
</p>
<p>After the transpilation, the code is ready to be evaluated into a <code>Pattern</code>.</p>
<p>
Behind the scenes, the user code string is parsed with <code>acorn</code>, turning it into an Abstract Syntax Tree
(AST). The AST allows changing the structure of the code before generating the transpiled version using
<code>escodegen</code>.
</p>
<h3 data-number="7.1.2" id="mini-notation"><span class="header-section-number">7.1.2</span> Mini Notation</h3>
<p>
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.
</p>
<p>
The Mini Notation parser is implemented using <code>peggy</code>, 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,
<code>"c3 [e3 g3]*2"</code> will result in the following calls:
</p>
<div class="sourceCode" id="cb9">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">seq</span>(</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;c3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">1</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">4</span><span class="op">,</span><span class="dv">4</span>])<span class="op">,</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">seq</span>(</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;e3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>])<span class="op">,</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;g3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">10</span><span class="op">,</span><span class="dv">10</span>])<span class="op">,</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="fu">fast</span>(<span class="dv">2</span>)</span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>)</span></code></pre>
</div>
<h3 data-number="7.1.3" id="highlighting-locations">
<span class="header-section-number">7.1.3</span> Highlighting Locations
</h3>
<p>
As seen in the examples above, both the JS and the Mini Notation parser add source code locations using
<code>withMiniLocation</code> and <code>withLocation</code> 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.
</p>
<h2 data-number="7.2" id="scheduling-events"><span class="header-section-number">7.2</span> Scheduling Events</h2>
<p>
After an instance of <code>Pattern</code> 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 intervals time span. A simplified implementation looks like this:
</p>
<div class="sourceCode" id="cb10">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> pattern <span class="op">=</span> <span class="fu">seq</span>(<span class="st">&#39;c3&#39;</span><span class="op">,</span> [<span class="st">&#39;e3&#39;</span><span class="op">,</span> <span class="st">&#39;g3&#39;</span>])<span class="op">;</span> <span class="co">// pattern from user</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> interval <span class="op">=</span> <span class="fl">0.5</span><span class="op">;</span> <span class="co">// query interval in seconds</span></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> time <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> <span class="co">// beginning of current time span</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> minLatency <span class="op">=</span> <span class="op">.</span><span class="dv">1</span><span class="op">;</span> <span class="co">// min time before a hap should trigger</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="pp">setInterval</span>(() <span class="kw">=&gt;</span> {</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> haps <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(time<span class="op">,</span> time <span class="op">+</span> interval)<span class="op">;</span></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> time <span class="op">+=</span> interval<span class="op">;</span> <span class="co">// increment time</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> haps<span class="op">.</span><span class="fu">forEach</span>((hap) <span class="kw">=&gt;</span> {</span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> deadline <span class="op">=</span> hap<span class="op">.</span><span class="at">whole</span><span class="op">.</span><span class="at">begin</span> <span class="op">-</span> time <span class="op">+</span> minLatency<span class="op">;</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration)<span class="op">;</span></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> interval <span class="op">*</span> <span class="dv">1000</span>)<span class="op">;</span> <span class="co">// query each &quot;interval&quot; seconds</span></span></code></pre>
</div>
<p>
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around
imprecise callbacks of <code>setInterval</code>. More about the implementation details can be read in
<a href="https://loophole-letters.vercel.app/web-audio-scheduling">this blog post</a>.
</p>
<p>
The fact that <code>Pattern.queryArc</code> 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.
</p>
<p>
The latency between the time the pattern is evaluated and the change is heard is between
<code>minLatency</code> and <code>interval + minLatency</code>, 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.
</p>
<h2 data-number="7.3" id="output"><span class="header-section-number">7.3</span> Output</h2>
<p>
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.
</p>
<h3 data-number="7.3.1" id="control-parameters">
<span class="header-section-number">7.3.1</span> Control Parameters
</h3>
<p>
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:
</p>
<div class="sourceCode" id="cb11">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">&quot;c3 e3&quot;</span>)<span class="op">.</span><span class="fu">cutoff</span>(<span class="dv">1000</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&#39;sawtooth&#39;</span>)</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>(hap <span class="kw">=&gt;</span> hap<span class="op">.</span><span class="at">value</span>)</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* [</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: &#39;c3&#39;, cutoff: 1000, s: &#39;sawtooth&#39; }</span></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: &#39;e3&#39;, cutoff: 1000, s: &#39;sawtooth&#39; }</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="co">] */</span></span></code></pre>
</div>
<p>
Here, the control parameter functions <code>note</code>, <code>cutoff</code> and <code>s</code> 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 <code>Pattern</code>, or a <code>Pattern</code>. In the example,
<code>note</code> gets a <code>Pattern</code> from a Mini Notation expression (double quoted), while
<code>cutoff</code> and <code>s</code> are given a <code>Number</code> and a (single quoted)
<code>String</code> respectively.
</p>
<p>
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:
</p>
<div class="sourceCode" id="cb12">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> { x<span class="op">,</span> y } <span class="op">=</span> <span class="fu">createParams</span>(<span class="st">&#39;x&#39;</span><span class="op">,</span> <span class="st">&#39;y&#39;</span>)</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="fu">x</span>(sine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">200</span>))<span class="op">.</span><span class="fu">y</span>(cosine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">200</span>))</span></code></pre>
</div>
<p>
This example creates the custom control parameters <code>x</code> and <code>y</code> which are then used to form a
pattern that descibes the coordinates of a circle.
</p>
<h3 data-number="7.3.2" id="outputs"><span class="header-section-number">7.3.2</span> Outputs</h3>
<p>
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 <code>onTrigger</code> function which is
used to implement the output. A very simple version of the web audio output could look like this:
</p>
<div class="sourceCode" id="cb13">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration) {</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> { note } <span class="op">=</span> hap<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> time <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">currentTime</span> <span class="op">+</span> deadline<span class="op">;</span></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> o <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="fu">createOscillator</span>()<span class="op">;</span></span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="at">frequency</span><span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="fu">getFreq</span>(note)<span class="op">;</span></span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">start</span>(time)<span class="op">;</span></span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">stop</span>(time <span class="op">+</span> <span class="bu">event</span><span class="op">.</span><span class="at">duration</span>)<span class="op">;</span></span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">connect</span>(<span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">destination</span>)<span class="op">;</span></span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>}</span></code></pre>
</div>
<p>
The above example will create an <code>OscillatorNode</code> for each event, where the frequency is controlled by
the <code>note</code> param. In essence, this is how the WebAudio API output of Strudel works, only with many more
parameters to control synths, samples and effects.
</p>
<h1 data-number="8" id="pattern-alignment-and-combination">
<span class="header-section-number">8</span> Pattern alignment and combination
</h1>
<p>
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
<em>how</em> this is done, only <em>what</em> is to be done.
</p>
<p>
As a simple example, consider two number patterns <code>"0 [1 2] 3"</code>, and <code>"10 20"</code>. 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:
</p>
<div class="sourceCode" id="cb14">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 [1 2] 3&quot;</span><span class="op">.</span><span class="fu">add</span>(<span class="st">&quot;10 20&quot;</span>)</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;10 [11 22] 23&quot;</span></span></code></pre>
</div>
<p>
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:
</p>
<div class="sourceCode" id="cb15">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 1 2&quot;</span><span class="op">.</span><span class="fu">add</span>(<span class="st">&quot;10 20&quot;</span>)</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;10 [11 21] 20&quot;</span></span></code></pre>
</div>
<p>
They are similar to the previous example in that the number <code>1</code> is split in two, with its two halves
added to <code>10</code> and <code>20</code> respectively. However, the <code>11</code> remembers that it is a
fragment of that original <code>1</code> 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 <code>21</code> is also a fragment of that
original <code>1</code> event, but a fragment of its second half. Because the start of its event is missing, it
wouldnt actually trigger a sound (unless it underwent further pattern transformations/combinations).
</p>
<p>
In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is
added <em>in</em> to the first one, and indeed this can be made explicit:
</p>
<div class="sourceCode" id="cb16">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 1 2&quot;</span><span class="op">.</span><span class="at">add</span><span class="op">.</span><span class="fu">in</span>(<span class="st">&quot;10 20&quot;</span>)</span></code></pre>
</div>
<p>This makes way for other ways to align the pattern, and several are already defined, in particular:</p>
<ul>
<li>
<code>in</code> - as explained above, aligns cycles, and applies values from the pattern on the right
<em>in</em> to the pattern on the left.
</li>
<li>
<code>out</code> - as with <code>in</code>, but values are applied <em>out</em> of the pattern on the left
(i.e. <em>in</em> to the one on the right).
</li>
<li>
<code>mix</code> - structures from both patterns are combined, so that the new events are not fragments but are
created at intersections of events from both sides.
</li>
<li>
<code>squeeze</code> - cycles from the pattern on the right are squeezed into events on the left. So that
e.g. <code>"0 1 2".add.squeeze("10 20")</code> is equivalent to <code>"[10 20] [11 21] [12 22]"</code>.
</li>
<li>
<code>squeezeout</code> - as with <code>squeeze</code>, but cycles from the left are squeezed into events on the
right. So, <code>"0 1 2".add.squeezeout("10 20")</code> is equivalent to <code>[10 11 12] [20 21 22]</code>.
</li>
<li>
<code>trig</code> is similar to <code>squeezeout</code> 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
<code>"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")</code> would be equivalent to
<code>10 11 12 13 20 21 30 31</code>. In effect, events on the right trigger cycles on the left.
</li>
<li>
<code>trigzero</code> is similar to <code>trig</code>, but the pattern is triggered from its very first cycle,
rather than from the current cycle. <code>trig</code> and <code>trigzero</code> therefore only give different
results where the leftmost pattern differs from one cycle to the next.
</li>
</ul>
<p>
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.
</p>
<h1 data-number="9" id="comparing-strudel-and-haskell-in-use">
<span class="header-section-number">9</span> Comparing Strudel and Haskell in use
</h1>
<p>
Unlike Haskell, JavaScript lacks the ability to define custom infix operators, or change the meaning of existing
ones. So the above Strudel example of <code>"0 1 2".add.out("10 20")</code> is equivalent to the Tidal expression
<code>"0 1 2" +| "10 20"</code>, where the vertical bar in the operator <code>+|</code> stands for
<code>out</code> (where <code>a |+ b</code> would be equivalent of <code>a.add.in(b)</code>).
</p>
<p>
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 <code>.</code> operator. This
then is the design trade-off of Tidals tersity, versus Strudels simplicity.
</p>
<p>To demonstrate this, consider the following Tidal pattern:</p>
<pre
class="tidal"
><code>iter 4 $ every 3 (||+ n &quot;10 20&quot;) $ (n &quot;0 1 3&quot;) # s &quot;triangle&quot; # crush 4</code></pre>
<p>This can be directly translated to the Strudel equivalent:</p>
<div class="sourceCode" id="cb18">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">iter</span>(<span class="dv">4</span><span class="op">,</span> <span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">&quot;10 20&quot;</span>)<span class="op">,</span> <span class="fu">n</span>(<span class="st">&quot;0 1 3&quot;</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;triangle&quot;</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)))</span></code></pre>
</div>
<p>Although for a more canonical Strudel expression, we would reorder it as:</p>
<div class="sourceCode" id="cb19">
<pre
class="sourceCode js"
><code class="sourceCode javascript"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">n</span>(<span class="st">&quot;0 1 3&quot;</span>)<span class="op">.</span><span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">&quot;10 20&quot;</span>))<span class="op">.</span><span class="fu">iter</span>(<span class="dv">4</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;triangle&quot;</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)</span></code></pre>
</div>
<p>
The Strudel example uses the <code>.</code> method call operator for all operations and combinations, whereas the
Tidal example has <code>#</code> 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
Haskells functions, and partly due to the use of the <code>$</code> operator as an alternative way to establish
precedence and control the order of evaluation.
</p>
<p>
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 Haskells dollar <code>$</code> 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.
</p>
<p>
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.
Tidals 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.
</p>
<h2 data-number="9.1" id="the-trade-off-of-flexible-typing">
<span class="header-section-number">9.1</span> The trade-off of flexible typing
</h2>
<p>
We have identified one problem with porting Tidal to JavaScript where we have missed Haskells 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.
</p>
<h1 data-number="10" id="future-outlook"><span class="header-section-number">10</span> Future Outlook</h1>
<p>
The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be
</p>
<ol type="1">
<li>accessible</li>
<li>consistent with Tidals approach to pattern</li>
<li>modular and extensible</li>
</ol>
<p>
While Haskells type system makes it a great language for the ongoing development of Tidals inner representation
of pattern, JavaScripts 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 <span class="citation" data-cites="lanChaosprintGlicol2022">(Lan [2020] 2022)</span> and
Faust
<span class="citation" data-cites="FaustProgrammingLanguage2022"
>(<em>Faust - Programming Language for Audio Applications and Plugins</em> [2016] 2022)</span
>. 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 Tidals 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.
</p>
<h1 data-number="11" id="links"><span class="header-section-number">11</span> Links</h1>
<p>
The Strudel REPL is available at <a href="https://strudel.cc" class="uri">https://strudel.cc</a>, including an
interactive tutorial. The repository is at
<a href="https://github.com/tidalcycles/strudel" class="uri">https://github.com/tidalcycles/strudel</a>, all the
code is open source under the AGPL-3.0 License.
</p>
<h1 data-number="12" id="acknowledgments"><span class="header-section-number">12</span> Acknowledgments</h1>
<p>
Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for
inspiration and support. Alex McLeans work on this project is supported by a UKRI Future Leaders Fellowship
[grant number MR/V025260/1].
</p>
<h1 class="unnumbered" id="references">References</h1>
<div id="refs" class="references csl-bib-body hanging-indent" role="doc-bibliography">
<div id="ref-FaustProgrammingLanguage2022" class="csl-entry" role="doc-biblioentry">
<em>Faust - Programming Language for Audio Applications and Plugins</em>. (2016) 2022. C++. GRAME.
<a href="https://github.com/grame-cncm/faust">https://github.com/grame-cncm/faust</a>.
</div>
<div id="ref-jackHydra2022" class="csl-entry" role="doc-biblioentry">
Jack, Olivia. (2022) 2022. <em>Hydra</em>.
<a href="https://github.com/ojack/hydra">https://github.com/ojack/hydra</a>.
</div>
<div id="ref-lanChaosprintGlicol2022" class="csl-entry" role="doc-biblioentry">
Lan, Qichao. (2020) 2022. <em>Chaosprint/Glicol</em>. Rust.
<a href="https://github.com/chaosprint/glicol">https://github.com/chaosprint/glicol</a>.
</div>
<div id="ref-mcleanAlgorithmicPattern2020a" class="csl-entry" role="doc-biblioentry">
Mclean, Alex. 2020. <span>“Algorithmic Pattern.”</span> In
<em>Proceedings of the International Conference on New Interfaces for Musical Expression</em>, 265--270.
Birmingham, UK. <a href="https://zenodo.org/record/4813352">https://zenodo.org/record/4813352</a>.
</div>
<div id="ref-mcleanFeedforward2020" class="csl-entry" role="doc-biblioentry">
McLean, Alex. 2020. <span>“Feedforward.”</span> In
<em>Proceedings of New Interfaces for Musical Expression</em>. Birmingham.
<a href="https://zenodo.org/record/6353969">https://zenodo.org/record/6353969</a>.
</div>
<div id="ref-mcleanTidalVortexZero2022" class="csl-entry" role="doc-biblioentry">
McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. 2022. <span>“TidalVortex Zero.”</span> In
<em>Proceedings of the 7th International Conference on Live Coding</em>. Limerick, Ireland: Zenodo.
<a href="https://doi.org/10.5281/zenodo.6456380">https://doi.org/10.5281/zenodo.6456380</a>.
</div>
<div id="ref-ogbornDktr0WebDirt2022" class="csl-entry" role="doc-biblioentry">
Ogborn, David. (2016) 2022. <em>Dktr0/WebDirt</em>. JavaScript.
<a href="https://github.com/dktr0/WebDirt">https://github.com/dktr0/WebDirt</a>.
</div>
<div id="ref-ogbornEstuaryBrowserbasedCollaborative2017" class="csl-entry" role="doc-biblioentry">
Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, and Alex McLean. 2017.
<span>“Estuary: Browser-Based Collaborative Projectional Live Coding of Musical Patterns.”</span> In
<em>Proceedings of the International Conference on Live Coding</em>, 11. Morelia.
</div>
<div id="ref-robertsGibberLiveCoding2012" class="csl-entry" role="doc-biblioentry">
Roberts, Charles, and Joann Kuchera-morin. 2012. <span>“Gibber: Live Coding Audio in the Browser.”</span> In
<em>In Proceedings of the 2012 International Computer Music Conference</em>.
</div>
<div id="ref-StrudelWAC2022" class="csl-entry" role="doc-biblioentry">
Roos, Felix, and Alex McLean. 2022. <span>“Strudel: Algorithmic Patterns for the Web.”</span> In. Zenodo.
<a href="https://doi.org/10.5281/zenodo.6768844">https://doi.org/10.5281/zenodo.6768844</a>.
</div>
<div id="ref-solomonPurescriptocarina2022" class="csl-entry" role="doc-biblioentry">
Solomon, Mike. (2021) 2022. <em>Purescript-Ocarina</em>. PureScript.
<a href="https://github.com/mikesol/purescript-ocarina">https://github.com/mikesol/purescript-ocarina</a>.
</div>
<div id="ref-SuperDirt2022" class="csl-entry" role="doc-biblioentry">
<em>SuperDirt</em>. (2015) 2022. SuperCollider. musikinformatik.
<a href="https://github.com/musikinformatik/SuperDirt">https://github.com/musikinformatik/SuperDirt</a>.
</div>
<div id="ref-toussaintEuclideanAlgorithmGenerates2005" class="csl-entry" role="doc-biblioentry">
Toussaint, Godfried. 2005. <span>“The Euclidean Algorithm Generates Traditional Musical Rhythms.”</span> In
<em>In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science</em>, 4756.
<a href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231"
>http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231</a
>.
</div>
<div id="ref-CsoundWebAssembly" class="csl-entry" role="doc-biblioentry">
Yi, Steven, Victor Lazzarini, and Edward Costello. 2018.
<span>“WebAssembly AudioWorklet Csound.”</span> In. Berlin, Germany.
<a href="https://mural.maynoothuniversity.ie/16018/">https://mural.maynoothuniversity.ie/16018/</a>.
</div>
</div>
</body>
</html>
-63
View File
@@ -1,63 +0,0 @@
$if(false)$ This is a pandoc template and should not be edited. $endif$
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" $if(lang)$ lang="$lang$" xml:lang="$lang$" $endif$>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="generator" content="pandoc" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$ $if(date-meta)$
<meta name="date" content="$date-meta$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">
code {
white-space: pre;
}
</style>
$if(quotes)$
<style type="text/css">
q {
quotes: '“' '”' '' '';
}
</style>
$endif$ $if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
<link rel="stylesheet" href="css/iclc.css" $if(html5)$$else$type="text/css" $endif$ />
$for(css)$
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$ />
$endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$
</head>
<body>
$for(include-before)$ $include-before$ $endfor$ $if(title)$
<div id="$idprefix$header">
<h1 class="title">$title$</h1>
$if(subtitle)$
<h1 class="subtitle">$subtitle$</h1>
$endif$
<ul id="authorlist">
$for(author)$
<li>$author$</li>
$endfor$
</ul>
$if(date)$
<h3 class="date">$date$</h3>
$endif$
</div>
$endif$ $if(toc)$
<div id="$idprefix$TOC">$toc$</div>
$endif$
<h2 class="abstract">Abstract</h2>
<div id="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$
</div>
$body$ $for(include-after)$ $include-after$ $endfor$
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

-11
View File
@@ -42,7 +42,6 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
@@ -84,14 +83,4 @@ export default [
],
},
},
{
// Properties provided by AudioWorkletGlobalScope
files: ['packages/superdough/worklets.mjs'],
languageOptions: {
globals: {
currentTime: 'readonly',
sampleRate: 'readonly',
},
},
},
];
@@ -1,5 +1,4 @@
<script src="https://unpkg.com/@strudel/repl@1.2.7"></script>
<!-- <script src="../../packages/repl/dist/index.js"></script> -->
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
<strudel-editor>
<!--
// @date 23-08-15
-3
View File
@@ -20,8 +20,5 @@
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -14,8 +14,5 @@
},
"dependencies": {
"@strudel/web": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -18,8 +18,5 @@
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -13,8 +13,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -32,8 +32,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
/*
jsdoc-synonyms.js - Add support for @synonyms tag
jsdoc-synonyms.js - Add support for @synonym tag
Copyright (C) 2023 Strudel contributors - see <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 the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+1 -4
View File
@@ -73,8 +73,5 @@
"prettier": "^3.4.2",
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
}
+1 -1
View File
@@ -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://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs.
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs.
+64 -444
View File
@@ -1,464 +1,84 @@
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
//TODO: fix tonal scale import
// import { Scale } from '@tonaljs/tonal';
// import { soundMap } from '@strudel/webaudio';
let soundMap = undefined;
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
function plaintext(str) {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
};
}
const stripHtml = (html) => {
const div = document.createElement('div');
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
const getDocLabel = (doc) => doc.name || doc.longname;
const buildParamsList = (params) =>
params?.length
? `
<div class="autocomplete-info-params-section">
<h4 class="autocomplete-info-section-title">Parameters</h4>
<ul class="autocomplete-info-params-list">
${params
.map(
({ name, type, description }) => `
<li class="autocomplete-info-param-item">
<span class="autocomplete-info-param-name">${name}</span>
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
</li>
`,
)
.join('')}
</ul>
</div>
`
: '';
const buildExamples = (examples) =>
examples?.length
? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
</div>
`[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 function bankCompletions() {
// TODO: FIX IMPORT
const soundDict = soundMap?.get() ?? {};
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
if (suffix && bank) banks.add(bank);
}
return Array.from(banks)
.sort()
.map((name) => ({ label: name, type: 'bank' }));
export function Autocomplete({ doc, label }) {
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
${doc.description}
<ul>
${doc.params?.map(
({ name, type, description }) =>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
)}
</ul>
<div>
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
</div>
</div>`[0];
/*
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
*/
}
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
let scaleCompletions = [];
// try {
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
// } catch (e) {
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
// }
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
}));
// Valid mode values for voicing
const modeCompletions = [
{ label: 'below', type: 'mode' },
{ label: 'above', type: 'mode' },
{ label: 'duck', type: 'mode' },
{ label: 'root', type: 'mode' },
];
// Valid chord symbols from ireal dictionary plus empty string for major triads
const chordSymbols = ['', ...Object.keys(complex)].sort();
const chordSymbolCompletions = chordSymbols.map((symbol) => {
if (symbol === '') {
return {
label: 'major',
apply: '',
type: 'chord-symbol',
};
}
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
label: symbol,
apply: symbol,
type: 'chord-symbol',
};
});
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(', '),
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
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;
})();
// --- Handler functions for each context ---
const pitchNames = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'E#',
'Fb',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'Bb',
'B',
'B#',
'Cb',
];
// Cached regex patterns for scaleHandler
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
function scaleHandler(context) {
// First check for scale context without quotes - block with empty completions
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
if (scaleNoQuotesContext) {
return {
from: scaleNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
if (scaleAfterColonContext) {
const text = scaleAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
const options = filteredScales.map((s) => ({
...s,
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
}));
const from = scaleAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
if (scalePreColonContext) {
if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) {
const text = scalePreColonContext.text;
const match = text.match(SCALE_PITCH_MATCH_REGEX);
const fragment = match ? match[0] : '';
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const from = scalePreColonContext.to - fragment.length;
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options };
} else {
return { from: scalePreColonContext.to, options: [] };
}
}
}
return null;
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
// Cached regex patterns for soundHandler
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
function soundHandler(context) {
// First check for sound context without quotes - block with empty completions
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
if (soundNoQuotesContext) {
return {
from: soundNoQuotesContext.to,
options: [],
};
}
// Then check for sound context with quotes - provide completions
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
if (!soundContext) return null;
const text = soundContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
return {
from,
options,
};
}
// Cached regex patterns for bankHandler
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
function bankHandler(context) {
// First check for bank context without quotes - block with empty completions
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
if (bankNoQuotesContext) {
return {
from: bankNoQuotesContext.to,
options: [],
};
}
// Then check for bank context with quotes - provide completions
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
if (!bankMatch) return null;
const text = bankMatch.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragment = inside;
let banks = bankCompletions();
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
const from = bankMatch.to - fragment.length;
return {
from,
options: filteredBanks,
};
}
// Cached regex patterns for modeHandler
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
function modeHandler(context) {
// First check for mode context without quotes - block with empty completions
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
if (modeNoQuotesContext) {
return {
from: modeNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
if (modeAfterColonContext) {
const text = modeAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
// For anchor after colon, we can suggest pitch names
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
const from = modeAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
if (!modeContext) return null;
const text = modeContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
const from = modeContext.to - fragment.length;
return {
from,
options: filteredModes,
};
}
// Cached regex patterns for chordHandler
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
function chordHandler(context) {
// First check for chord context without quotes - block with empty completions
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
if (chordNoQuotesContext) {
return {
from: chordNoQuotesContext.to,
options: [],
};
}
// Then check for chord context with quotes - provide completions
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
if (!chordContext) return null;
const text = chordContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
// Use same fragment matching as sound/mode for expressions like "<G Am>"
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
// Check if fragment contains any pitch name at start (for root + symbol)
let rootMatch = null;
let symbolFragment = fragment;
for (const pitch of pitchNames) {
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
rootMatch = pitch;
symbolFragment = fragment.slice(pitch.length);
break;
}
}
if (rootMatch) {
// We have a root, now complete chord symbols
const filteredSymbols = chordSymbolCompletions.filter((s) =>
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
);
// Create completions that replace the entire chord, not just the symbol part
const options = filteredSymbols;
const from = chordContext.to - symbolFragment.length;
return { from, options };
} else {
// No root yet, complete with pitch names
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
const from = chordContext.to - fragment.length;
return { from, options };
}
}
// Cached regex patterns for fallbackHandler
const FALLBACK_WORD_REGEX = /\w*/;
function fallbackHandler(context) {
const word = context.matchBefore(FALLBACK_WORD_REGEX);
if (word && word.from === word.to && !context.explicit) return null;
if (word) {
return {
from: word.from,
options: jsdocCompletions,
};
}
return null;
}
const handlers = [
soundHandler,
bankHandler,
chordHandler,
scaleHandler,
modeHandler,
// this handler *must* be last
fallbackHandler,
];
export const strudelAutocomplete = (context) => {
for (const handler of handlers) {
const result = handler(context);
if (result) {
return result;
}
}
return null;
};
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
-63
View File
@@ -1,63 +0,0 @@
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]),
])();
+32 -103
View File
@@ -1,34 +1,31 @@
import { closeBrackets } from '@codemirror/autocomplete';
import { indentWithTab, toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
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 { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
drawSelection,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightActiveLine,
keymap,
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core';
import { cleanupDraw, Drawer } from '@strudel/draw';
import { repl, registerControl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { basicSetup } from './basicSetup.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
export const extensions = {
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
@@ -40,16 +37,8 @@ export 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),
]
: [],
};
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
export const defaultSettings = {
keybindings: 'codemirror',
@@ -62,8 +51,6 @@ export const defaultSettings = {
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -75,7 +62,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
});
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
@@ -88,17 +75,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
/* search(),
highlightSelectionMatches(), */
...initialSettings,
basicSetup,
mondo ? [] : javascript(),
javascriptLanguage.data.of({
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
}),
javascript(),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript
// extension? bracketMatching(), // does not do anything
// 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(
@@ -120,22 +103,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
preventDefault: true,
run: () => onStop?.(),
},
{
key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1),
},
{
key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1),
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
),
],
@@ -216,8 +191,7 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
// if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting)
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
@@ -235,7 +209,6 @@ export class StrudelMirror {
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
mondo: replOptions.mondo,
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
@@ -256,33 +229,6 @@ export class StrudelMirror {
}
};
document.addEventListener('start-repl', this.onStartRepl);
// Handle global evaluation requests (e.g., from Vim :w)
this.onEvaluateRequest = (e) => {
try {
// Evaluate current editor on repl-evaluate
logger('[repl] evaluate via event');
this.evaluate();
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-evaluate event', err);
}
};
document.addEventListener('repl-evaluate', this.onEvaluateRequest);
document.addEventListener('repl-stop', this.onStopRequest);
// Toggle comments requested from Vim (gc)
this.onToggleComment = (e) => {
try {
// Honor selections; toggleLineComment handles both selections and
// single line
toggleLineComment(this.editor);
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-toggle-comment event', err);
}
};
document.addEventListener('repl-toggle-comment', this.onToggleComment);
}
draw(haps, time, painters) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
@@ -302,23 +248,13 @@ export class StrudelMirror {
console.warn('first frame could not be painted');
}
}
async evaluate(autostart = true) {
async evaluate() {
this.flash();
await this.repl.evaluate(this.code, autostart);
await this.repl.evaluate(this.code);
}
async stop() {
this.repl.scheduler.stop();
}
// Listen for global stop requests (e.g., from Vim :q)
onStopRequest = (e) => {
try {
this.stop();
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-stop event', err);
}
};
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
@@ -394,18 +330,11 @@ export class StrudelMirror {
}
}
setCode(code) {
const changes = {
from: 0,
to: this.editor.state.doc.length,
insert: code,
};
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
}
getCursorLocation() {
return this.editor.state.selection.main.head;
@@ -420,7 +349,7 @@ export class StrudelMirror {
}
}
export function parseBooleans(value) {
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
import { RangeSetBuilder, StateEffect, StateField } 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 ? Prec.highest(highlightExtension) : [];
return on ? highlightExtension : [];
};
+2 -3
View File
@@ -1,7 +1,6 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
const template = document.createElement('template');
template.innerHTML = string.trim();
return template.content.childNodes;
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
-1
View File
@@ -4,4 +4,3 @@ export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';
export * from './widget.mjs';
export { Vim } from './keybindings.mjs';
+5 -117
View File
@@ -1,12 +1,10 @@
import { defaultKeymap } from '@codemirror/commands';
import { Prec, EditorState } from '@codemirror/state';
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { logger } from '@strudel/core';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -20,124 +18,14 @@ const vscodePlugin = ViewPlugin.fromClass(
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
// event 'repl-evaluate' that the editor listens for, and also simulate
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
// appears in the Console panel.
try {
if (Vim && typeof Vim.defineEx === 'function') {
// Map gc to toggle line comments by dispatching a custom event that our
// CodeMirror integration listens to. This avoids depending on Vim's
// internal actions and works with current selections/visual mode.
try {
Vim.defineAction('strudelToggleComment', (cm) => {
const view = cm?.view || cm;
try {
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
document.dispatchEvent(ev);
} catch (e) {
console.error('strudelToggleComment dispatch failed', e);
}
});
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
} catch (e) {
console.error('Vim gc mapping failed', e);
}
// :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => {
try {
const view = cm?.view || cm;
// First try dispatching our custom stop event, then fallback to Alt+.
let handled = false;
try {
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
handled = document.dispatchEvent(ev) === false;
} catch (e) {
console.error('Error dispatching repl-stop event', e);
}
if (!handled) {
const altDot = new KeyboardEvent('keydown', {
key: '.',
code: 'Period',
altKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(altDot);
}
} catch (e) {
console.error('Error dispatching :q stop event', e);
}
});
// :w to evaluate
Vim.defineEx('write', 'w', (cm) => {
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
try {
view?.focus?.();
// Let the app know this came from Vim :w
try {
logger('[vim] :w — evaluating code');
} catch (e) {
console.error('Error logging Vim :w evaluation', e);
}
// Dispatch a dedicated evaluate event first
let handled = false;
try {
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
} catch (e) {
console.error('Error dispatching repl-evaluate event', e);
}
if (handled) {
return;
}
// Try Ctrl+Enter first if not handled by custom event
const ctrlEnter = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
ctrlKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(ctrlEnter);
// If not handled (no handler called preventDefault), try Alt+Enter as
// fallback
if (!ctrlEnter.defaultPrevented) {
const altEnter = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
altKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(altEnter);
}
} catch (e) {
console.error('Error dispatching :w evaluation event', e);
}
});
}
} catch (e) {
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
}
const keymaps = {
vim,
emacs,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
};
export { Vim } from '@replit/codemirror-vim';
export function keybindings(name) {
const active = keymaps[name];
const extensions = active ? [Prec.high(active())] : [];
if (name === 'vim') {
extensions.push(EditorState.allowMultipleSelections.of(true));
}
return extensions;
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
}
-31
View File
@@ -1,31 +0,0 @@
import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
export function jumpToCharacter(view, character, direction = 1) {
const { state, dispatch } = view;
const pos = state.selection.main.head;
const cursor = new SearchCursor(state.doc, character);
let characterPositions = [];
let jumpPos;
while (!cursor.next().done) {
characterPositions.push(cursor.value.to);
}
if (!characterPositions.length) {
return false;
}
if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
} else {
jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0);
}
if (jumpPos == null) {
return false;
}
dispatch({
selection: EditorSelection.cursor(jumpPos - 1),
scrollIntoView: true,
});
return true;
}
+3 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.7",
"version": "1.2.2",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -42,20 +42,14 @@
"@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
"nanostores": "^0.11.3",
"superdough": "workspace:*"
"nanostores": "^0.11.3"
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-12
View File
@@ -8,11 +8,6 @@ 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';
@@ -44,20 +39,17 @@ import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
algoboy,
archBtw,
androidstudio,
atomone,
aura,
bbedit,
blackscreen,
bluescreen,
bluescreenlight,
CutiePi,
darcula,
dracula,
duotoneDark,
eclipse,
fruitDaw,
githubDark,
githubLight,
greenText,
@@ -86,12 +78,10 @@ export const themes = {
export const settings = {
strudelTheme: strudelThemeSettings,
bluescreen: bluescreenSettings,
bluescreenlight: bluescreenlightsettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
archBtw: archBtwSettings,
androidstudio: androidstudioSettings,
atomone: atomOneSettings,
aura: auraSettings,
@@ -102,11 +92,9 @@ export const settings = {
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
fruitDaw: fruitDawSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings,
-38
View File
@@ -1,38 +0,0 @@
/*
* 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] },
],
});
-37
View File
@@ -1,37 +0,0 @@
/*
* 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] },
],
});
-50
View File
@@ -1,50 +0,0 @@
/*
* 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] },
],
});
+4 -5
View File
@@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
import { Autocomplete } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
@@ -52,11 +52,10 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!doc) {
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) {
return null;
}
entry = getSynonymDoc(doc, word);
}
return {
@@ -67,7 +66,7 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete(entry);
const ac = Autocomplete({ doc: entry, label: word });
dom.appendChild(ac);
return { dom };
},
+6 -6
View File
@@ -1,11 +1,11 @@
import { describe, bench } from 'vitest';
import { calculateSteps, sequence, stack } from '../index.mjs';
import { calculateTactus, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('steps', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -14,7 +14,7 @@ describe('steps', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -25,7 +25,7 @@ describe('steps', () => {
});
describe('stack', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -34,7 +34,7 @@ describe('stack', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -43,4 +43,4 @@ describe('stack', () => {
{ time: 1000 },
);
});
calculateSteps(true);
calculateTactus(true);
-49
View File
@@ -1,49 +0,0 @@
import { describe, bench } from 'vitest';
import { calculateSteps, rand, useRNG } from '../index.mjs';
const testingResolution = 128;
const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle();
describe('old random', () => {
calculateSteps(true);
bench(
'+tactus',
() => {
useRNG('legacy');
_generateRandomPattern();
},
{
time: 1000,
teardown() {
useRNG('legacy');
},
},
);
calculateSteps(false);
bench(
'-tactus',
() => {
useRNG('precise');
_generateRandomPattern();
},
{
time: 1000,
teardown() {
useRNG('legacy');
},
},
);
});
describe('random', () => {
calculateSteps(true);
bench('+tactus', _generateRandomPattern, { time: 1000 });
calculateSteps(false);
bench('-tactus', _generateRandomPattern, { time: 1000 });
});
calculateSteps(true);
+69 -1275
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import createClock from './zyklus.mjs';
import { errorLogger, logger } from './logger.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
constructor({
@@ -57,7 +57,7 @@ export class Cyclist {
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
@@ -67,7 +67,6 @@ export class Cyclist {
// the following line is dumb and only here for backwards compatibility
// 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;
@@ -76,7 +75,7 @@ export class Cyclist {
}
});
} catch (e) {
errorLogger(e);
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
},
+27 -57
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Fraction from './fraction.mjs';
const left = function (n, x) {
const [ons, offs] = n;
@@ -35,33 +35,36 @@ const right = function (n, x) {
return result;
};
const _bjorklund = function (n, x) {
const _bjork = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
export const bjorklund = function (ons, steps) {
export const bjork = function (ons, steps) {
const inverted = ons < 0;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
const result = _bjorklund([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
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;
};
/**
* Changes the structure of the pattern to form an Euclidean rhythm.
* Euclidean rhythms are rhythms obtained using the greatest common
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common
* divisor of two numbers. They were described in 2004 by Godfried
* Toussaint, a Canadian computer scientist. Euclidean rhythms are
* Toussaint, a canadian computer scientist. Euclidian 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
@@ -73,7 +76,7 @@ export const bjorklund = 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
@@ -83,13 +86,13 @@ export const bjorklund = 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)
@@ -128,7 +131,7 @@ export const bjorklund = function (ons, steps) {
*/
const _euclidRot = function (pulses, steps, rotation) {
const b = bjorklund(pulses, steps);
const b = bjork(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
@@ -139,14 +142,6 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const bjork = register('bjork', 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));
});
@@ -156,10 +151,8 @@ 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)
*/
@@ -168,13 +161,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, 0);
const bin_pat = _euclidRot(pulses, steps, rotation);
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
return pat.struct(timeCat(...gapless));
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
@@ -187,7 +180,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
@@ -196,26 +189,3 @@ 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(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
-3
View File
@@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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);
@@ -20,7 +18,6 @@ export const evalScope = async (...args) => {
modules.forEach((module) => {
Object.entries(module).forEach(([name, value]) => {
globalThis[name] = value;
strudelScope[name] = value;
});
});
return modules;
-2
View File
@@ -126,8 +126,6 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;
+7 -2
View File
@@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
export class Hap {
/*
@@ -149,7 +148,13 @@ export class Hap {
}
showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
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
}`;
}
combineContext(b) {
-90
View File
@@ -1,90 +0,0 @@
/*
stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { register, reify, Pattern } from './pattern.mjs';
let timelines = {};
export const reset_state = function () {
reset_timelines();
};
export const reset_timelines = function () {
timelines = {};
};
/***
* Allows you to switch a pattern between different 'timelines'. This is particularly useful when
* live coding, for example when you want to cue a pattern up to play from its start.
*
* Timelines are specified by number, so that if you had a pattern like
* `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1'
* to '2', it would always align '0' to the nearest cycle. You will likely want to trigger
* an evaluation a little bit before the cycle starts, to avoid missing events.
*
* After the first use, a timeline will continue with the same 'offset'. That is, if you change
* a pattern without changing its timeline number, it will stay on that timeline without resetting.
*
* Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2`
* and `2`. This is because when you negate a timeline it will always reset.
*
* You can also pattern the timeline if you want, to create strange resetting patterns.
* @param {number | Pattern} timeline The timeline that the pattern should play on.
* @example
* n("<0 1 2 3>(3,8)")
* .sound("num")
* // resets the timeline every two cycles, by negating the timeline.
* // in a lot of cases this will be edited by a human live coder
* // rather than patterned!
* .timeline("<2 -2>".slow(2))
*/
export const timeline = register(
'timeline',
function (tpat, pat) {
tpat = reify(tpat);
const f = function (state) {
// Is this called from the scheduler? (rather than from e.g. the visualiser)
const scheduler = !!state.controls.cyclist;
const timehaps = tpat.query(state);
const result = [];
for (const timehap of timehaps) {
const tlid = timehap.value;
let offset;
if (tlid === 0) {
offset = 0;
} else if (tlid in timelines) {
offset = timelines[tlid];
} else {
const timearc = timehap.wholeOrPart();
if (!scheduler || state.span.begin.lt(timearc.midpoint())) {
offset = timearc.begin;
} else {
// Sync to end of timearc if we first see it over halfway into its
// timespan. Allows 'cuing up' next timeline when live coding.
offset = timearc.end;
}
}
if (scheduler) {
// update state
timelines[tlid] = offset;
if (tlid !== 0) {
delete timelines[-tlid];
}
}
const pathaps = pat
.late(offset)
.query(state.setSpan(timehap.part))
.map((h) => h.setContext(h.combineContext(timehap)));
result.push(...pathaps);
}
return result;
};
return new Pattern(f, pat._steps);
},
false,
);
+9 -10
View File
@@ -1,6 +1,6 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -11,21 +11,20 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls, createClock };
export * from './controls.mjs';
export * from './cyclist.mjs';
export * from './evaluate.mjs';
export * from './hap.mjs';
export * from './impure.mjs';
export * from './logger.mjs';
export * from './pattern.mjs';
export * from './pick.mjs';
export * from './repl.mjs';
export * from './signal.mjs';
export * from './speak.mjs';
export * from './pick.mjs';
export * from './state.mjs';
export * from './schedulerState.mjs';
export * from './timespan.mjs';
export * from './ui.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
-7
View File
@@ -4,13 +4,6 @@ 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) {
+3 -1
View File
@@ -11,6 +11,7 @@ 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
@@ -38,7 +39,8 @@ export class NeoCyclist {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.6",
"version": "1.2.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -31,15 +31,11 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@kabelsalat/web": "^0.4.1",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+37 -554
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import TimeSpan from './timespan.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@@ -21,12 +21,9 @@ import {
numeralArgs,
parseNumeral,
pairs,
zipWith,
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { errorLogger, logger } from './logger.mjs';
import { strudelScope } from './evaluate.mjs';
import { logger } from './logger.mjs';
let stringParser;
@@ -99,7 +96,10 @@ export class Pattern {
// runs func on query state
withState(func) {
return new Pattern((state) => this.query(func(state)));
return this.withHaps((haps, state) => {
func(state);
return haps;
});
}
/**
@@ -415,7 +415,7 @@ export class Pattern {
try {
return this.query(new State(new TimeSpan(begin, end), controls));
} catch (err) {
errorLogger(err, 'query');
logger(`[query]: ${err.message}`, 'error');
return [];
}
}
@@ -575,8 +575,7 @@ export class Pattern {
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern
* @example
* s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
* @noAutocomplete
*/
filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test));
@@ -587,11 +586,7 @@ export class Pattern {
* inside haps.
* @param {Function} value_test
* @returns Pattern
* @example
* const drums = s("bd sd bd sd")
* kick: drums.filterValues((v) => v.s === 'bd').duck(2)
* snare: drums.filterValues((v) => v.s === 'sd')
* bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
* @noAutocomplete
*/
filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
@@ -857,29 +852,14 @@ export class Pattern {
);
}
/**
* 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 })) {
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
/**
* 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));
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
//////////////////////////////////////////////////////////////////////
@@ -889,31 +869,6 @@ 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();
}
}
//////////////////////////////////////////////////////////////////////
@@ -949,13 +904,12 @@ Pattern.prototype.collect = function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
export const arpWith = register('arpWith', (func, pat) => {
return pat
.collect()
Pattern.prototype.arpWith = function (func) {
return this.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.
@@ -963,11 +917,9 @@ export const arpWith = register('arpWith', (func, pat) => {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]")
* */
export const arp = register(
'arp',
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
false,
);
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
};
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
@@ -1002,7 +954,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
return !Array.isArray(x) && typeof x === 'object';
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
@@ -1249,8 +1201,7 @@ 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
@@ -1303,14 +1254,13 @@ 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:
@@ -1387,11 +1337,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps);
}
/**
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: `cat`
*
* @return {Pattern}
* @synonyms cat
* @example
* slowcat("e5", "b4", ["d5", "c5"])
*
@@ -1591,15 +1541,9 @@ 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 | string[]} name name of the function, or an array of names to be used as synonyms
* @param {string} name name of the function
* @param {function} func function with 1 or more params, where last is the current pattern
* @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
* in their raw form and it will be up to you to patternify them and/or query them for values
* @example
* const vlpf = register('vlpf', (freq, pat) => {
* return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
* })
* s("saw").seg(8).velocity(rand).vlpf(800)
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
@@ -1684,9 +1628,7 @@ export function register(name, func, patternify = true, preserveSteps = false, j
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
const curried = curry(pfunc, null, arity);
strudelScope[name] = curried;
return curried;
return curry(pfunc, null, arity);
}
// Like register, but defaults to stepJoin
@@ -2039,7 +1981,6 @@ export const apply = register('apply', function (func, pat) {
/**
* Plays the pattern at the given cycles per minute.
* @deprecated
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
@@ -2248,7 +2189,7 @@ export const brak = register('brak', function (pat) {
});
/**
* Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern.
* Reverse all haps in a pattern
*
* @name rev
* @memberof Pattern
@@ -2280,23 +2221,6 @@ export const rev = register(
true,
);
/**
* Reverse a whole pattern. See also `rev` for reversing each cycle.
*
* @name revv
* @memberof Pattern
* @returns Pattern
* @example
* // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get
* // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of
* // cycles stays the same.
* note("<[c d] [e g]>").revv()
*/
export const revv = register('revv', function (pat) {
const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin));
return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan);
});
/** Like press, but allows you to specify the amount by which each
* event is shifted. pressBy(0.5) is the same as press, while
* pressBy(1/3) shifts each event by a third of its timespan.
@@ -2427,57 +2351,6 @@ 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
@@ -2605,8 +2478,8 @@ export const { chunkBack, chunkback } = register(
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7"
* .scale("C2:major").note()
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
@@ -2617,37 +2490,6 @@ export const { fastchunk, fastChunk } = register(
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
export const bypass = register(
'bypass',
@@ -2663,7 +2505,7 @@ export const bypass = register(
* 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
* @synonym rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
@@ -2690,13 +2532,8 @@ export const hsl = register('hsl', (h, s, l, pat) => {
/**
* Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
* @name tag
* @noAutocomplete
* @param {string} tag anything unique
* @example
* s("saw!16").note("F1")
* .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
* .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
* .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
* .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
*/
Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
@@ -2707,16 +2544,15 @@ Pattern.prototype.tag = function (tag) {
* @name filter
* @param {Function} test function to test Hap
* @example
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
*/
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/**
* Filters haps by their begin time
* @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin
* @example
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
*/
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
@@ -2883,7 +2719,7 @@ export function stepcat(...timepats) {
if (timepats.length === 0) {
return nothing;
}
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
timepats = timepats.map(findsteps);
if (timepats.find((x) => x[0] === undefined)) {
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
@@ -3035,24 +2871,6 @@ export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
/**
* *Experimental*
*
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const replicate = stepRegister('replicate', function (factor, pat) {
return pat.repeatCycles(factor).fast(factor).expand(factor);
});
/**
* *Experimental*
*
@@ -3382,10 +3200,10 @@ export const slice = register(
* @memberof Pattern
* @returns Pattern
* @example
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => {
const diff = targetTime - currentTime;
window.setTimeout(() => {
func(hap);
@@ -3522,338 +3340,3 @@ 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))));
};
const _distortWithAlg = function (name) {
const func = function (args, pat) {
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
if (!pat) {
return pure({}).distort(argsPat);
}
return pat.distort(argsPat);
};
Pattern.prototype[name] = function (args) {
return func(args, this);
};
return func;
};
/**
* Soft-clipping distortion
*
* @name soft
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const soft = _distortWithAlg('soft');
/**
* Hard-clipping distortion
*
* @name hard
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const hard = _distortWithAlg('hard');
/**
* Cubic polynomial distortion
*
* @name cubic
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const cubic = _distortWithAlg('cubic');
/**
* Diode-emulating distortion
*
* @name diode
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const diode = _distortWithAlg('diode');
/**
* Asymmetrical diode distortion
*
* @name asym
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const asym = _distortWithAlg('asym');
/**
* Wavefolding distortion
*
* @name fold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const fold = _distortWithAlg('fold');
/**
* Wavefolding distortion composed with sinusoid
*
* @name sinefold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const sinefold = _distortWithAlg('sinefold');
/**
* Distortion via Chebyshev polynomials
*
* @name chebyshev
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
export const chebyshev = _distortWithAlg('chebyshev');
/**
* Turns a list of patterns into a single pattern which outputs list-values
*
* @name parray
* @returns Pattern
*/
export const parray = (pats) => {
const pack = (...xs) => xs;
let acc = pure(curry(pack, null, pats.length));
for (const p of pats) acc = acc.appBoth(reify(p));
return acc;
};
const _ensureListPattern = (list) => {
if (Array.isArray(list)) {
return parray(list);
}
return reify(list);
};
/**
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
*
* Can also be used to create a new synth via `s('user').partials(...)`
*
* @name partials
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
* @example
* s("user").seg(16).n(irand(8)).scale("A:major")
* .partials([1, 0, 1, 0, 0, 1])
* @example
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
* .partials(binaryL(irand(256).add("1")))
*/
Pattern.prototype.partials = function (list) {
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const partials = (list) => {
return _ensureListPattern(list).as('partials');
};
/**
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
*
* @name phases
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
* @example
* // Phase cancellation
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
* .partials(partials([1, 1, 1]))
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
*/
Pattern.prototype.phases = function (list) {
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const phases = (list) => {
return _ensureListPattern(list).as('phases');
};
/**
* Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
* establish the controls of the given effect. See examples.
* @name FX
* @memberof Pattern
* @returns Pattern
* @example
* $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
* .FX(
* phaser(0.5).gain(2),
* bpf(800),
* distort(1.3),
* room(0.2),
* delay(0.5).gain(1.25),
* distort(0.3),
* ).fxr(1.7) // sets release time of effects (like delay)
* @example
* $: s("saw").fm(0.5)
* .delay(0.3) // outer effects are applied *last*
* .FX(coarse(4)) // first coarse
* .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
* .FX(distort(1)) // then distort
*/
Pattern.prototype.FX = function (...effects) {
effects = effects.map(reify);
return this.withValue((v) => (vEff) => {
const currFX = v.FX ?? [];
return { ...v, FX: currFX.concat(vEff) };
}).appLeft(parray(effects));
};
const _asArrayPattern = (pats) => {
const pack = (...xs) => xs;
let acc = pure(curry(pack, null, pats.length));
for (const p of pats) acc = acc.appLeft(p);
return acc;
};
/**
* Produces a [Kabelsalat](https://kabel.salat.dev/) modular sound engine.
* This can be used as either an effect (by including `audioin()` at the beginning
* of your kabel expression) or as a sound source (via any expression which doesn't
* start with `audioin()`).
*
* Some helpers you have available to you:
* * Strudel mini notation works fine in K(..) via "" or ``
* * More complex Strudel expressions (like "0 1 2".fast(4) or irand(24)) can be
* written by wrapping them in `S(..)` inside your Kabel code
* * We expose Strudel's note frequency under `sFreq` and Strudel's gate
* information under `sGate`
* * You can use more complex multi-line expressions (like `let x = a; let y = b; x.lpf(y);`)
* by wrapping them inside a function in K (see example).
*
* @name K
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
* @memberof Pattern
* @returns Pattern
*
* @example
* note("A c e".fast(4)).transpose("<0 2 4 6 8>")
* .scale("F:minor").transpose("12")
* .s("saw")
* .K(
* // audioin().mul(sGate.adsr(0.001, 0.3, 0, 0.2)) // as effect
* saw(saw(sFreq / "2!3 16").mul(8).add(sFreq).lag("0!3 0.1")).mul(0.3) // as source
* .mul(sGate.adsr(0, 0.15, 0.5, "0.1!3 1"))
* .lpf(sGate.adsr(0, 0.2, 0.3, 0.2).mul(1).add(0))
* .add(x => x.delay(S("0.3 0.2".fast(2))).mul(0.7))
* .add(x => x.delay("0.03 [0.08 0.01] 0.01 0.013").mul(0.77)).mul(0.7)
* .add(x => x.delay(.13).mul(0.7))
* .out()
* )
*
* @example
* n("<0 1 <2 3 2 4>>*16")
* .scale("G#2:minor").sometimes(x => x.transpose("12 | 24"))
* .K(() => {
* const att = S(rand.range(0, 0.05))
* const dec = S(rand.range(0.05, 0.2))
* let f = n(sFreq);
* const mod = sine(f).mul("0.1 | 0.2 | 0.3")
* .add("[[1.5 1] | 1 | 2 | 4 | [6 4@3]]*2")
* saw(f.mul(mod))
* .mul(sGate.ad(att, dec))
* .add(x => x.delay(0.4).mul(0.3))
* .out()
* }).fxr(1).room(0.3)
*/
/**
* Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
* Kabelsalat code.
*
* @name worklet
* @param {string} src Source code of the worklet update function
* @param {...number | ...Pattern} inputs Worklet inputs
* @memberof Pattern
* @returns Pattern
* @noAutocomplete
*/
Pattern.prototype.worklet = function (src, ...inputs) {
inputs = inputs.map(reify);
return this.outerBind((v) => {
return _asArrayPattern(inputs).withValue((vInput) => {
const currInputs = v.workletInputs ?? [];
return { ...v, workletSrc: src, workletInputs: currInputs.concat(vInput) };
});
});
};
export const worklet = (...args) => pure({}).worklet(...args);
+14 -85
View File
@@ -1,18 +1,10 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import {
setCpsFunc,
setIsStarted,
setPattern as exposeSchedulerPattern,
setTime,
setTriggerFunc,
} from './schedulerState.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs';
import { SalatRepl } from '@kabelsalat/web';
export function repl({
defaultOutput,
@@ -29,9 +21,7 @@ export function repl({
setInterval,
clearInterval,
id,
mondo = false,
}) {
const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -60,11 +50,7 @@ export function repl({
getTime,
onToggle: (started) => {
updateState({ started });
setIsStarted(started);
onToggle?.(started);
if (!started) {
reset_state();
}
},
setInterval,
clearInterval,
@@ -74,8 +60,6 @@ export function repl({
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
setTriggerFunc(schedulerOptions.onTrigger);
setCpsFunc(() => scheduler.cps);
let pPatterns = {};
let anonymousIndex = 0;
let allTransform;
@@ -89,23 +73,9 @@ export function repl({
return silence;
};
const compileKabel = (code) => {
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
// 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);
exposeSchedulerPattern(pattern);
return pattern;
};
setTime(() => scheduler.now()); // TODO: refactor?
@@ -114,25 +84,8 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
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;
};
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@@ -149,9 +102,8 @@ export function repl({
* all(x => x.pianoroll())
* ```
*/
let allTransforms = [];
const all = function (transform) {
allTransforms.push(transform);
allTransform = 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.
@@ -173,9 +125,9 @@ export function repl({
// allows muting a pattern x with x_ or _x
return silence;
}
if (id.includes('$')) {
if (id === '$') {
// allows adding anonymous patterns with $:
id = `${id}${anonymousIndex}`;
id = `$${anonymousIndex}`;
anonymousIndex++;
}
pPatterns[id] = this;
@@ -215,7 +167,6 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
compileKabel,
});
};
@@ -228,29 +179,10 @@ 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 = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) {
// handle soloed patterns ex: S$: s("bd!4")
const isSolod = key.length > 1 && key.startsWith('S');
if (isSolod && soloActive === false) {
// first time we see a soloed pattern, clear existing patterns
patterns = [];
soloActive = true;
}
if (!soloActive || (soloActive && isSolod)) {
const valWithState = value.withState((state) => state.setControls({ id: key }));
patterns.push(valWithState);
}
}
let patterns = Object.values(pPatterns);
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
@@ -259,14 +191,12 @@ export function repl({
} else if (eachTransform) {
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (const transform of allTransforms) {
pattern = transform(pattern);
}
if (allTransform) {
pattern = allTransform(pattern);
}
if (!isPattern(pattern)) {
pattern = silence;
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
@@ -295,7 +225,6 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => {
// ^ 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) {
@@ -303,9 +232,9 @@ export const getTrigger =
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(hap, getTime(), cps, t);
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
}
} catch (err) {
errorLogger(err, 'getTrigger');
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
-53
View File
@@ -1,53 +0,0 @@
/*
schedulerState.mjs - Module to pipe out various parameters from the scheduler for global consumption
Copyright (C) 2026 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/schedulerState.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let time;
let cpsFunc;
let pattern;
let triggerFunc;
let isStarted;
export function getTime() {
if (!time) {
throw new Error('no time set! use setTime to define a time source');
}
return time();
}
export function setTime(func) {
time = func;
}
export function setCpsFunc(func) {
cpsFunc = func;
}
export function getCps() {
return cpsFunc?.();
}
export function setPattern(pat) {
pattern = pat;
}
export function getPattern() {
return pattern;
}
export function setTriggerFunc(func) {
triggerFunc = func;
}
export function getTriggerFunc() {
return triggerFunc;
}
export function setIsStarted(val) {
isStarted = !!val;
}
export function getIsStarted() {
return isStarted;
}
+45 -225
View File
@@ -16,7 +16,7 @@ export function steady(value) {
}
export const signal = (func) => {
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))];
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))];
return new Pattern(query);
};
@@ -71,6 +71,7 @@ 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))
@@ -99,6 +100,7 @@ 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")
@@ -186,97 +188,38 @@ export const mouseY = signal(() => _mouseY);
export const mousex = signal(() => _mouseX);
export const mouseX = signal(() => _mouseX);
// Random number generators
// random signals
// Produce "Avalanche effect" where flipping a single bit of x
// results in all output bits flipping with probability 0.5
// See e.g. https://github.com/aappleby/smhasher/blob/0ff96f7835817a27d0487325b6c16033e2992eb5/src/MurmurHash3.cpp#L68-L77
const _murmurHashFinalizer = (x) => {
x |= 0;
x ^= x >>> 16;
x = Math.imul(x, 0x85ebca6b);
x ^= x >>> 13;
x = Math.imul(x, 0xc2b2ae35);
x ^= x >>> 16;
return x >>> 0; // unsigned
};
// Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29
const _tToT = (t) => {
return Math.floor(t * 536870912);
};
// Used to decorrelate nearby T, i, and seed prior to hashing
const _decorrelate = (T, i = 0, seed = 0) => {
const lowBits = (T >>> 0) >>> 0;
const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32
let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35);
key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9);
key ^= Math.imul(seed ^ 0x165667b1, 0x27d4eb2d);
return key >>> 0;
};
const randAt = (T, i = 0, seed = 0) => {
return _murmurHashFinalizer(_decorrelate(T, i, seed)) / 4294967296; // 2^32
};
// n samples at time t
const timeToRands = (t, n, seed = 0) => {
const T = _tToT(t);
if (n === 1) {
return randAt(T, 0, seed);
}
const out = new Array(n);
for (let i = 0; i < n; i++) out[i] = randAt(T, i, seed);
return out;
};
// Old random signals. Currently the default, but can also be chosen via
// `useRNG('legacy')`
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
const __xorwise = (x) => {
const xorwise = (x) => {
const a = (x << 13) ^ x;
const b = (a >> 17) ^ a;
return (b << 5) ^ b;
};
const __frac = (x) => x - Math.trunc(x);
const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912));
const __intSeedToRand = (x) => (x % 536870912) / 536870912;
const __timeToRandsPrime = (seed, n) => {
if (n === 1) {
return Math.abs(__intSeedToRand(seed));
}
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
const _frac = (x) => x - Math.trunc(x);
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
const intSeedToRand = (x) => (x % 536870912) / 536870912;
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => {
const result = [];
for (let i = 0; i < n; i++) {
result.push(__intSeedToRand(seed));
seed = __xorwise(seed);
// eslint-disable-next-line
for (let i = 0; i < n; ++i) {
result.push(intSeedToRand(seed));
seed = xorwise(seed);
}
return result;
};
const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n);
// End old random
let RNG_MODE = 'legacy';
export const getRandsAtTime = (t, n = 1, seed = 0) => {
return RNG_MODE === 'legacy' ? __timeToRands(t + seed, n) : timeToRands(t, n, seed);
};
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* Sets which random number generator to use. Historically Strudel would
* use `useRNG('legacy')`, which remains the default. To use a new more statistically
* precise RNG, try `useRNG('precise')`.
*
* @name useRNG
* @param {string} mod - Mode. One of 'legacy', 'precise'
* @example
* useRNG('legacy')
* // Repeats every 300 cycles
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)
*/
export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
/**
* A discrete pattern of numbers from 0 to n-1
@@ -287,7 +230,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
export const run = (n) => saw.range(0, n).round().segment(n);
/**
* Creates a binary pattern from a number.
* Creates a pattern from a binary number.
*
* @name binary
* @param {number} n - input number to convert to binary
@@ -301,7 +244,7 @@ export const binary = (n) => {
};
/**
* Creates a binary pattern from a number, padded to n bits long.
* Creates a pattern from a binary number, padded to n bits long.
*
* @name binaryN
* @param {number} n - input number to convert to binary
@@ -317,58 +260,13 @@ export const binaryN = (n, nBits = 16) => {
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
};
/**
* Creates a binary list pattern from a number.
*
* @name binaryL
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
*/
export const binaryL = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
return binaryNL(n, nBits);
};
/**
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
export const binaryNL = (n, nBits = 16) => {
return reify(n)
.withValue((v) => (bits) => {
const bList = [];
for (let i = bits - 1; i >= 0; i--) {
bList.push((v >> i) & 1);
}
return bList;
})
.appLeft(reify(nBits));
};
/**
* Creates a list of random numbers of the given length
*
* @name randL
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
return signal((t, controls) => {
return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,...
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
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];
@@ -387,7 +285,7 @@ const _rearrangeWith = (ipat, n, pat) => {
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
* seq("c d e f".shuffle(4), "g").note().sound("piano")
* note("c d e f".shuffle(4), "g").sound("piano")
*/
export const shuffle = register('shuffle', (n, pat) => {
return _rearrangeWith(randrun(n), n, pat);
@@ -400,43 +298,12 @@ export const shuffle = register('shuffle', (n, pat) => {
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
* seq("c d e f".scramble(4), "g").note().sound("piano")
* note("c d e f".scramble(4), "g").sound("piano")
*/
export const scramble = register('scramble', (n, pat) => {
return _rearrangeWith(_irand(n)._segment(n), n, pat);
});
/**
* Modify a pattern by applying a function to the `randomSeed` control if present
*
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
* @param {Pattern} pat Pattern to update
* @returns Pattern
*/
export const withSeed = (func, pat) => {
return new Pattern((state) => {
let { randSeed, ...controls } = state.controls;
randSeed = func(randSeed);
return pat.query(state.setControls({ ...controls, randSeed }));
}, pat._steps);
};
/**
* Change the seed for random signals. Normally, random signals depend on time,
* so two patterns at the same time will have the same random values. Specifying
* a new seed changes the signal output by `rand`. This also affects other functions
* that use randomness, like `shuffle` and `sometimes`.
*
* @name seed
* @param {number} n A new seed. Can be any number.
* @example
* $: s("hh*4").degrade();
* $: s("bd*4").degrade().seed(1); // Will degrade different events from the hi-hat
*/
export const seed = register('seed', (n, pat) => {
return withSeed(() => n, pat);
});
/**
* A continuous pattern of random numbers, between 0 and 1.
*
@@ -446,7 +313,7 @@ export const seed = register('seed', (n, pat) => {
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
*
*/
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
export const rand = signal(timeToRand);
/**
* A continuous pattern of random numbers, between -1 and 1
*/
@@ -531,10 +398,6 @@ 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
@@ -614,7 +477,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
* @example
* // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
@@ -623,32 +486,36 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair
export const wrandcat = wchooseCycles;
function _perlin(t, seed = 0) {
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);
const ra = getRandsAtTime(ta, 1, seed);
const rb = getRandsAtTime(tb, 1, seed);
const v = interp(t - ta)(ra)(rb);
const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb));
return v;
}
export const perlinWith = (tpat) => {
return tpat.fmap(_perlin);
};
function _berlin(t, seed = 0) {
function _berlin(t) {
const prevRidgeStartIndex = Math.floor(t);
const nextRidgeStartIndex = prevRidgeStartIndex + 1;
const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex, 1, seed);
const height = getRandsAtTime(nextRidgeStartIndex, 1, seed);
const nextRidgeTopPoint = prevRidgeBottomPoint + height;
const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex);
const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint;
const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex);
const interp = (a, b, t) => {
return a + t * (b - a);
return a + (b - a) * t;
};
return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2;
}
export const berlinWith = (tpat) => {
return tpat.fmap(_berlin);
};
/**
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
*
@@ -658,7 +525,7 @@ function _berlin(t, seed = 0) {
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
*
*/
export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
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,
@@ -670,7 +537,7 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
* n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")
*
*/
export const berlin = signal((t, controls) => _berlin(t, controls.randSeed));
export const berlin = berlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register(
'degradeByWith',
@@ -968,50 +835,3 @@ export const whenKey = register('whenKey', function (input, func, pat) {
export const keyDown = register('keyDown', function (pat) {
return pat.fmap(_keyDown);
});
/**
* A pattern measuring the duration of events,
* in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore
* event durations, from the pattern that it is combined with.
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
* See also its reciprocal, `per`, also known as `perCycle`.
* @example
* // Shorter events are lower in pitch
* sound("saw saw [saw saw] saw")
* .note(cyclesPer.range(50, 100))
* @example
* sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]")
* .note(cyclesPer.add(20))
*/
export const cyclesPer = new Pattern(function (state) {
return [new Hap(undefined, state.span, state.span.duration)];
});
/**
* A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events,
* in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore
* event durations, from the pattern that it is combined with.
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
* See also its reciprocal, `cyclesPer`.
* @synonyms perCycle
* @example
* // Shorter events are more distorted
* n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd")
* .distort(per.div(2))
*/
export const per = new Pattern(function (state) {
return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))];
});
export const perCycle = per;
/**
* Like `per` but measures the shortness of events according to an exponential curve. In
* particular, where the event duration halves, the
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
* the same as `"3 3 [4 [5 5]] 3"`.
*/
export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
});
+1 -1
View File
@@ -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);
});
});
+2 -2
View File
@@ -19,9 +19,9 @@ export class State {
return this.setSpan(func(this.span));
}
// Returns new State with added controls.
// Returns new State with different controls
setControls(controls) {
return new State(this.span, { ...this.controls, ...controls });
return new State(this.span, controls);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
controls.test.mjs - <short description TODO>
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/controls.test.mjs>
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+1 -1
View File
@@ -1,6 +1,6 @@
/*
drawLine.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/drawLine.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/drawLine.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-89
View File
@@ -1,89 +0,0 @@
import { bjorklund } from '../euclid.mjs';
import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs';
describe('bjorklund', () => {
it('should apply bjorklundlund to ons and steps', () => {
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjorklund(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']);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
/*
fraction.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/fraction.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/fraction.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+18 -93
View File
@@ -1,12 +1,12 @@
/*
pattern.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import {
TimeSpan,
@@ -52,11 +52,8 @@ 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';
@@ -586,18 +583,6 @@ describe('Pattern', () => {
.map((a) => a.value),
).toStrictEqual(['c', 'b', 'a']);
});
it('Does not reverse the order of cycles', () => {
expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
fastcat('b', 'a', 'd', 'c').firstCycle(),
);
});
});
describe('revv()', () => {
it('Does reverse the order of cycles', () => {
expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
fastcat('d', 'c', 'b', 'a').firstCycle(),
);
});
});
describe('sequence()', () => {
it('Can work like fastcat', () => {
@@ -740,6 +725,20 @@ describe('Pattern', () => {
);
});
});
describe('signal()', () => {
it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
});
it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
});
});
describe('_setContext()', () => {
it('Can set the hap context', () => {
expect(
@@ -885,7 +884,7 @@ describe('Pattern', () => {
);
});
it('Doesnt drop haps in the 9th cycle', () => {
// fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
});
});
@@ -1002,7 +1001,7 @@ describe('Pattern', () => {
});
describe('hurry', () => {
it('Can speed up patterns and sounds', () => {
sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
/*describe('composable functions', () => {
@@ -1180,9 +1179,6 @@ 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', () => {
@@ -1271,75 +1267,4 @@ 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();
});
});
});
-61
View File
@@ -1,61 +0,0 @@
/*
signal.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
};
describe('signal()', () => {
it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
});
it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
});
});
describe('cyclesPer', () => {
it('gives cycles per hap', () => {
sameFirst(
cyclesPer.struct(true, true, true, fastcat(true, true)),
sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction),
);
});
});
describe('per', () => {
it('gives haps per cycle', () => {
sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction));
});
});
describe('perx', () => {
it('gives exponential haps per cycle', () => {
sameFirst(
perx.struct(true, true, true, fastcat(true, fastcat(true, true))),
sequence(3, 3, 3, fastcat(4, fastcat(5, 5))),
);
});
});
+10 -57
View File
@@ -1,30 +1,26 @@
/*
util.test.mjs - Tests for the core 'util' module
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/util.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
flatten,
fractionalArgs,
freqToMidi,
getFrequency,
getPlayableNoteValue,
isNote,
midiToFreq,
noteToMidi,
numeralArgs,
parseFractional,
parseNumeral,
rotate,
splitAt,
tokenizeNote,
zipWith,
parseFractional,
numeralArgs,
fractionalArgs,
} from '../util.mjs';
import { describe, expect, it } from 'vitest';
import { describe, it, expect } from 'vitest';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@@ -237,46 +233,3 @@ 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]);
});
});
+11
View File
@@ -0,0 +1,11 @@
let time;
export function getTime() {
if (!time) {
throw new Error('no time set! use setTime to define a time source');
}
return time();
}
export function setTime(func) {
time = func;
}
+1 -1
View File
@@ -72,7 +72,7 @@ export class TimeSpan {
}
intersection(other) {
// Intersection of two timespans, returns undefined if they don't intersect.
// Intersection of two timespans, returns None if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
+4 -20
View File
@@ -7,13 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name);
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 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 [];
}
@@ -23,10 +23,6 @@ export const tokenizeNote = (note) => {
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 };
export const getAccidentalsOffset = (accidentals) => {
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
};
// turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
@@ -34,7 +30,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = getAccidentalsOffset(acc);
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
@@ -166,7 +162,6 @@ 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;
@@ -242,7 +237,6 @@ 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) {
@@ -491,13 +485,3 @@ 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;
}
+3 -3
View File
@@ -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((hap, currentTime, _cps, targetTime) => {
return pat.onTrigger((time_deprecate, 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((hap, currentTime, _cps, targetTime) => {
return pat.onTrigger((tidal_time, hap) => {
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 = targetTime - currentTime;
const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.7",
"version": "1.2.3",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
@@ -38,8 +38,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((hap, currentTime, cps, targetTime) => {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
+1 -1
View File
@@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
const collator = new ClockCollator({});
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
const controls = parseControlsFromHap(hap, cps);
const params = [];
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
+1 -4
View File
@@ -25,8 +25,5 @@
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
"homepage": "https://codeberg.org/uzu/strudel#readme"
}
+2 -11
View File
@@ -84,18 +84,9 @@ Pattern.prototype.onPaint = function (painter) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
return state;
});
};
// TODO - Why isn't this pure deep copy not working?
// Pattern.prototype.onPaint = function (painter) {
// return this.withState((state) => {
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
// return new State(state.span, { ...state.controls, painters });
// });
// };
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
@@ -159,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.whole && h.endClipped >= phase - lookbehind - lookahead)
.filter((h) => h.endClipped >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
@@ -184,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 });
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.6",
"version": "1.2.2",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.1.2",
"version": "1.1.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
@@ -20,8 +20,5 @@
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
"homepage": "https://codeberg.org/uzu/strudel#readme"
}
-6
View File
@@ -40,12 +40,6 @@ const pattern = sequence([
- D-Pad
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
- Stick Buttons
- `l3`, `r3` (or `ls`, `rs`)
- Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`)
- System Buttons
- `start`, `back` (or uppercase `START`, `BACK`)
- Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`)
### Analog Sticks
- Left Stick
+7 -12
View File
@@ -29,10 +29,6 @@ The gamepad module provides access to buttons and analog sticks as normalized si
| | 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`) |
| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) |
| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) |
| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) |
| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) |
### Analog Sticks
@@ -60,15 +56,14 @@ You can use button inputs to control different aspects of your music, such as ga
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
setcpm(120)
// Use button values to control amplitude
// Use button values to control amplitude
$: stack(
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
)
).cpm(120)
`}
/>
@@ -79,13 +74,14 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
setcpm(120)
// Use analog stick for continuous control
// Use analog stick for continuous control
$: note("c4 d3 a3 e3").sound("sawtooth")
.lpf(gp.x1.range(100,4000))
.lpq(gp.y1.range(5,30))
.decay(gp.y2.range(0.1,2))
.lpenv(gp.x2.range(-5,5))`}
.lpenv(gp.x2.range(-5,5))
.cpm(120)
`}
/>
### Button Sequences
@@ -93,7 +89,6 @@ $: note("c4 d3 a3 e3").sound("sawtooth")
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
<MiniRepl client:idle tune={`const gp = gamepad(0)
setcpm(120)
// Define button sequences
const HADOUKEN = [
'd', // Down
@@ -104,7 +99,7 @@ const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
$: s("free_hadouken -").slow(2)
.mask(gp.btnSequence(HADOUKEN)).room(1)
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
// hadouken.wav by Syna-Max
//https://freesound.org/people/Syna-Max/sounds/67674/
-4
View File
@@ -14,10 +14,6 @@ export const buttonMap = {
rt: 7,
back: 8,
start: 9,
l3: 10,
ls: 10,
r3: 11,
rs: 11,
u: 12,
up: 12,
d: 13,
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.6",
"version": "1.2.2",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -34,8 +34,5 @@
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-1
View File
@@ -35,7 +35,6 @@ export async function initHydra(options = {}) {
hydra.synth.s0.init({ src: canvas });
}
}
return hydra;
}
export function clearHydra() {
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.6",
"version": "1.2.2",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
@@ -40,8 +40,5 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+60 -248
View File
@@ -5,24 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import {
Hap,
Pattern,
TimeSpan,
getCps,
getIsStarted,
getPattern,
getTime,
getTriggerFunc,
isPattern,
logger,
ref,
reify,
} from '@strudel/core';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { getAudioContext } from '@strudel/webaudio';
import { scheduleAtTime } from '../superdough/helpers.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -205,7 +190,7 @@ function mapCC(mapping, value) {
}
// sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, targetTime) {
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -213,23 +198,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
scheduleAtTime(() => {
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
// sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, targetTime) {
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');
}
scheduleAtTime(() => {
device.sendProgramChange(progNum, midichan);
}, targetTime);
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
}
// sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, targetTime) {
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');
@@ -244,13 +225,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255');
}
scheduleAtTime(() => {
device.sendSysex(sysexid, sysexdata);
}, targetTime);
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
}
// sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
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');
@@ -258,34 +237,28 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
} 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');
}
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan);
}, targetTime);
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, targetTime) {
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');
}
scheduleAtTime(() => {
device.sendPitchBend(midibend, midichan);
}, targetTime);
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
}
// sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, targetTime) {
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');
}
scheduleAtTime(() => {
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
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, targetTime) {
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
if (note == null || note === '') {
throw new Error('note cannot be null or empty');
}
@@ -295,12 +268,12 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
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 });
scheduleAtTime(() => {
device.playNote(midiNote, midichan);
}, targetTime);
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
}
/**
@@ -336,6 +309,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
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
@@ -359,13 +333,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, _currentTime, cps, targetTime) => {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
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}`;
// midi event values from hap with configurable defaults
let {
note,
@@ -401,7 +380,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// 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, targetTime));
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(', ')}`);
@@ -413,12 +392,12 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// 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, targetTime);
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
}
// Handle program change
if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, targetTime);
sendProgramChange(progNum, device, midichan, timeOffsetString);
}
// Handle sysex
@@ -428,76 +407,74 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, targetTime);
sendSysex(sysexid, sysexdata, device, timeOffsetString);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, targetTime);
sendCC(ccn, ccv, device, midichan, timeOffsetString);
}
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, targetTime);
sendPitchBend(midibend, device, midichan, timeOffsetString);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, targetTime);
sendAftertouch(miditouch, device, midichan, timeOffsetString);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendClock();
}, targetTime);
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendStop();
}, targetTime);
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendContinue();
}, targetTime);
device.sendContinue({ time: timeOffsetString });
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, targetTime);
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
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, targetTime);
sendSysex(id, data, device, timeOffsetString);
}
}
}
});
};
let listeners = {};
const refs = {};
/**
* Initialize a midi device
* 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")
*/
async function _initialize(input) {
export async function midin(input) {
if (isPattern(input)) {
throw new Error(
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
@@ -506,191 +483,26 @@ async function _initialize(input) {
const device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
return device;
}
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
*
* @name midin
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
* that came through that cc number (and channel, if provided)
* @example
* const 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")
* @example
* const allCC = await midin('IAC Driver Bus 1')
* const cc = (ccNum) => allCC(ccNum, 2) // just channel 2
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/
let listeners = {};
const refs = {};
const refsByChan = {};
export async function midin(input) {
const device = await _initialize(input);
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
if (chan !== undefined) {
return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const [ccNum, v] = e.dataBytes;
const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
/**
* MIDI keyboard: Opens a MIDI input port to receive MIDI keyboard messages.
*
* The note length is fixed as Superdough is not currently set up for undetermined
* note durations
*
* @name midikeys
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
* When queried, the pattern will produces the most recently played midi notes and velocities,
* lasting for the specified duration
* @example
* const kb = await midikeys('Arturia KeyStep 32')
* kb().s("tri").lpf(80).lpe(6).lpd(0.1).room(2).delay(0.35)
* @example
* const kb = await midikeys('Arturia KeyStep 32')
* kb("0.5 1")
* .s("saw")
* .add(note(rand.mul(0.3)))
* .lpf(1000).lpe(2).room(0.5)
*/
const kHaps = {};
const kListeners = {};
function _triggerKeyboard(input, cps, now, latencyCycles) {
const pattern = getPattern();
const trigger = getTriggerFunc();
if (!pattern || !trigger) {
return false;
}
const t = now + latencyCycles;
const eps = 1e-6;
const haps = pattern.queryArc(t - eps, t + eps, { _cps: cps });
// Only keep haps coming from `midikeys`
const kbHaps = haps.filter((hap) => hap.value?.midikey?.startsWith(`${input}_`));
const ctxNow = getAudioContext().currentTime;
if (!kbHaps.length) {
return false;
}
kbHaps.forEach((hap) => {
if (!hap.hasOnset()) {
return;
}
const t = ctxNow + (hap.whole.begin - now) / cps;
const duration = hap.duration / cps;
trigger(hap, t - ctxNow, duration, cps, t);
});
return true;
}
export async function midikeys(input) {
const device = await _initialize(input);
if (!kHaps[input]) {
kHaps[input] = [];
}
kListeners[input] && device.removeListener('midimessage', kListeners[input]);
kListeners[input] = (e) => {
const { dataBytes, message } = e;
const noteon = message.command === 9;
let noteoff = message.command === 8;
// Don't enqueue or trigger midi notes if scheduler is not started
const notStarted = !getIsStarted();
// Ignore non-note messages (e.g. CC, pitchbend, modwheel, etc.)
const notANote = !noteon && !noteoff;
if (notStarted || notANote) {
return;
}
const [note, velocity] = dataBytes;
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
const key = `${input}_${note}`;
const cps = getCps() ?? 0.5;
const triggerAvailable = !!(getPattern() && getTriggerFunc());
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
const now = getTime();
const t = now + latencySeconds * cps;
const span = new TimeSpan(t, t);
let value = { midikey: key };
if (noteoff) {
/* TODO: It's a big effort, but we could modify superdough to allow for situations where
we don't know the hap duration in advance. This would mean, for example, that if the hap
is flagged as such a special note-on event, we have all effects be persistent & all ADSR
envelopes stop at the S stage [and store references to them by `midikey`]
If this is implemented, then getting full keyboard functionality should be as simple
as sending the corresponding note-off event below and triggering `release` on each of those
referenced effects/envelopes
value = { ...value, noteoff: true };
If this is achieved, we can remove the noteLength parameter
*/
return;
} else {
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
}
kHaps[input].push(new Hap(span, span, value, {}));
if (!noteoff && triggerAvailable) {
// If we have access to a trigger function, we call it to immediately
// dispatch to the audio engine, rather than waiting for cyclist to catch these haps
const triggered = _triggerKeyboard(input, cps, now, latencySeconds * cps);
if (triggered) {
kHaps[input] = [];
}
}
};
device.addListener('midimessage', kListeners[input]);
const kb = (noteLength = 0.5) => {
const nlPat = reify(noteLength);
const query = (state) => {
const haps = kHaps[input].flatMap((hap) => {
const lenHaps = nlPat.query(state.setSpan(hap.wholeOrPart()));
return lenHaps.map((lenHap) => {
const nl = lenHap.value ?? 0.5;
const whole = new TimeSpan(hap.whole.begin, hap.whole.begin.add(nl));
const part = new TimeSpan(hap.part.begin, hap.part.begin.add(nl));
const context = hap.combineContext(lenHap);
return new Hap(whole, part, hap.value, context);
});
});
if (state.controls.cyclist) {
// Notes have been sent; clear them
kHaps[input] = [];
}
return haps;
};
return new Pattern(query);
};
return kb;
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.7",
"version": "1.2.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -4
View File
@@ -1,10 +1,10 @@
import { describe, bench } from 'vitest';
import { calculateSteps } from '../../core/index.mjs';
import { calculateTactus } from '../../core/index.mjs';
import { mini } from '../index.mjs';
describe('mini', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -13,7 +13,7 @@ describe('mini', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -21,5 +21,5 @@ describe('mini', () => {
},
{ time: 1000 },
);
calculateSteps(true);
calculateTactus(true);
});
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.6",
"version": "1.2.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -38,8 +38,5 @@
"peggy": "^4.2.0",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
mini.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/test/mini.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-39
View File
@@ -1,39 +0,0 @@
# 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
```
-487
View File
@@ -1,487 +0,0 @@
/*
mondo.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// 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: /^</,
close_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-~_^#]+/,
};
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
// 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, types) {
while (true) {
let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value));
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];
}
// for each precendence group, call desugar_ops once
this.op_precedence.forEach((ops) => {
children = this.desugar_ops(children, ops);
});
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);
}
}
-40
View File
@@ -1,40 +0,0 @@
{
"name": "mondolang",
"version": "1.1.2",
"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 <flix91@gmail.com>",
"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"
},
"engines": {
"node": ">=18.0.0"
}
}
-979
View File
@@ -1,979 +0,0 @@
/*
mondo.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, 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('<a b c>')).toEqual('(angle a b c)'));
it('should desugar <> nested', () => expect(desguar('<a <b c> d>')).toEqual('(angle a (angle b c) d)'));
it('should desugar mixed [] <>', () => expect(desguar('[a <b c>]')).toEqual('(square a (angle b c))'));
it('should desugar mixed <> []', () => expect(desguar('<a [b c]>')).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('<bd, hh>')).toEqual('(stack bd hh)'));
it('should desugar , angle 2', () => expect(desguar('<bd, hh oh>')).toEqual('(stack bd (angle hh oh))'));
it('should desugar , angle 3', () =>
expect(desguar('<bd cp, hh oh>')).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 []*<x y>', () => 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 x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) 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) <mt ht lt>] # 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 Newtons 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
// Ackermanns 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 Newtons 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]));
});
-19
View File
@@ -1,19 +0,0 @@
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',
},
});
-3
View File
@@ -1,3 +0,0 @@
# @strudel/mondough
connects mondo to strudel.
-134
View File
@@ -1,134 +0,0 @@
import {
strudelScope,
reify,
fast,
slow,
seq,
stepcat,
replicate,
expand,
pace,
chooseIn,
degradeBy,
silence,
bjork,
} 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['!'] = replicate;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib['&'] = bjork;
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,
}); */
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@strudel/mondo",
"version": "1.1.6",
"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 <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/transpiler": "workspace:*",
"mondolang": "workspace:*"
},
"devDependencies": {
"mondo": "*",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-19
View File
@@ -1,19 +0,0 @@
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',
},
});

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