mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-17 16:16:09 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95ecd73d49 | |||
| a9995d185f | |||
| 5da2085436 | |||
| ae9820b39f |
@@ -1,46 +0,0 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
deployments: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: "npm"
|
||||
- name: Install Dependencies
|
||||
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: "./out"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
+1
-4
@@ -34,7 +34,4 @@ repl_old
|
||||
tutorial.rendered.mdx
|
||||
doc.json
|
||||
talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
talk/public/samples
|
||||
Vendored
+1
-4
@@ -2,8 +2,5 @@
|
||||
"cSpell.words": [
|
||||
"subspan",
|
||||
"vals"
|
||||
],
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
-18
@@ -12,7 +12,7 @@ To get in touch with the contributors, either
|
||||
|
||||
## Ask a Question
|
||||
|
||||
If you have any questions about strudel, make sure you've read the
|
||||
If you have any questions about strudel, make sure you've read the
|
||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
||||
If not, use one of the Communication Channels above!
|
||||
|
||||
@@ -31,9 +31,10 @@ Use one of the Communication Channels listed above.
|
||||
|
||||
## Improve the Tutorial
|
||||
|
||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
||||
|
||||
|
||||
This will even work without setting up a development environment, only a github account is required.
|
||||
|
||||
## Propose a Feature
|
||||
@@ -51,11 +52,11 @@ Please check that it has not been reported before.
|
||||
To fix a bug that has been reported,
|
||||
|
||||
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
||||
2. fork the repository
|
||||
3. make sure you've setup the project (see below)
|
||||
4. hopefully fix the bug
|
||||
5. make sure the tests pass
|
||||
6. send a pull request
|
||||
3. fork the repository
|
||||
4. make sure you've setup the project (see below)
|
||||
5. hopefully fix the bug
|
||||
6. make sure the tests pass
|
||||
7. send a pull request
|
||||
|
||||
## Write Tests
|
||||
|
||||
@@ -79,7 +80,7 @@ cd repl && npm i # install repl dependencies
|
||||
npm run start # start repl
|
||||
```
|
||||
|
||||
Those commands might look slightly different for your OS.
|
||||
Those commands might look slightly different for your OS.
|
||||
Please report any problems you've had with the setup instructions!
|
||||
|
||||
## Code Style
|
||||
@@ -96,7 +97,7 @@ If you use VSCode, you can
|
||||
|
||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
which allows developing multiple packages at the same time
|
||||
|
||||
## Package Publishing
|
||||
@@ -104,18 +105,9 @@ which allows developing multiple packages at the same time
|
||||
To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
npx lerna publish
|
||||
```
|
||||
|
||||
### New Packages
|
||||
|
||||
To add a new package, you have to publish it manually the first time, using:
|
||||
|
||||
```sh
|
||||
cd packages/<package-name> && npm publish --access public
|
||||
```
|
||||
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/tidalcycles/strudel/actions)
|
||||
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
|
||||
|
||||
- Try it here: <https://strudel.tidalcycles.org/>
|
||||
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
|
||||
@@ -38,12 +38,6 @@ Click on the package names to find out more about each one.
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
<a href="https://github.com/tidalcycles/strudel/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
## Community
|
||||
|
||||
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
||||
|
||||
-1806
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 150 KiB |
Generated
+1178
-3310
File diff suppressed because it is too large
Load Diff
+5
-12
@@ -1,20 +1,17 @@
|
||||
{
|
||||
"name": "@strudel.cycles/monorepo",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"scripts": {
|
||||
"test": "vitest run --version",
|
||||
"test-ui": "vitest --ui",
|
||||
"test-coverage": "vitest --coverage",
|
||||
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||
"snapshot": "cd repl && npm run snapshot",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i",
|
||||
"repl": "cd repl && npm run dev",
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
||||
"preview": "npx serve ./out",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||
"deploy": "gh-pages -d out",
|
||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
||||
},
|
||||
@@ -39,16 +36,12 @@
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.21.1",
|
||||
"c8": "^7.12.0",
|
||||
"events": "^3.3.0",
|
||||
"gh-pages": "^4.0.0",
|
||||
"happy-dom": "^6.0.4",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.21.1"
|
||||
"mocha": "^9.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Packages
|
||||
|
||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
||||
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||
|
||||
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
||||
|
||||

|
||||
|
||||
Generated with
|
||||
|
||||
```sh
|
||||
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
||||
```
|
||||
+30
-43
@@ -11,7 +11,7 @@ const generic_params = [
|
||||
/**
|
||||
* Select a sound / sample by name.
|
||||
*
|
||||
* <details style={{display:'none'}}>
|
||||
* <details>
|
||||
* <summary>show all sounds</summary>
|
||||
*
|
||||
* 808 (6) 808bd (25) 808cy (25) 808hc (5) 808ht (5) 808lc (5) 808lt (5) 808mc (5) 808mt (5) 808oh (5) 808sd (25) 909 (1) ab (12) ade (10) ades2 (9) ades3 (7) ades4 (6) alex (2) alphabet (26) amencutup (32) armora (7) arp (2) arpy (11) auto (11) baa (7) baa2 (7) bass (4) bass0 (3) bass1 (30) bass2 (5) bass3 (11) bassdm (24) bassfoo (3) battles (2) bd (24) bend (4) bev (2) bin (2) birds (10) birds3 (19) bleep (13) blip (2) blue (2) bottle (13) breaks125 (2) breaks152 (1) breaks157 (1) breaks165 (1) breath (1) bubble (8) can (14) casio (3) cb (1) cc (6) chin (4) circus (3) clak (2) click (4) clubkick (5) co (4) coins (1) control (2) cosmicg (15) cp (2) cr (6) crow (4) d (4) db (13) diphone (38) diphone2 (12) dist (16) dork2 (4) dorkbot (2) dr (42) dr2 (6) dr55 (4) dr_few (8) drum (6) drumtraks (13) e (8) east (9) electro1 (13) em2 (6) erk (1) f (1) feel (7) feelfx (8) fest (1) fire (1) flick (17) fm (17) foo (27) future (17) gab (10) gabba (4) gabbaloud (4) gabbalouder (4) glasstap (3) glitch (8) glitch2 (8) gretsch (24) gtr (3) h (7) hand (17) hardcore (12) hardkick (6) haw (6) hc (6) hh (13) hh27 (13) hit (6) hmm (1) ho (6) hoover (6) house (8) ht (16) if (5) ifdrums (3) incoming (8) industrial (32) insect (3) invaders (18) jazz (8) jungbass (20) jungle (13) juno (12) jvbass (13) kicklinn (1) koy (2) kurt (7) latibro (8) led (1) less (4) lighter (33) linnhats (6) lt (16) made (7) made2 (1) mash (2) mash2 (4) metal (10) miniyeah (4) monsterb (6) moog (7) mouth (15) mp3 (4) msg (9) mt (16) mute (28) newnotes (15) noise (1) noise2 (8) notes (15) numbers (9) oc (4) odx (15) off (1) outdoor (6) pad (3) padlong (1) pebbles (1) perc (6) peri (15) pluck (17) popkick (10) print (11) proc (2) procshort (8) psr (30) rave (8) rave2 (4) ravemono (2) realclaps (4) reverbkick (1) rm (2) rs (1) sax (22) sd (2) seawolf (3) sequential (8) sf (18) sheffield (1) short (5) sid (12) sine (6) sitar (8) sn (52) space (18) speakspell (12) speech (7) speechless (10) speedupdown (9) stab (23) stomp (10) subroc3d (11) sugar (2) sundance (6) tabla (26) tabla2 (46) tablex (3) tacscan (22) tech (13) techno (7) tink (5) tok (4) toys (13) trump (11) ul (10) ulgab (5) uxay (3) v (6) voodoo (5) wind (10) wobble (1) world (3) xmas (1) yeah (31)
|
||||
@@ -23,7 +23,7 @@ const generic_params = [
|
||||
* @name s
|
||||
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
||||
* @example
|
||||
* s("bd hh")
|
||||
* s("bd hh").osc()
|
||||
*
|
||||
*/
|
||||
['s', 's', 'sound'],
|
||||
@@ -62,12 +62,12 @@ const generic_params = [
|
||||
*/
|
||||
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
|
||||
/**
|
||||
* Controls the gain by an exponential amount.
|
||||
* Like {@link amp}, but exponential.
|
||||
*
|
||||
* @name gain
|
||||
* @param {number | Pattern} amount gain.
|
||||
* @example
|
||||
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
|
||||
* s("bd*8").gain(".7*2 1 .7*2 1 .7 1").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -129,7 +129,7 @@ const generic_params = [
|
||||
* @name bandf
|
||||
* @param {number | Pattern} frequency center frequency
|
||||
* @example
|
||||
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
|
||||
* s("bd sd").bandf("<1000 2000 4000 8000>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
||||
@@ -140,19 +140,17 @@ const generic_params = [
|
||||
* @name bandq
|
||||
* @param {number | Pattern} q q factor
|
||||
* @example
|
||||
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
|
||||
* s("bd sd").bandf(2000).bandq("<.2 .9>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
|
||||
/**
|
||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name begin
|
||||
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
|
||||
* @example
|
||||
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
|
||||
* s("rave").begin("<0 .25 .5 .75>")
|
||||
* s("rave").begin("<0 .25 .5 .75>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -161,13 +159,12 @@ const generic_params = [
|
||||
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
|
||||
],
|
||||
/**
|
||||
* The same as .begin, but cuts off the end off each sample.
|
||||
* The same as {@link begin}, but cuts off the end off each sample.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name end
|
||||
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
|
||||
* @example
|
||||
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
|
||||
* s("bd*2,ho*4").end("<.1 .2 .5 1>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -205,7 +202,7 @@ const generic_params = [
|
||||
* @name crush
|
||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||
* @example
|
||||
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
|
||||
* s("<bd sd>,hh*3,jvbass*2").fast(2).crush("<16 8 7 6 5 4 3 2>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -214,12 +211,12 @@ const generic_params = [
|
||||
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
|
||||
],
|
||||
/**
|
||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
* fake-resampling for lowering the sample rate
|
||||
*
|
||||
* @name coarse
|
||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||
* @example
|
||||
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
|
||||
* s("xmas").coarse("<1 4 8 16 32>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -256,7 +253,7 @@ const generic_params = [
|
||||
* @name cutoff
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @example
|
||||
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
|
||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff("<4000 2000 1000 500 200 100>").osc()
|
||||
*
|
||||
*/
|
||||
// TODO: add lpf synonym
|
||||
@@ -267,7 +264,7 @@ const generic_params = [
|
||||
* @name hcutoff
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @example
|
||||
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
|
||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff("<4000 2000 1000 500 200 100>").osc()
|
||||
*
|
||||
*/
|
||||
// TODO: add hpf synonym
|
||||
@@ -277,12 +274,12 @@ const generic_params = [
|
||||
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
|
||||
],
|
||||
/**
|
||||
* Applies the resonance of the high-pass filter.
|
||||
* Applies the cutoff frequency of the high-pass filter.
|
||||
*
|
||||
* @name hresonance
|
||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
||||
* @example
|
||||
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
|
||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff(2000).hresonance("<0 .2 .4 .6>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -295,15 +292,15 @@ const generic_params = [
|
||||
* Applies the cutoff frequency of the low-pass filter.
|
||||
*
|
||||
* @name resonance
|
||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
|
||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff(2000).resonance("<0 .2 .4 .6>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
|
||||
// TODO: add lpq synonym?
|
||||
/**
|
||||
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
||||
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
|
||||
*
|
||||
* @name djf
|
||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||
@@ -371,7 +368,7 @@ const generic_params = [
|
||||
* @name fadeTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
* s("ho*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -496,7 +493,7 @@ const generic_params = [
|
||||
* @name pan
|
||||
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
|
||||
* @example
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>")
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -584,11 +581,6 @@ const generic_params = [
|
||||
'size',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'roomsize',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
// ['f', 'sagogo', ''],
|
||||
// ['f', 'sclap', ''],
|
||||
// ['f', 'sclaves', ''],
|
||||
@@ -599,7 +591,7 @@ const generic_params = [
|
||||
* @name shape
|
||||
* @param {number | Pattern} distortion between 0 and 1
|
||||
* @example
|
||||
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
|
||||
* s("bd sd").shape("<0 .2 .4 .6 .8 1>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -656,16 +648,16 @@ const generic_params = [
|
||||
// ['f', 'tomdecay', ''],
|
||||
// ['f', 'vcfegint', ''],
|
||||
// ['f', 'vcoegint', ''],
|
||||
// TODO: Use a rest (~) to override the effect <- vowel
|
||||
/**
|
||||
*
|
||||
* Formant filter to make things sound like vowels.
|
||||
*
|
||||
* @name vowel
|
||||
* @param {string | Pattern} vowel You can use a e i o u.
|
||||
* @param {string | Pattern} vowel You can use a e i o u. Use a rest (~) to override the effect
|
||||
* @example
|
||||
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
|
||||
* .vowel("<a e i <o u>>")
|
||||
* vowel("a e i [o u]").slow(2)
|
||||
* .n("<[0,7]!4 [2,7]!4>")
|
||||
* .s('supersquare').osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -752,30 +744,25 @@ const generic_params = [
|
||||
['f', 'uid', ''],
|
||||
['f', 'val', ''],
|
||||
['f', 'cps', ''],
|
||||
['f', 'clip', ''],
|
||||
];
|
||||
|
||||
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
|
||||
|
||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||
|
||||
const _setter = (func, name) =>
|
||||
const _setter = (func) =>
|
||||
function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap((value) => ({ [name]: value }));
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
};
|
||||
|
||||
generic_params.forEach(([type, name, description]) => {
|
||||
controls[name] = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(controls[name], name);
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
});
|
||||
|
||||
// create custom param
|
||||
controls.createParam = (name) => {
|
||||
const func = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(func, name);
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
return (...pats) => _name(name, ...pats);
|
||||
};
|
||||
|
||||
|
||||
+28
-37
@@ -25,70 +25,61 @@ const euclid = (pulses, steps, rotation = 0) => {
|
||||
* describe a large number of rhythms used in the most important music world traditions.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name euclid
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation (optional) offset in steps
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* // The Cuban tresillo pattern.
|
||||
* note("c3").euclid(3,8)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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").euclid(3,5,2)
|
||||
* @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)
|
||||
* "c3".euclid(3,8)
|
||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||
* "c3".euclid(2,5)
|
||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||
* "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.
|
||||
* "c3".euclid(3,5,2)
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||
* "c3".euclid(3,7)
|
||||
* @example // The Cuban tresillo pattern.
|
||||
* "c3".euclid(3,8)
|
||||
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
||||
* note("c3").euclid(4,7)
|
||||
* "c3".euclid(4,7)
|
||||
* @example // The Aksak rhythm of Turkey.
|
||||
* note("c3").euclid(4,9)
|
||||
* "c3".euclid(4,9)
|
||||
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
||||
* note("c3").euclid(4,11)
|
||||
* "c3".euclid(4,11)
|
||||
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
||||
* note("c3").euclid(5,6)
|
||||
* "c3".euclid(5,6)
|
||||
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
||||
* note("c3").euclid(5,7)
|
||||
* "c3".euclid(5,7)
|
||||
* @example // The Cuban cinquillo pattern.
|
||||
* note("c3").euclid(5,8)
|
||||
* "c3".euclid(5,8)
|
||||
* @example // A popular Arab rhythm called Agsag-Samai.
|
||||
* note("c3").euclid(5,9)
|
||||
* "c3".euclid(5,9)
|
||||
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
||||
* note("c3").euclid(5,11)
|
||||
* "c3".euclid(5,11)
|
||||
* @example // The Venda clapping pattern of a South African children’s song.
|
||||
* note("c3").euclid(5,12)
|
||||
* "c3".euclid(5,12)
|
||||
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
||||
* note("c3").euclid(5,16)
|
||||
* "c3".euclid(5,16)
|
||||
* @example // A typical rhythm played on the Bendir (frame drum).
|
||||
* note("c3").euclid(7,8)
|
||||
* "c3".euclid(7,8)
|
||||
* @example // A common West African bell pattern.
|
||||
* note("c3").euclid(7,12)
|
||||
* "c3".euclid(7,12)
|
||||
* @example // A Samba rhythm necklace from Brazil.
|
||||
* note("c3").euclid(7,16,14)
|
||||
* "c3".euclid(7,16,14)
|
||||
* @example // A rhythm necklace used in the Central African Republic.
|
||||
* note("c3").euclid(9,16)
|
||||
* "c3".euclid(9,16)
|
||||
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
||||
* note("c3").euclid(11,24,14)
|
||||
* "c3".euclid(11,24,14)
|
||||
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
||||
* note("c3").euclid(13,24,5)
|
||||
* "c3".euclid(13,24,5)
|
||||
*/
|
||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
||||
return this.struct(euclid(pulses, steps, rotation));
|
||||
};
|
||||
|
||||
/**
|
||||
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||
* Similar to {@link Pattern#euclid}, but each pulse is held until the next pulse, so there will be no gaps.
|
||||
*/
|
||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
||||
const bin_pat = euclid(pulses, steps, rotation);
|
||||
|
||||
+92
-10
@@ -4,9 +4,87 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
// import Fraction from 'fraction.js';
|
||||
import { TimeSpan } from './timespan.mjs';
|
||||
|
||||
let instances = 0;
|
||||
let strings = 0;
|
||||
let fractions = 0;
|
||||
let numbers = 0;
|
||||
setInterval(() => {
|
||||
console.log(`${instances} calls = ${numbers} numbers + ${fractions} fractions + ${strings} strings`);
|
||||
instances = 0;
|
||||
strings = 0;
|
||||
fractions = 0;
|
||||
numbers = 0;
|
||||
}, 1000);
|
||||
|
||||
// http://localhost:3000/#c3RhY2soCiAgImUzLGJiMyxkNCIuc3RydWN0KCJ4KDMsOCwtMSkiKS5vZmYoMS84LHg9PngudHJhbnNwb3NlKDEyKS52ZWxvY2l0eSguMikpLAogICJjMiIuc3RydWN0KCJ4KDQsOCwtMikiKSwKICAiYzMiLnN0cnVjdCgieCgzLDgsLTIpIi5mYXN0KDIpKQopLnNsb3coMikKIC5lY2hvKDQsLjEyNSwuOCkKIC52ZWxvY2l0eShzaW5lLnN0cnVjdCgieCo4IikuYWRkKDMvNSkubXVsKDIvNSkuZmFzdCg4KSkKIC8vIC5waWFub3JvbGwoKQovLyBzdHJ1ZGVsIGRpc2FibGUtaGlnaGxpZ2h0aW5n
|
||||
// ~400k/s
|
||||
|
||||
// this is a "mock" for fraction.js, using just floats without any rational arithmetic
|
||||
// to test if the performance gets better without fraction.js
|
||||
// result: it seems to get better but not by much
|
||||
// the main jankyness remains for some complicated patterns
|
||||
|
||||
class Fraction {
|
||||
value; // number
|
||||
constructor(value) {
|
||||
instances++;
|
||||
if (value instanceof Fraction) {
|
||||
// TODO: return this?
|
||||
this.value = value.value;
|
||||
fractions++;
|
||||
} else if (typeof value === 'string') {
|
||||
const [n, d] = value.split('/');
|
||||
this.value = n / (d || 1);
|
||||
strings++;
|
||||
} else if (typeof value !== 'number' || isNaN(value)) {
|
||||
console.warn('Fraction got NaN', value);
|
||||
} else {
|
||||
numbers++;
|
||||
this.value = Number(value);
|
||||
if (isNaN(this.value)) {
|
||||
console.warn('Fraction parsed NaN from', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
add(other) {
|
||||
return new Fraction(this.value + other);
|
||||
}
|
||||
sub(other) {
|
||||
return new Fraction(this.value - other);
|
||||
}
|
||||
mul(other) {
|
||||
return new Fraction(this.value * other);
|
||||
}
|
||||
div(other) {
|
||||
return new Fraction(this.value / other);
|
||||
}
|
||||
toString() {
|
||||
return this.value + '';
|
||||
}
|
||||
valueOf() {
|
||||
return this.value;
|
||||
}
|
||||
floor() {
|
||||
return new Fraction(Math.floor(this.value));
|
||||
}
|
||||
abs() {
|
||||
return new Fraction(Math.abs(this.value));
|
||||
}
|
||||
inverse() {
|
||||
return new Fraction(1 / this.value);
|
||||
}
|
||||
compare(other) {
|
||||
return this.value - other;
|
||||
}
|
||||
equals(other) {
|
||||
return this.value.valueOf() === other.valueOf();
|
||||
}
|
||||
// TODO: toFraction
|
||||
}
|
||||
|
||||
// Returns the start of the cycle.
|
||||
Fraction.prototype.sam = function () {
|
||||
return this.floor();
|
||||
@@ -14,12 +92,16 @@ Fraction.prototype.sam = function () {
|
||||
|
||||
// Returns the start of the next cycle.
|
||||
Fraction.prototype.nextSam = function () {
|
||||
// return new Fraction(Math.floor(this.value) + 1);
|
||||
return this.sam().add(1);
|
||||
};
|
||||
|
||||
// Returns a TimeSpan representing the begin and end of the Time value's cycle
|
||||
Fraction.prototype.wholeCycle = function () {
|
||||
return new TimeSpan(this.sam(), this.nextSam());
|
||||
/* const begin = Math.floor(this.value);
|
||||
const end = begin + 1;
|
||||
return new TimeSpan(begin, end); */
|
||||
};
|
||||
|
||||
// The position of a time value relative to the start of its cycle.
|
||||
@@ -63,20 +145,20 @@ Fraction.prototype.or = function (other) {
|
||||
return this.eq(0) ? other : this;
|
||||
};
|
||||
|
||||
const fraction = (n) => {
|
||||
/* const fraction = (n) => {
|
||||
if (typeof n === 'number') {
|
||||
/*
|
||||
https://github.com/infusion/Fraction.js/#doubles
|
||||
„If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences."
|
||||
„If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations“
|
||||
// https://github.com/infusion/Fraction.js/#doubles
|
||||
// „If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences."
|
||||
// „If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations“
|
||||
|
||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||
*/
|
||||
// -> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||
// -> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||
n = String(n);
|
||||
}
|
||||
return Fraction(n);
|
||||
};
|
||||
}; */
|
||||
|
||||
const fraction = (n) => new Fraction(n);
|
||||
|
||||
export const gcd = (...fractions) => {
|
||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// this is a shortcut to eval code from a gist
|
||||
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
||||
export default (route, cache = true) =>
|
||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
||||
export default (route) =>
|
||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
|
||||
.then((res) => res.text())
|
||||
.then((code) => eval(code));
|
||||
|
||||
@@ -86,9 +86,11 @@ export class Hap {
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
||||
}`;
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
|
||||
+2
-16
@@ -15,19 +15,5 @@ export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
export const version = p.version; */
|
||||
console.log(
|
||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
||||
);
|
||||
if (globalThis._strudelLoaded) {
|
||||
console.warn(
|
||||
`@strudel.cycles/core was loaded more than once...
|
||||
This might happen when you have multiple versions of strudel installed.
|
||||
Please check with "npm ls @strudel.cycles/core".`,
|
||||
);
|
||||
}
|
||||
globalThis._strudelLoaded = true;
|
||||
export * as gist from './gist.js';
|
||||
// export * from './value.mjs';
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,5 +28,8 @@
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.2.2"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
||||
}
|
||||
|
||||
+70
-431
@@ -15,7 +15,6 @@ import drawLine from './drawLine.mjs';
|
||||
|
||||
/** @class Class representing a pattern. */
|
||||
export class Pattern {
|
||||
_Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
|
||||
/**
|
||||
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
||||
*
|
||||
@@ -468,11 +467,7 @@ export class Pattern {
|
||||
/**
|
||||
* Assumes a numerical pattern. Returns a new pattern with all values rounded
|
||||
* to the nearest integer.
|
||||
* @name round
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0.5 1.5 2.5".round().scale('C major').note()
|
||||
*/
|
||||
round() {
|
||||
return this._asNumber().fmap((v) => Math.round(v));
|
||||
@@ -517,16 +512,13 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern, containing unipolar values in the range 0 .. 1.
|
||||
* Returns a new pattern with values scaled to the given min/max range.
|
||||
* Most useful in combination with continuous patterns.
|
||||
* @name range
|
||||
* @memberof Pattern
|
||||
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
|
||||
* 1. Returns a new pattern with values scaled to the given min/max range.
|
||||
* @param {Number} min
|
||||
* @param {Number} max
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
|
||||
*/
|
||||
_range(min, max) {
|
||||
range(min, max) {
|
||||
return this.mul(max - min).add(min);
|
||||
}
|
||||
|
||||
@@ -538,8 +530,8 @@ export class Pattern {
|
||||
* @param {Number} max
|
||||
* @returns Pattern
|
||||
*/
|
||||
_rangex(min, max) {
|
||||
return this._range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
rangex(min, max) {
|
||||
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,8 +541,8 @@ export class Pattern {
|
||||
* @param {Number} max
|
||||
* @returns Pattern
|
||||
*/
|
||||
_range2(min, max) {
|
||||
return this._fromBipolar()._range(min, max);
|
||||
range2(min, max) {
|
||||
return this._fromBipolar().range(min, max);
|
||||
}
|
||||
|
||||
_bindWhole(choose_whole, func) {
|
||||
@@ -649,26 +641,13 @@ export class Pattern {
|
||||
return this._trigJoin(true);
|
||||
}
|
||||
|
||||
// Like the other joins above, joins a pattern of patterns of values, into a flatter
|
||||
// pattern of values. In this case it takes whole cycles of the inner pattern to fit each event
|
||||
// in the outer pattern.
|
||||
_squeezeJoin() {
|
||||
// A pattern of patterns, which we call the 'outer' pattern, with patterns
|
||||
// as values which we call the 'inner' patterns.
|
||||
const pat_of_pats = this;
|
||||
function query(state) {
|
||||
// Get the events with the inner patterns. Ignore continuous events (without 'wholes')
|
||||
const haps = pat_of_pats.discreteOnly().query(state);
|
||||
// A function to map over the events from the outer pattern.
|
||||
function flatHap(outerHap) {
|
||||
// Get the inner pattern, slowed and shifted so that the 'whole'
|
||||
// timespan of the outer event corresponds to the first cycle of the
|
||||
// inner event
|
||||
const inner_pat = outerHap.value._focusSpan(outerHap.wholeOrPart());
|
||||
// Get the inner events, from the timespan of the outer event's part
|
||||
const innerHaps = inner_pat.query(state.setSpan(outerHap.part));
|
||||
// A function to map over the inner events, to combine them with the
|
||||
// outer event
|
||||
const pat = outerHap.value._compressSpan(outerHap.wholeOrPart().cycleArc());
|
||||
const innerHaps = pat.query(state.setSpan(outerHap.part));
|
||||
function munge(outer, inner) {
|
||||
let whole = undefined;
|
||||
if (inner.whole && outer.whole) {
|
||||
@@ -699,27 +678,10 @@ export class Pattern {
|
||||
return this.fmap(func)._squeezeJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like layer, but with a single function:
|
||||
* @name apply
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
|
||||
*/
|
||||
_apply(func) {
|
||||
return func(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers the result of the given function(s). Like {@link superimpose}, but without the original pattern:
|
||||
* @name layer
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* .layer(x=>x.add("0,2"))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
layer(...funcs) {
|
||||
return stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
@@ -750,25 +712,15 @@ export class Pattern {
|
||||
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
};
|
||||
const ef = function (hap) {
|
||||
const begin = hap.part.begin;
|
||||
const end = hap.part.end;
|
||||
const cycle = begin.sam();
|
||||
const beginPos = begin.sub(cycle).div(factor).min(1);
|
||||
const endPos = end.sub(cycle).div(factor).min(1);
|
||||
const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos));
|
||||
const newWhole = !hap.whole
|
||||
? undefined
|
||||
: new TimeSpan(
|
||||
newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)),
|
||||
newPart.end.add(hap.whole.end.sub(end).div(factor)),
|
||||
);
|
||||
return new Hap(newWhole, newPart, hap.value, hap.context);
|
||||
const ef = function (span) {
|
||||
const cycle = span.begin.sam();
|
||||
const begin = cycle.add(span.begin.sub(cycle).div(factor).min(1));
|
||||
const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
};
|
||||
return this.withQuerySpan(qf)._withHap(ef)._splitQueries();
|
||||
return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
|
||||
}
|
||||
|
||||
// Compress each cycle into the given timespan, leaving a gap
|
||||
_compress(b, e) {
|
||||
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
|
||||
return silence;
|
||||
@@ -780,25 +732,15 @@ export class Pattern {
|
||||
return this._compress(span.begin, span.end);
|
||||
}
|
||||
|
||||
// Similar to compress, but doesn't leave gaps, and the 'focus' can be
|
||||
// bigger than a cycle
|
||||
_focus(b, e) {
|
||||
return this._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
|
||||
}
|
||||
|
||||
_focusSpan(span) {
|
||||
return this._focus(span.begin, span.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed up a pattern by the given factor. Used by "*" in mini notation.
|
||||
* Speed up a pattern by the given factor.
|
||||
*
|
||||
* @name fast
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} factor speed up factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||
* seq(e5, b4, d5, c5).fast(2)
|
||||
*/
|
||||
_fast(factor) {
|
||||
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
||||
@@ -806,14 +748,14 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
|
||||
* Slow down a pattern over the given number of cycles.
|
||||
*
|
||||
* @name slow
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} factor slow down factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||
* seq(e5, b4, d5, c5).slow(2)
|
||||
*/
|
||||
_slow(factor) {
|
||||
return this._fast(Fraction(1).div(factor));
|
||||
@@ -831,20 +773,6 @@ export class Pattern {
|
||||
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'.
|
||||
* It turns a pattern of samples into a pattern of parts of samples.
|
||||
* @name chop
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes")
|
||||
* .chop(4)
|
||||
* .rev() // reverse order of chops
|
||||
* .loopAt(4,1) // fit sample into 4 cycles
|
||||
*
|
||||
*/
|
||||
_chop(n) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
@@ -866,32 +794,14 @@ export class Pattern {
|
||||
return this._fast(cpm / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator
|
||||
*
|
||||
* @name early
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} cycles number of cycles to nudge left
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "bd ~".stack("hh ~".early(.1)).s()
|
||||
*/
|
||||
_early(offset) {
|
||||
// Equivalent of Tidal's <~ operator
|
||||
offset = Fraction(offset);
|
||||
return this.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
|
||||
*
|
||||
* @name late
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} cycles number of cycles to nudge right
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "bd ~".stack("hh ~".late(.1)).s()
|
||||
*/
|
||||
_late(offset) {
|
||||
// Equivalent of Tidal's ~> operator
|
||||
offset = Fraction(offset);
|
||||
return this._early(Fraction(0).sub(offset));
|
||||
}
|
||||
@@ -918,17 +828,6 @@ export class Pattern {
|
||||
return this._zoom(0, t)._slow(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given structure to the pattern:
|
||||
*
|
||||
* @name struct
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3,eb3,g3")
|
||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||
* .slow(4)
|
||||
*/
|
||||
// struct(...binary_pats) {
|
||||
// // Re structure the pattern according to a binary pattern (false values are dropped)
|
||||
// const binary_pat = sequence(binary_pats);
|
||||
@@ -976,16 +875,6 @@ export class Pattern {
|
||||
return this.invert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function whenever the given pattern is in a true state.
|
||||
* @name when
|
||||
* @memberof Pattern
|
||||
* @param {Pattern} binary_pat
|
||||
* @param {function} func
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
|
||||
*/
|
||||
when(binary_pat, func) {
|
||||
//binary_pat = sequence(binary_pat)
|
||||
const true_pat = binary_pat._filterValues(id);
|
||||
@@ -995,47 +884,10 @@ export class Pattern {
|
||||
return stack(with_pat, without_pat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimposes the function result on top of the original pattern, delayed by the given time.
|
||||
* @name off
|
||||
* @memberof Pattern
|
||||
* @param {Pattern | number} time offset time
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
|
||||
*/
|
||||
off(time_pat, func) {
|
||||
return stack(this, func(this.late(time_pat)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function every n cycles.
|
||||
* @name every
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
every(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
// pats.unshift(func(pat));
|
||||
pats.push(func(pat));
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
/**
|
||||
* Applies the given function every n cycles, starting from the first cycle.
|
||||
* @name every
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
every(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
@@ -1043,23 +895,6 @@ export class Pattern {
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function every n cycles, starting from the last cycle.
|
||||
* @name each
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
each(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
pats.push(func(pat));
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new pattern where every other cycle is played once, twice as
|
||||
* fast, and offset in time by one quarter of a cycle. Creates a kind of
|
||||
@@ -1070,15 +905,6 @@ export class Pattern {
|
||||
return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse all haps in a pattern
|
||||
*
|
||||
* @name rev
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").rev()
|
||||
*/
|
||||
rev() {
|
||||
const pat = this;
|
||||
const query = function (state) {
|
||||
@@ -1121,15 +947,6 @@ export class Pattern {
|
||||
return this.juxBy(1, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stacks the given pattern(s) to the current pattern.
|
||||
* @name stack
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").stack(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
}
|
||||
@@ -1138,28 +955,11 @@ export class Pattern {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the current pattern. Synonyms: .sequence .fastcat
|
||||
* @name seq
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").seq(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
// shorthand for sequence
|
||||
seq(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the next cycle. Synonym: .slowcat
|
||||
* @name cat
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").cat(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
cat(...pats) {
|
||||
return cat(this, ...pats);
|
||||
}
|
||||
@@ -1172,16 +972,6 @@ export class Pattern {
|
||||
return slowcat(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimposes the result of the given function(s) on top of the original pattern:
|
||||
* @name superimpose
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* .superimpose(x=>x.add(2))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
superimpose(...funcs) {
|
||||
return this.stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
@@ -1194,70 +984,24 @@ export class Pattern {
|
||||
return this.stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimpose and offset multiple times, applying the given function each time.
|
||||
* @name echoWith
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @param {number} times how many times to repeat
|
||||
* @param {number} time cycle offset between iterations
|
||||
* @param {function} func function to apply, given the pattern and the iteration index
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||
* .scale('C minor').note().legato(.2)
|
||||
*/
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimpose and offset multiple times, gradually decreasing the velocity
|
||||
* @name echo
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @param {number} times how many times to repeat
|
||||
* @param {number} time cycle offset between iterations
|
||||
* @param {number} feedback velocity multiplicator for each iteration
|
||||
* @example
|
||||
* s("bd sd").echo(3, 1/6, .8)
|
||||
*/
|
||||
_echo(times, time, feedback) {
|
||||
return this._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("0 1 2 3".scale('A minor')).iter(4)
|
||||
*/
|
||||
iter(times, back = false) {
|
||||
return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
|
||||
* @name iterBack
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("0 1 2 3".scale('A minor')).iterBack(4)
|
||||
*/
|
||||
// known as iter' in tidalcycles
|
||||
iterBack(times) {
|
||||
return this.iter(times, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||
* @name chunk
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
|
||||
*/
|
||||
_chunk(n, func, back = false) {
|
||||
const binary = Array(n - 1).fill(false);
|
||||
binary.unshift(true);
|
||||
@@ -1265,18 +1009,17 @@ export class Pattern {
|
||||
return this.when(binary_pat, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
|
||||
* @name chunkBack
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
|
||||
*/
|
||||
_chunkBack(n, func) {
|
||||
return this._chunk(n, func, true);
|
||||
}
|
||||
|
||||
edit(...funcs) {
|
||||
return stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
pipe(func) {
|
||||
return func(this);
|
||||
}
|
||||
|
||||
_bypass(on) {
|
||||
on = Boolean(parseInt(on));
|
||||
return on ? silence : this;
|
||||
@@ -1287,69 +1030,25 @@ export class Pattern {
|
||||
}
|
||||
|
||||
// sets absolute duration of haps
|
||||
/* _duration(value) {
|
||||
_duration(value) {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Multiplies the hap duration with the given factor.
|
||||
* @name legato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* note("c3 eb3 g3 c4").legato("<.25 .5 1 2>")
|
||||
*/
|
||||
// sets hap relative duration of haps
|
||||
_legato(value) {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||
* @name velocity
|
||||
* @example
|
||||
* s("hh*8")
|
||||
* .gain(".4!2 1 .4!2 1 .4 1")
|
||||
* .velocity(".4 1")
|
||||
*/
|
||||
_velocity(velocity) {
|
||||
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Makes the sample fit the given number of cycles by changing the speed.
|
||||
* @name loopAt
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes").loopAt(4,1)
|
||||
*/
|
||||
// move this to controls? (speed and unit are controls)
|
||||
_loopAt(factor, cps = 1) {
|
||||
return this.speed((1 / factor) * cps)
|
||||
.unit('c')
|
||||
.slow(factor);
|
||||
}
|
||||
onTrigger(onTrigger) {
|
||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
||||
}
|
||||
log(func = id) {
|
||||
return this._withHap((hap) =>
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (hap.context.onTrigger) {
|
||||
hap.context.onTrigger(...args);
|
||||
}
|
||||
console.log(func(...args));
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
logValues(func = id) {
|
||||
return this.log((_, hap) => func(hap.value));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - adopt value.mjs fully..
|
||||
@@ -1381,48 +1080,9 @@ function _composeOp(a, b, func) {
|
||||
keepif: [(a, b) => (b ? a : undefined)],
|
||||
|
||||
// numerical functions
|
||||
/**
|
||||
*
|
||||
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
|
||||
* @name add
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
||||
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
|
||||
* // Without add, the equivalent would be:
|
||||
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
|
||||
* @example
|
||||
* // You can also use add with notes:
|
||||
* "c3 e3 g3".add("<0 5 7 0>").note()
|
||||
* // Behind the scenes, the notes are converted to midi numbers:
|
||||
* // "48 52 55".add("<0 5 7 0>").note()
|
||||
*/
|
||||
add: [(a, b) => a + b, numOrString], // support string concatenation
|
||||
/**
|
||||
*
|
||||
* Like add, but the given numbers are subtracted.
|
||||
* @name sub
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
|
||||
* // See add for more information.
|
||||
*/
|
||||
sub: [(a, b) => a - b, num],
|
||||
/**
|
||||
*
|
||||
* Multiplies each number by the given factor.
|
||||
* @name mul
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
|
||||
*/
|
||||
mul: [(a, b) => a * b, num],
|
||||
/**
|
||||
*
|
||||
* Divides each number by the given factor.
|
||||
* @name div
|
||||
* @memberof Pattern
|
||||
*/
|
||||
div: [(a, b) => a / b, num],
|
||||
mod: [mod, num],
|
||||
pow: [Math.pow, num],
|
||||
@@ -1458,14 +1118,10 @@ function _composeOp(a, b, func) {
|
||||
pat = preprocess(pat);
|
||||
other = preprocess(other);
|
||||
}
|
||||
var result;
|
||||
var result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
|
||||
// hack to remove undefs when doing 'keepif'
|
||||
if (what === 'keepif') {
|
||||
// avoid union, as we want to throw away the value of 'b' completely
|
||||
result = pat['_op' + how](other, (a) => (b) => op(a, b));
|
||||
result = result._removeUndefineds();
|
||||
} else {
|
||||
result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1502,21 +1158,19 @@ Pattern.prototype.patternified = [
|
||||
'chop',
|
||||
'color',
|
||||
'cpm',
|
||||
'duration',
|
||||
'early',
|
||||
'fast',
|
||||
'jux',
|
||||
'late',
|
||||
'legato',
|
||||
'linger',
|
||||
'ply',
|
||||
'segment',
|
||||
'striate',
|
||||
'slow',
|
||||
'velocity',
|
||||
];
|
||||
|
||||
// aliases
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
Pattern.prototype.factories = {
|
||||
pure,
|
||||
@@ -1539,11 +1193,12 @@ Pattern.prototype.factories = {
|
||||
// Nothing
|
||||
export const silence = new Pattern((_) => []);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/** A discrete value that repeats once per cycle:
|
||||
*
|
||||
* @param {any} value - The value to repeat
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* pure('e4') // "e4"
|
||||
* pure('e4')
|
||||
*/
|
||||
export function pure(value) {
|
||||
function query(state) {
|
||||
@@ -1554,15 +1209,7 @@ export function pure(value) {
|
||||
|
||||
export function isPattern(thing) {
|
||||
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
||||
const is = thing instanceof Pattern || thing._Pattern;
|
||||
if (!thing instanceof Pattern) {
|
||||
console.warn(
|
||||
`Found Pattern that fails "instanceof Pattern" check.
|
||||
This may happen if you are using multiple versions of @strudel.cycles/core.
|
||||
Please check by running "npm ls @strudel.cycles/core".`,
|
||||
);
|
||||
}
|
||||
return is;
|
||||
return thing instanceof Pattern;
|
||||
}
|
||||
|
||||
export function reify(thing) {
|
||||
@@ -1573,11 +1220,12 @@ export function reify(thing) {
|
||||
return pure(thing);
|
||||
}
|
||||
|
||||
/** 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:
|
||||
*
|
||||
* @param {...any} items - The items to stack
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
|
||||
* stack(g3, b3, [e4, d4])
|
||||
*/
|
||||
export function stack(...pats) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
@@ -1590,6 +1238,7 @@ export function stack(...pats) {
|
||||
*
|
||||
* synonyms: {@link cat}
|
||||
*
|
||||
* @param {...any} items - The items to concatenate
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* slowcat(e5, b4, [d5, c5])
|
||||
@@ -1624,8 +1273,8 @@ export function slowcatPrime(...pats) {
|
||||
pats = pats.map(reify);
|
||||
const query = function (state) {
|
||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
||||
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
|
||||
return pat?.query(state) || [];
|
||||
const pat = pats[pat_n];
|
||||
return pat.query(state);
|
||||
};
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
@@ -1645,22 +1294,16 @@ export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
/** The given items are con**cat**enated, where each one takes one cycle. Synonym: slowcat
|
||||
*
|
||||
* @param {...any} items - The items to concatenate
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
|
||||
*
|
||||
*/
|
||||
/** See {@link slowcat} */
|
||||
export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
}
|
||||
|
||||
/** Like {@link seq}, but each step has a length, relative to the whole.
|
||||
/** Like {@link fastcat}, but where each step has a temporal weight:
|
||||
* @param {...Array} items - The items to concatenate
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
|
||||
* timeCat([3,e3],[1, g3])
|
||||
*/
|
||||
export function timeCat(...timepats) {
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
@@ -1679,11 +1322,7 @@ export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
/** Like **cat**, but the items are crammed into one cycle. Synonyms: fastcat, sequence
|
||||
* @example
|
||||
* seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
|
||||
*
|
||||
*/
|
||||
/** See {@link fastcat} */
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
@@ -1732,8 +1371,21 @@ export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export function polyrhythm(...xs) {
|
||||
const seqs = xs.map((a) => sequence(a));
|
||||
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return stack(...seqs);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pr(args) {
|
||||
polyrhythm(args);
|
||||
}
|
||||
|
||||
export const add = curry((a, pat) => pat.add(a));
|
||||
export const chop = curry((a, pat) => pat.chop(a));
|
||||
export const chunk = curry((a, pat) => pat.chunk(a));
|
||||
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
export const div = curry((a, pat) => pat.div(a));
|
||||
@@ -1754,7 +1406,6 @@ export const mul = curry((a, pat) => pat.mul(a));
|
||||
export const off = curry((t, f, pat) => pat.off(t, f));
|
||||
export const ply = curry((a, pat) => pat.ply(a));
|
||||
export const range = curry((a, b, pat) => pat.range(a, b));
|
||||
export const rangex = curry((a, b, pat) => pat.rangex(a, b));
|
||||
export const range2 = curry((a, b, pat) => pat.range2(a, b));
|
||||
export const rev = (pat) => pat.rev();
|
||||
export const slow = curry((a, pat) => pat.slow(a));
|
||||
@@ -1841,18 +1492,6 @@ Pattern.prototype.inside = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._inside)(...args, this);
|
||||
};
|
||||
Pattern.prototype.range = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._range)(...args, this);
|
||||
};
|
||||
Pattern.prototype.rangex = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._rangex)(...args, this);
|
||||
};
|
||||
Pattern.prototype.range2 = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._range2)(...args, this);
|
||||
};
|
||||
|
||||
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
|
||||
Pattern.prototype.bootstrap = function () {
|
||||
|
||||
+14
-258
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, isPattern } from './pattern.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id } from './util.mjs';
|
||||
|
||||
@@ -27,9 +27,9 @@ export const isaw2 = isaw._toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4))
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4)
|
||||
*
|
||||
*/
|
||||
export const saw = signal((t) => t % 1);
|
||||
@@ -42,28 +42,30 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor')
|
||||
*
|
||||
*/
|
||||
export const sine = sine2._fromBipolar();
|
||||
|
||||
|
||||
/**
|
||||
* A cosine signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor')
|
||||
*
|
||||
*/
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
|
||||
/**
|
||||
* A square signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* square.segment(2).range(0,7).scale('C minor').note()
|
||||
* square.segment(2).range(0,7).scale('C minor')
|
||||
*
|
||||
*/
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
@@ -74,7 +76,7 @@ export const square2 = square._toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* tri.segment(8).range(0,7).scale('C minor').note()
|
||||
* triangle.segment(2).range(0,7).scale('C minor')
|
||||
*
|
||||
*/
|
||||
export const tri = fastcat(isaw, saw);
|
||||
@@ -110,112 +112,28 @@ const timeToRandsPrime = (seed, n) => {
|
||||
|
||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
* @name rand
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const rand = signal(timeToRand);
|
||||
/**
|
||||
* A continuous pattern of random numbers, between -1 and 1
|
||||
*/
|
||||
export const rand2 = rand._toBipolar();
|
||||
|
||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||
export const brand = _brandBy(0.5);
|
||||
|
||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
|
||||
/**
|
||||
* A continuous pattern of random integers, between 0 and n-1.
|
||||
*
|
||||
* @name irand
|
||||
* @param {number} n max value (exclusive)
|
||||
* @example
|
||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||
* irand(8).struct("x(3,8)").scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
export const __chooseWith = (pat, xs) => {
|
||||
export const chooseWith = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
||||
};
|
||||
/**
|
||||
* Choose from the list of values (or patterns of values) using the given
|
||||
* pattern of numbers, which should be in the range of 0..1
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const chooseWith = (pat, xs) => {
|
||||
return __chooseWith(pat, xs).outerJoin();
|
||||
return pat
|
||||
.range(0, xs.length)
|
||||
.fmap((i) => xs[Math.floor(i)])
|
||||
.outerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
||||
* than the pattern you're using to choose with.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const chooseInWith = (pat, xs) => {
|
||||
return __chooseWith(pat, xs).innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
* Chooses randomly from the given list of elements.
|
||||
* @param {...any} xs values / patterns to choose from.
|
||||
* @returns {Pattern} - a continuous pattern.
|
||||
*/
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
|
||||
/**
|
||||
* Chooses from the given list of values (or patterns of values), according
|
||||
* to the pattern that the method is called on. The pattern should be in
|
||||
* the range 0 .. 1.
|
||||
* @param {...any} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
Pattern.prototype.choose = function (...xs) {
|
||||
return chooseWith(this, xs);
|
||||
};
|
||||
|
||||
/**
|
||||
* As with choose, but the pattern that this method is called on should be
|
||||
* in the range -1 .. 1
|
||||
* @param {...any} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
Pattern.prototype.choose2 = function (...xs) {
|
||||
return chooseWith(this._fromBipolar(), xs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Picks one of the elements at random each cycle.
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
||||
* @example
|
||||
* "bd | hh | sd".s().fast(4)
|
||||
*/
|
||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||
|
||||
export const randcat = chooseCycles;
|
||||
|
||||
const _wchooseWith = function (pat, ...pairs) {
|
||||
const values = pairs.map((pair) => reify(pair[0]));
|
||||
const weights = [];
|
||||
@@ -246,68 +164,20 @@ export const perlinWith = (pat) => {
|
||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||
*
|
||||
* @name perlin
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time);
|
||||
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
};
|
||||
|
||||
/**
|
||||
* Randomly removes events from the pattern by a given amount.
|
||||
* 0 = 0% chance of removal
|
||||
* 1 = 100% chance of removal
|
||||
*
|
||||
* @name degradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").degradeBy(0.2)
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
Pattern.prototype._degradeBy = function (x) {
|
||||
return this._degradeByWith(rand, x);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||
*
|
||||
* @name degrade
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").degrade()
|
||||
* @example
|
||||
* s("[hh?]*8")
|
||||
*/
|
||||
Pattern.prototype.degrade = function () {
|
||||
return this._degradeBy(0.5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||
* 0 = 100% chance of removal
|
||||
* 1 = 0% chance of removal
|
||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||
*
|
||||
* @name undegradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").undegradeBy(0.2)
|
||||
*/
|
||||
Pattern.prototype._undegradeBy = function (x) {
|
||||
return this._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
@@ -323,25 +193,6 @@ Pattern.prototype._sometimesBy = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
||||
};
|
||||
|
||||
// https://github.com/tidalcycles/strudel/discussions/198
|
||||
/* Pattern.prototype._sometimesBy = function (x, other) {
|
||||
other = typeof other === 'function' ? other(this._undegradeBy(1 - x)) : reify(other)._undegradeBy(1 - x);
|
||||
return stack(this._degradeBy(x), other);
|
||||
}; */
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability.
|
||||
* Similar to {@link Pattern#someCyclesBy}
|
||||
*
|
||||
* @name sometimesBy
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
@@ -349,7 +200,6 @@ Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
// why does this exist? it is identical to sometimesBy
|
||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
||||
};
|
||||
@@ -361,17 +211,6 @@ Pattern.prototype.sometimesByPre = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Applies the given function with a 50% chance
|
||||
*
|
||||
* @name sometimes
|
||||
* @memberof Pattern
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.sometimes = function (func) {
|
||||
return this._sometimesBy(0.5, func);
|
||||
};
|
||||
@@ -387,19 +226,6 @@ Pattern.prototype._someCyclesBy = function (x, func) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||
* Similar to {@link Pattern#sometimesBy}
|
||||
*
|
||||
* @name someCyclesBy
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
@@ -407,100 +233,30 @@ Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.someCyclesBy(0.5, fn)`
|
||||
*
|
||||
* @name someCycles
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.someCycles = function (func) {
|
||||
return this._someCyclesBy(0.5, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.75, fn)`
|
||||
*
|
||||
* @name often
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").often(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.often = function (func) {
|
||||
return this.sometimesBy(0.75, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.25, fn)`
|
||||
*
|
||||
* @name rarely
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.rarely = function (func) {
|
||||
return this.sometimesBy(0.25, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||
*
|
||||
* @name almostNever
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.almostNever = function (func) {
|
||||
return this.sometimesBy(0.1, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||
*
|
||||
* @name almostAlways
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.almostAlways = function (func) {
|
||||
return this.sometimesBy(0.9, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||
*
|
||||
* @name never
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").never(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.never = function (func) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||
*
|
||||
* @name always
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").always(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.always = function (func) {
|
||||
return func(this);
|
||||
};
|
||||
|
||||
@@ -5,42 +5,52 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { strict as assert } from 'assert';
|
||||
import drawLine from '../drawLine.mjs';
|
||||
|
||||
describe('drawLine', () => {
|
||||
it('supports equal lengths', () => {
|
||||
expect(drawLine(fastcat(0), 4)).toEqual('|0|0');
|
||||
expect(drawLine(fastcat(0, 1), 4)).toEqual('|01|01');
|
||||
expect(drawLine(fastcat(0, 1, 2), 6)).toEqual('|012|012');
|
||||
assert.equal(drawLine(fastcat(0), 4), '|0|0');
|
||||
assert.equal(drawLine(fastcat(0, 1), 4), '|01|01');
|
||||
assert.equal(drawLine(fastcat(0, 1, 2), 6), '|012|012');
|
||||
});
|
||||
it('supports unequal lengths', () => {
|
||||
expect(drawLine(fastcat(0, [1, 2]), 10)).toEqual('|0-12|0-12');
|
||||
expect(drawLine(fastcat(0, [1, 2, 3]), 10)).toEqual('|0--123|0--123');
|
||||
expect(drawLine(fastcat(0, 1, [2, 3]), 10)).toEqual('|0-1-23|0-1-23');
|
||||
assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12');
|
||||
assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123');
|
||||
assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23');
|
||||
});
|
||||
it('supports unequal silence', () => {
|
||||
expect(drawLine(fastcat(0, silence, [1, 2]), 10)).toEqual('|0-..12|0-..12');
|
||||
assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12');
|
||||
});
|
||||
it('supports polyrhythms', () => {
|
||||
// assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
||||
expect(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10)).toEqual('|0--0--1-1-1-');
|
||||
'0*2 1*3';
|
||||
assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
||||
});
|
||||
it('supports multiple lines', () => {
|
||||
expect(drawLine(fastcat(0, stack(1, 2)), 10)).toEqual(`|01|01|01|01
|
||||
|.2|.2|.2|.2`);
|
||||
|
||||
expect(drawLine(fastcat(0, 1, stack(2, 3)), 10)).toEqual(`|012|012|012
|
||||
|..3|..3|..3`);
|
||||
|
||||
expect(drawLine(fastcat(0, stack(1, 2, 3)), 10)).toEqual(`|01|01|01|01
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, stack(1, 2)), 10),
|
||||
`|01|01|01|01
|
||||
|.2|.2|.2|.2`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, 1, stack(2, 3)), 10),
|
||||
`|012|012|012
|
||||
|..3|..3|..3`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, stack(1, 2, 3)), 10),
|
||||
`|01|01|01|01
|
||||
|.2|.2|.2|.2
|
||||
|.3|.3|.3|.3`);
|
||||
expect(drawLine(fastcat(0, 1, stack(2, 3, 4)), 10)).toEqual(`|012|012|012
|
||||
|.3|.3|.3|.3`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, 1, stack(2, 3, 4)), 10),
|
||||
`|012|012|012
|
||||
|..3|..3|..3
|
||||
|..4|..4|..4`);
|
||||
|..4|..4|..4`,
|
||||
);
|
||||
});
|
||||
it('supports unequal cycle lengths', () => {
|
||||
expect(drawLine(slowcat(0, [1, 2]), 10)).toEqual(`|0|12|0|12`);
|
||||
assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import Fraction, { gcd } from '../fraction.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { strict as assert } from 'assert';
|
||||
|
||||
describe('gcd', () => {
|
||||
it('should work', () => {
|
||||
const F = Fraction._original;
|
||||
expect(gcd(F(1 / 6), F(1 / 4)).toFraction()).toEqual('1/12');
|
||||
assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12');
|
||||
});
|
||||
});
|
||||
|
||||
+290
-266
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { deepStrictEqual, strict as assert } from 'assert';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
@@ -41,12 +41,15 @@ import {
|
||||
tri2,
|
||||
id,
|
||||
ply,
|
||||
rev,
|
||||
time,
|
||||
rev
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
//import { Time } from 'tone';
|
||||
import pkg from 'tone';
|
||||
const { Time } = pkg;
|
||||
|
||||
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);
|
||||
@@ -55,35 +58,35 @@ const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
|
||||
return assert.deepStrictEqual(a._sortHapsByPart().firstCycle(), b._sortHapsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('TimeSpan', () => {
|
||||
describe('equals()', () => {
|
||||
it('Should be equal to the same value', () => {
|
||||
expect(new TimeSpan(0, 4).equals(new TimeSpan(0, 4))).toBe(true);
|
||||
describe('TimeSpan', function () {
|
||||
describe('equals()', function () {
|
||||
it('Should be equal to the same value', function () {
|
||||
assert.equal(new TimeSpan(0, 4).equals(new TimeSpan(0, 4)), true);
|
||||
});
|
||||
});
|
||||
describe('splitCycles', () => {
|
||||
it('Should split two cycles into two', () => {
|
||||
expect(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length).toBe(2);
|
||||
describe('splitCycles', function () {
|
||||
it('Should split two cycles into two', function () {
|
||||
assert.equal(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length, 2);
|
||||
});
|
||||
});
|
||||
describe('intersection_e', () => {
|
||||
describe('intersection_e', function () {
|
||||
var a = new TimeSpan(Fraction(0), Fraction(2));
|
||||
var b = new TimeSpan(Fraction(1), Fraction(3));
|
||||
var c = new TimeSpan(Fraction(1), Fraction(2));
|
||||
var d = new TimeSpan(Fraction(1), Fraction(2));
|
||||
it('Should create an intersection', () => {
|
||||
expect(a.intersection_e(b).equals(c)).toBe(true);
|
||||
it('Should create an intersection', function () {
|
||||
assert.equal(a.intersection_e(b).equals(c), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hap', () => {
|
||||
describe('hasOnset()', () => {
|
||||
it('True if part includes onset from whole', () => {
|
||||
expect(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset()).toBe(true);
|
||||
describe('Hap', function () {
|
||||
describe('hasOnset()', function () {
|
||||
it('True if part includes onset from whole', function () {
|
||||
assert.equal(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset(), true);
|
||||
});
|
||||
});
|
||||
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
|
||||
@@ -91,19 +94,19 @@ describe('Hap', () => {
|
||||
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
|
||||
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
|
||||
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
|
||||
describe('spanEquals', () => {
|
||||
it('True if two haps have the same whole and part', () => {
|
||||
expect(a.spanEquals(b)).toBe(true);
|
||||
describe('spanEquals', function () {
|
||||
it('True if two haps have the same whole and part', function () {
|
||||
assert.equal(a.spanEquals(b), true);
|
||||
});
|
||||
it("False if two haps don't the same whole and part", () => {
|
||||
expect(a.spanEquals(c)).toBe(false);
|
||||
it("False if two haps don't the same whole and part", function () {
|
||||
assert.equal(a.spanEquals(c), false);
|
||||
});
|
||||
it('True if two haps have the same part and undefined wholes', () => {
|
||||
expect(d.spanEquals(e)).toBe(true);
|
||||
it('True if two haps have the same part and undefined wholes', function () {
|
||||
assert.equal(d.spanEquals(e), true);
|
||||
});
|
||||
});
|
||||
describe('resolveState()', () => {
|
||||
it('Can increment some state', () => {
|
||||
it('Can increment some state', function () {
|
||||
const stateful_value = (state) => {
|
||||
const newValue = state['incrementme'];
|
||||
// TODO Does the state *need* duplicating here?
|
||||
@@ -115,8 +118,8 @@ describe('Hap', () => {
|
||||
const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
|
||||
const [state2, ev2] = ev1.resolveState(state);
|
||||
const [state3, ev3] = ev1.resolveState(state2);
|
||||
expect(ev3).toStrictEqual(new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
|
||||
expect(state3).toStrictEqual({ incrementme: 12 });
|
||||
assert.deepStrictEqual(ev3, new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
|
||||
assert.deepStrictEqual(state3, { incrementme: 12 });
|
||||
});
|
||||
});
|
||||
describe('wholeOrPart()', () => {
|
||||
@@ -125,40 +128,42 @@ describe('Hap', () => {
|
||||
const continuousHap = new Hap(undefined, ts1, 'hello');
|
||||
const discreteHap = new Hap(ts1, ts0_5, 'hello');
|
||||
it('Can pick a whole', () => {
|
||||
expect(discreteHap.wholeOrPart()).toStrictEqual(ts1);
|
||||
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
|
||||
});
|
||||
it('Can pick a part', () => {
|
||||
expect(continuousHap.wholeOrPart()).toStrictEqual(ts1);
|
||||
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Pattern', () => {
|
||||
describe('pure', () => {
|
||||
it('Can make a pattern', () => {
|
||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||
|
||||
describe('Pattern', function () {
|
||||
describe('pure', function () {
|
||||
it('Can make a pattern', function () {
|
||||
assert.equal(pure('hello').query(st(0.5, 2.5)).length, 3);
|
||||
});
|
||||
});
|
||||
describe('fmap()', () => {
|
||||
it('Can add things', () => {
|
||||
expect(
|
||||
describe('fmap()', function () {
|
||||
it('Can add things', function () {
|
||||
assert.equal(
|
||||
pure(3)
|
||||
.fmap((x) => x + 4)
|
||||
.firstCycle()[0].value,
|
||||
).toBe(7);
|
||||
7,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('add()', () => {
|
||||
it('can structure In()', () => {
|
||||
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||
expect(pure(3).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||
describe('add()', function () {
|
||||
it('can structure In()', function () {
|
||||
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
|
||||
assert.equal(pure(3).addIn(pure(4)).query(st(0, 1))[0].value, 7);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||
assert.deepStrictEqual(sequence(1, 2).addMix(silence, 5, silence).firstCycle(), [
|
||||
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
||||
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -192,18 +197,18 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('keep()', () => {
|
||||
it('can structure In()', () => {
|
||||
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||
describe('keep()', function () {
|
||||
it('can structure In()', function () {
|
||||
assert.equal(pure(3).keep(pure(4)).query(st(0, 1))[0].value, 3);
|
||||
assert.equal(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value, 3);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
assert.deepStrictEqual(sequence(1, 2).keepMix(silence, 5, silence).firstCycle(), [
|
||||
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -234,8 +239,8 @@ describe('Pattern', () => {
|
||||
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||
});
|
||||
});
|
||||
describe('keepif()', () => {
|
||||
it('can structure In()', () => {
|
||||
describe('keepif()', function () {
|
||||
it('can structure In()', function () {
|
||||
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
||||
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
|
||||
});
|
||||
@@ -243,9 +248,9 @@ describe('Pattern', () => {
|
||||
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
assert.deepStrictEqual(sequence(1, 2).keepifMix(false, true, false).firstCycle(), [
|
||||
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -276,39 +281,35 @@ describe('Pattern', () => {
|
||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
});
|
||||
});
|
||||
describe('sub()', () => {
|
||||
it('Can subtract things', () => {
|
||||
expect(pure(3).sub(pure(4)).query(st(0, 1))[0].value).toBe(-1);
|
||||
describe('sub()', function () {
|
||||
it('Can subtract things', function () {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
|
||||
});
|
||||
});
|
||||
describe('mul()', () => {
|
||||
it('Can multiply things', () => {
|
||||
expect(pure(3).mul(pure(2)).firstCycle()[0].value).toBe(6);
|
||||
describe('mul()', function () {
|
||||
it('Can multiply things', function () {
|
||||
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6);
|
||||
});
|
||||
});
|
||||
describe('div()', () => {
|
||||
it('Can divide things', () => {
|
||||
expect(pure(3).div(pure(2)).firstCycle()[0].value).toBe(1.5);
|
||||
describe('div()', function () {
|
||||
it('Can divide things', function () {
|
||||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
|
||||
});
|
||||
});
|
||||
describe('set()', () => {
|
||||
it('Can set things in objects', () => {
|
||||
expect(
|
||||
describe('set()', function () {
|
||||
it('Can set things in objects', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 4, b: 6 })
|
||||
.set(pure({ c: 7 }))
|
||||
.firstCycle()[0].value,
|
||||
).toStrictEqual({
|
||||
a: 4,
|
||||
b: 6,
|
||||
c: 7,
|
||||
});
|
||||
|
||||
{ a: 4, b: 6, c: 7 },
|
||||
);
|
||||
sameFirst(
|
||||
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
|
||||
sequence({ a: 4, b: 2, c: 5 }).fast(3),
|
||||
);
|
||||
});
|
||||
it('Can set things with plain values', () => {
|
||||
it('Can set things with plain values', function () {
|
||||
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
||||
});
|
||||
describe('setOut()', () => {
|
||||
@@ -341,89 +342,94 @@ describe('Pattern', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('stack()', () => {
|
||||
it('Can stack things', () => {
|
||||
expect(
|
||||
describe('stack()', function () {
|
||||
it('Can stack things', function () {
|
||||
assert.deepStrictEqual(
|
||||
stack(pure('a'), pure('b'), pure('c'))
|
||||
.firstCycle()
|
||||
.map((h) => h.value),
|
||||
).toStrictEqual(['a', 'b', 'c']);
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
});
|
||||
it('Can stack subpatterns', () => {
|
||||
it('Can stack subpatterns', function () {
|
||||
sameFirst(stack('a', ['b', 'c']), stack('a', sequence('b', 'c')));
|
||||
});
|
||||
});
|
||||
describe('_fast()', () => {
|
||||
it('Makes things faster', () => {
|
||||
expect(pure('a')._fast(2).firstCycle().length).toBe(2);
|
||||
describe('_fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure('a')._fast(2).firstCycle().length, 2);
|
||||
});
|
||||
});
|
||||
describe('_fastGap()', () => {
|
||||
it('Makes things faster, with a gap', () => {
|
||||
expect(sequence('a', 'b', 'c')._fastGap(2).firstCycle()).toStrictEqual(
|
||||
describe('_fastGap()', function () {
|
||||
it('Makes things faster, with a gap', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(2).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
expect(sequence('a', 'b', 'c')._fastGap(3).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(3).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Makes things faster, with a gap, when speeded up further', () => {
|
||||
expect(sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle()).toStrictEqual(
|
||||
it('Makes things faster, with a gap, when speeded up further', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('copes with breaking up events across cycles', () => {
|
||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual(
|
||||
[hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')]
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', () => {
|
||||
it('Can squash cycles of a pattern into a given timespan', () => {
|
||||
expect(pure('a')._compressSpan(ts(0.25, 0.5)).firstCycle()).toStrictEqual(
|
||||
describe('_compressSpan()', function () {
|
||||
it('Can squash cycles of a pattern into a given timespan', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
|
||||
sequence(silence, 'a', silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fast()', () => {
|
||||
it('Makes things faster', () => {
|
||||
expect(pure('a').fast(2).firstCycle().length).toBe(2);
|
||||
describe('fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure('a').fast(2).firstCycle().length, 2);
|
||||
});
|
||||
it('Makes things faster, with a pattern of factors', () => {
|
||||
expect(pure('a').fast(sequence(1, 4)).firstCycle().length).toBe(3);
|
||||
expect(pure('a').fast(sequence(1, 4)).firstCycle()).toStrictEqual(
|
||||
it('Makes things faster, with a pattern of factors', function () {
|
||||
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
|
||||
// .fast(sequence(1,silence) is a quick hack to cut a hap in two..
|
||||
assert.deepStrictEqual(
|
||||
pure('a').fast(sequence(1, 4)).firstCycle(),
|
||||
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('defaults to accepting sequences', () => {
|
||||
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
||||
it('defaults to accepting sequences', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle(),
|
||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a static function', () => {
|
||||
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
|
||||
it('works as a static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a curried static function', () => {
|
||||
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
|
||||
it('works as a curried static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_slow()', () => {
|
||||
it('Makes things slower', () => {
|
||||
expect(pure('a')._slow(2).firstCycle()[0]).toStrictEqual(hap(ts(0, 2), ts(0, 1), 'a'));
|
||||
describe('_slow()', function () {
|
||||
it('Makes things slower', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')._slow(2).firstCycle()[0],
|
||||
new Hap(new TimeSpan(Fraction(0), Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), 'a'),
|
||||
);
|
||||
|
||||
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
|
||||
|
||||
expect(pat.query(st(0, 1))[1]).toStrictEqual(hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
|
||||
|
||||
assert.deepStrictEqual(pat.query(st(0, 1))[1], hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
|
||||
// the following test fails
|
||||
// assert.deepStrictEqual(
|
||||
// pat.query(ts(1,2))[1], undefined
|
||||
// )
|
||||
/* assert.deepStrictEqual(
|
||||
pat.query(ts(1,2))[1], undefined
|
||||
) */
|
||||
// expecting [c3 eb3] [c3 ~]
|
||||
// what happens [c3 eb3] [c3 eb3]
|
||||
// notable examples:
|
||||
@@ -433,125 +439,134 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('inside', () => {
|
||||
it('can rev inside a cycle', () => {
|
||||
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev), sequence('b', 'a', 'd', 'c'));
|
||||
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev),
|
||||
sequence('b', 'a', 'd', 'c')
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('outside', () => {
|
||||
it('can rev outside a cycle', () => {
|
||||
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev), sequence('d', 'c'));
|
||||
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev),
|
||||
sequence('d', 'c')
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_filterValues()', () => {
|
||||
it('Filters true', () => {
|
||||
expect(
|
||||
describe('_filterValues()', function () {
|
||||
it('Filters true', function () {
|
||||
assert.equal(
|
||||
pure(true)
|
||||
._filterValues((x) => x)
|
||||
.firstCycle().length,
|
||||
).toBe(1);
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when()', () => {
|
||||
it('Always faster', () => {
|
||||
expect(
|
||||
describe('when()', function () {
|
||||
it('Always faster', function () {
|
||||
assert.equal(
|
||||
pure('a')
|
||||
.when(pure(true), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
).toBe(2);
|
||||
2,
|
||||
);
|
||||
});
|
||||
it('Never faster', () => {
|
||||
expect(
|
||||
it('Never faster', function () {
|
||||
assert.equal(
|
||||
pure('a')
|
||||
.when(pure(false), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
).toBe(1);
|
||||
1,
|
||||
);
|
||||
});
|
||||
it('Can alternate', () => {
|
||||
expect(
|
||||
pure(10)
|
||||
.when(slowcat(true, false), (x) => x.add(3))
|
||||
.fast(4)
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
||||
it('Can alternate', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortHapsByPart().firstCycle(),
|
||||
fastcat(13, 10, 13, 10).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', () => {
|
||||
it('Can concatenate two things', () => {
|
||||
expect(
|
||||
describe('fastcat()', function () {
|
||||
it('Can concatenate two things', function () {
|
||||
assert.deepStrictEqual(
|
||||
fastcat(pure('a'), pure('b'))
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual(['a', 'b']);
|
||||
['a', 'b'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', () => {
|
||||
it('Can go into negative time', () => {
|
||||
describe('fastcat()', function () {
|
||||
it('Can go into negative time', function () {
|
||||
sameFirst(fastcat('a', 'b', 'c').late(1000000), fastcat('a', 'b', 'c'));
|
||||
});
|
||||
});
|
||||
describe('slowcat()', () => {
|
||||
it('Can concatenate things slowly', () => {
|
||||
expect(
|
||||
describe('slowcat()', function () {
|
||||
it('Can concatenate things slowly', function () {
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', 'b')
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual(['a']);
|
||||
|
||||
expect(
|
||||
['a'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', 'b')
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual(['b']);
|
||||
|
||||
expect(
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual(['b']);
|
||||
|
||||
expect(
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(3)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual(['c']);
|
||||
['c'],
|
||||
);
|
||||
});
|
||||
it('Can cat subpatterns', () => {
|
||||
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
||||
});
|
||||
});
|
||||
describe('rev()', () => {
|
||||
it('Can reverse things', () => {
|
||||
expect(
|
||||
describe('rev()', function () {
|
||||
it('Can reverse things', function () {
|
||||
assert.deepStrictEqual(
|
||||
fastcat('a', 'b', 'c')
|
||||
.rev()
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.value),
|
||||
).toStrictEqual(['c', 'b', 'a']);
|
||||
['c', 'b', 'a'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sequence()', () => {
|
||||
it('Can work like fastcat', () => {
|
||||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
||||
assert.deepStrictEqual(sequence(1, 2, 3).firstCycle(), fastcat(1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('polyrhythm()', () => {
|
||||
it('Can layer up cycles', () => {
|
||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
polyrhythm(['a', 'b'], ['c']).firstCycle(),
|
||||
stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('polymeter()', () => {
|
||||
it('Can layer up cycles, stepwise', () => {
|
||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
polymeterSteps(3, ['d', 'e']).firstCycle(),
|
||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||
);
|
||||
|
||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle(),
|
||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -559,24 +574,29 @@ describe('Pattern', () => {
|
||||
|
||||
describe('every()', () => {
|
||||
it('Can apply a function every 3rd time', () => {
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
.every(3, (x) => x._fast(2))
|
||||
._fast(3)
|
||||
.firstCycle(),
|
||||
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
||||
});
|
||||
it('works with currying', () => {
|
||||
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||
});
|
||||
it('works with currying', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure('a').every(3, fast(2))._fast(3).firstCycle(),
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle(),
|
||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle(),
|
||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
|
||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -586,91 +606,98 @@ describe('Pattern', () => {
|
||||
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
|
||||
});
|
||||
});
|
||||
describe('timeCat()', () => {
|
||||
it('Can concatenate patterns with different relative durations', () => {
|
||||
expect(sequence('a', ['a', 'a']).firstCycle()).toStrictEqual(
|
||||
describe('timeCat()', function () {
|
||||
it('Can concatenate patterns with different relative durations', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('struct()', () => {
|
||||
it('Can restructure a discrete pattern', () => {
|
||||
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
describe('struct()', function () {
|
||||
it('Can restructure a discrete pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
|
||||
hap(ts(0, third), ts(0, third), 'a'),
|
||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true))
|
||||
.firstCycle(),
|
||||
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
|
||||
|
||||
expect(
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true).invert())
|
||||
.firstCycle(),
|
||||
).toStrictEqual(sequence(silence, [silence, 'a'], silence).firstCycle());
|
||||
|
||||
expect(
|
||||
sequence(silence, [silence, 'a'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, silence], true))
|
||||
.firstCycle(),
|
||||
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can structure a continuous pattern', () => {
|
||||
expect(steady('a').struct(true, [true, true]).firstCycle()).toStrictEqual(sequence('a', ['a', 'a']).firstCycle());
|
||||
assert.deepStrictEqual(
|
||||
steady('a').struct(true, [true, true]).firstCycle(),
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mask()', () => {
|
||||
it('Can fragment a pattern', () => {
|
||||
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
describe('mask()', function () {
|
||||
it('Can fragment a pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle(), [
|
||||
hap(ts(0, 0.5), ts(0, third), 'a'),
|
||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
});
|
||||
it('Can mask off parts of a pattern', () => {
|
||||
expect(sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle()).toStrictEqual(
|
||||
it('Can mask off parts of a pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle(),
|
||||
sequence(['a', 'b'], silence).firstCycle(),
|
||||
);
|
||||
|
||||
expect(sequence('a').mask(sequence(true, false)).firstCycle()).toStrictEqual([hap(ts(0, 1), ts(0, 0.5), 'a')]);
|
||||
assert.deepStrictEqual(sequence('a').mask(sequence(true, false)).firstCycle(), [hap(ts(0, 1), ts(0, 0.5), 'a')]);
|
||||
});
|
||||
});
|
||||
describe('invert()', () => {
|
||||
it('Can invert a binary pattern', () => {
|
||||
expect(sequence(true, false, [true, false]).invert().firstCycle()).toStrictEqual(
|
||||
describe('invert()', function () {
|
||||
it('Can invert a binary pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(true, false, [true, false]).invert().firstCycle(),
|
||||
sequence(false, true, [false, true]).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signal()', () => {
|
||||
it('Can make saw/saw2', () => {
|
||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
describe('signal()', function () {
|
||||
it('Can make saw/saw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
saw.struct(true, true, true, true).firstCycle(),
|
||||
sequence(1 / 8, 3 / 8, 5 / 8, 7 / 8).firstCycle(),
|
||||
);
|
||||
|
||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
saw2.struct(true, true, true, true).firstCycle(),
|
||||
sequence(-3 / 4, -1 / 4, 1 / 4, 3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can make isaw/isaw2', () => {
|
||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
it('Can make isaw/isaw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
isaw.struct(true, true, true, true).firstCycle(),
|
||||
sequence(7 / 8, 5 / 8, 3 / 8, 1 / 8).firstCycle(),
|
||||
);
|
||||
|
||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
isaw2.struct(true, true, true, true).firstCycle(),
|
||||
sequence(3 / 4, 1 / 4, -1 / 4, -3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_setContext()', () => {
|
||||
it('Can set the hap context', () => {
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
@@ -679,19 +706,20 @@ describe('Pattern', () => {
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_withContext()', () => {
|
||||
it('Can update the hap context', () => {
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
@@ -707,49 +735,52 @@ describe('Pattern', () => {
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('apply', () => {
|
||||
it('Can apply a function', () => {
|
||||
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
assert.deepStrictEqual(sequence('a', 'b')._apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
}),
|
||||
it('Can apply a pattern of functions', () => {
|
||||
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(sequence('a', 'b').apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b').apply(fast(2), fast(3)).firstCycle(),
|
||||
sequence('a', 'b').fast(2, 3).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('layer', () => {
|
||||
it('Can layer up multiple functions', () => {
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3)
|
||||
.layer(fast(2), (pat) => pat.add(3, 4))
|
||||
.firstCycle(),
|
||||
).toStrictEqual(stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle());
|
||||
stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('early', () => {
|
||||
it('Can shift a hap earlier', () => {
|
||||
expect(pure(30)._late(0.25).query(st(1, 2))).toStrictEqual([
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
|
||||
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
|
||||
hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
|
||||
]);
|
||||
});
|
||||
it('Can shift a hap earlier, into negative time', () => {
|
||||
expect(pure(30)._late(0.25).query(st(0, 1))).toStrictEqual([
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
|
||||
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
|
||||
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
|
||||
]);
|
||||
@@ -757,14 +788,16 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('off', () => {
|
||||
it('Can offset a transformed pattern from the original', () => {
|
||||
expect(pure(30).off(0.25, add(2)).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
pure(30).off(0.25, add(2)).firstCycle(),
|
||||
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('jux', () => {
|
||||
it('Can juxtapose', () => {
|
||||
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle(),
|
||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
@@ -773,7 +806,8 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('juxBy', () => {
|
||||
it('Can juxtapose by half', () => {
|
||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle(),
|
||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
@@ -782,12 +816,11 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('_squeezeJoin', () => {
|
||||
it('Can squeeze', () => {
|
||||
expect(
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['a', 'a'])
|
||||
.fmap((a) => fastcat('b', 'c'))
|
||||
._squeezeJoin()
|
||||
.firstCycle(),
|
||||
).toStrictEqual(
|
||||
sequence(
|
||||
['b', 'c'],
|
||||
[
|
||||
@@ -797,23 +830,17 @@ describe('Pattern', () => {
|
||||
).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Squeezes to the correct cycle', () => {
|
||||
expect(
|
||||
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
|
||||
).toStrictEqual(
|
||||
[Fraction(3.5)]
|
||||
)
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
it('Can ply(3)', () => {
|
||||
expect(sequence('a', ['b', 'c']).ply(3).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['b', 'c']).ply(3).firstCycle(),
|
||||
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Doesnt drop haps in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
|
||||
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
|
||||
});
|
||||
});
|
||||
describe('striate', () => {
|
||||
@@ -826,7 +853,8 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('chop', () => {
|
||||
it('Can _chop(2)', () => {
|
||||
expect(sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle(),
|
||||
sequence(
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
{ sound: 'a', begin: 0.5, end: 1 },
|
||||
@@ -836,7 +864,8 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('Can chop(2,3)', () => {
|
||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
|
||||
sequence(
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
@@ -853,23 +882,18 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
|
||||
sequence(0, 0.5).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
it('Can change the range of a bipolar pattern', () => {
|
||||
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle(),
|
||||
sequence(1000, 1025, 1050, 1075).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('linger', () => {
|
||||
it('Can linger on the first quarter of a cycle', () => {
|
||||
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
||||
assert.deepStrictEqual(
|
||||
sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle(),
|
||||
sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,161 +1,120 @@
|
||||
/*
|
||||
util.test.mjs - Tests for the core 'util' module
|
||||
util.test.mjs - <short description TODO>
|
||||
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 { strict as assert } from 'assert';
|
||||
import { pure } from '../pattern.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => {
|
||||
expect(isNote(note)).toBe(true);
|
||||
assert.equal(isNote(note), true);
|
||||
});
|
||||
});
|
||||
it('should recognize notes with accidentals', () => {
|
||||
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
|
||||
expect(isNote(note)).toBe(true);
|
||||
assert.equal(isNote(note), true);
|
||||
});
|
||||
});
|
||||
it('should not recognize invalid notes', () => {
|
||||
expect(isNote('H5')).toBe(false);
|
||||
expect(isNote('C')).toBe(false);
|
||||
expect(isNote('X')).toBe(false);
|
||||
expect(isNote(1)).toBe(false);
|
||||
assert.equal(isNote('H5'), false);
|
||||
assert.equal(isNote('C'), false);
|
||||
assert.equal(isNote('X'), false);
|
||||
assert.equal(isNote(1), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should tokenize notes without accidentals', () => {
|
||||
expect(tokenizeNote('C3')).toStrictEqual(['C', '', 3]);
|
||||
expect(tokenizeNote('D3')).toStrictEqual(['D', '', 3]);
|
||||
expect(tokenizeNote('E3')).toStrictEqual(['E', '', 3]);
|
||||
expect(tokenizeNote('F3')).toStrictEqual(['F', '', 3]);
|
||||
expect(tokenizeNote('G3')).toStrictEqual(['G', '', 3]);
|
||||
expect(tokenizeNote('A3')).toStrictEqual(['A', '', 3]);
|
||||
expect(tokenizeNote('B3')).toStrictEqual(['B', '', 3]);
|
||||
expect(tokenizeNote('C4')).toStrictEqual(['C', '', 4]);
|
||||
expect(tokenizeNote('D5')).toStrictEqual(['D', '', 5]);
|
||||
assert.deepStrictEqual(tokenizeNote('C3'), ['C', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('D3'), ['D', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('E3'), ['E', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('F3'), ['F', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('G3'), ['G', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('A3'), ['A', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('B3'), ['B', '', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('C4'), ['C', '', 4]);
|
||||
assert.deepStrictEqual(tokenizeNote('D5'), ['D', '', 5]);
|
||||
});
|
||||
it('should tokenize notes with accidentals', () => {
|
||||
expect(tokenizeNote('C#3')).toStrictEqual(['C', '#', 3]);
|
||||
expect(tokenizeNote('D##3')).toStrictEqual(['D', '##', 3]);
|
||||
expect(tokenizeNote('Eb3')).toStrictEqual(['E', 'b', 3]);
|
||||
expect(tokenizeNote('Fbb3')).toStrictEqual(['F', 'bb', 3]);
|
||||
expect(tokenizeNote('Bb5')).toStrictEqual(['B', 'b', 5]);
|
||||
assert.deepStrictEqual(tokenizeNote('C#3'), ['C', '#', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('D##3'), ['D', '##', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('Eb3'), ['E', 'b', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('Fbb3'), ['F', 'bb', 3]);
|
||||
assert.deepStrictEqual(tokenizeNote('Bb5'), ['B', 'b', 5]);
|
||||
});
|
||||
it('should tokenize notes without octave', () => {
|
||||
expect(tokenizeNote('C')).toStrictEqual(['C', '', undefined]);
|
||||
expect(tokenizeNote('C#')).toStrictEqual(['C', '#', undefined]);
|
||||
expect(tokenizeNote('Bb')).toStrictEqual(['B', 'b', undefined]);
|
||||
expect(tokenizeNote('Bbb')).toStrictEqual(['B', 'bb', undefined]);
|
||||
assert.deepStrictEqual(tokenizeNote('C'), ['C', '', undefined]);
|
||||
assert.deepStrictEqual(tokenizeNote('C#'), ['C', '#', undefined]);
|
||||
assert.deepStrictEqual(tokenizeNote('Bb'), ['B', 'b', undefined]);
|
||||
assert.deepStrictEqual(tokenizeNote('Bbb'), ['B', 'bb', undefined]);
|
||||
});
|
||||
it('should not tokenize invalid notes', () => {
|
||||
expect(tokenizeNote('X')).toStrictEqual([]);
|
||||
expect(tokenizeNote('asfasf')).toStrictEqual([]);
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
assert.deepStrictEqual(tokenizeNote('X'), []);
|
||||
assert.deepStrictEqual(tokenizeNote('asfasf'), []);
|
||||
assert.deepStrictEqual(tokenizeNote(123), []);
|
||||
});
|
||||
});
|
||||
describe('toMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
assert.equal(toMidi('A4'), 69);
|
||||
assert.equal(toMidi('C4'), 60);
|
||||
assert.equal(toMidi('Db4'), 61);
|
||||
assert.equal(toMidi('C3'), 48);
|
||||
assert.equal(toMidi('Cb3'), 47);
|
||||
assert.equal(toMidi('Cbb3'), 46);
|
||||
assert.equal(toMidi('C#3'), 49);
|
||||
assert.equal(toMidi('C#3'), 49);
|
||||
assert.equal(toMidi('C##3'), 50);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
assert.equal(fromMidi(69), 440);
|
||||
assert.equal(fromMidi(57), 220);
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should turn note into frequency', () => {
|
||||
expect(getFrequency(happify('a4'))).toEqual(440);
|
||||
expect(getFrequency(happify('a3'))).toEqual(220);
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
|
||||
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequencies unchanged', () => {
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||
});
|
||||
it('should turn object with a "freq" property into frequency', () => {
|
||||
expect(getFrequency(happify({freq: 220}))).toEqual(220)
|
||||
expect(getFrequency(happify({freq: 440}))).toEqual(440)
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
assert.equal(getFrequency(happify('a4')), 440);
|
||||
assert.equal(getFrequency(happify('a3')), 220);
|
||||
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
|
||||
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mod', () => {
|
||||
it('should work like regular modulo with positive numbers', () => {
|
||||
expect(mod(0, 3)).toEqual(0);
|
||||
expect(mod(1, 3)).toEqual(1);
|
||||
expect(mod(2, 3)).toEqual(2);
|
||||
expect(mod(3, 3)).toEqual(0);
|
||||
expect(mod(4, 3)).toEqual(1);
|
||||
expect(mod(4, 2)).toEqual(0);
|
||||
assert.equal(mod(0, 3), 0);
|
||||
assert.equal(mod(1, 3), 1);
|
||||
assert.equal(mod(2, 3), 2);
|
||||
assert.equal(mod(3, 3), 0);
|
||||
assert.equal(mod(4, 3), 1);
|
||||
assert.equal(mod(4, 2), 0);
|
||||
});
|
||||
it('should work with negative numbers', () => {
|
||||
expect(mod(-1, 3)).toEqual(2);
|
||||
expect(mod(-2, 3)).toEqual(1);
|
||||
expect(mod(-3, 3)).toEqual(0);
|
||||
expect(mod(-4, 3)).toEqual(2);
|
||||
expect(mod(-5, 3)).toEqual(1);
|
||||
expect(mod(-3, 2)).toEqual(1);
|
||||
assert.equal(mod(-1, 3), 2);
|
||||
assert.equal(mod(-2, 3), 1);
|
||||
assert.equal(mod(-3, 3), 0);
|
||||
assert.equal(mod(-4, 3), 2);
|
||||
assert.equal(mod(-5, 3), 1);
|
||||
assert.equal(mod(-3, 2), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose', () => {
|
||||
const add1 = (a) => a + 1;
|
||||
it('should compose', () => {
|
||||
expect(compose(add1, add1)(0)).toEqual(2);
|
||||
expect(compose(add1)(0)).toEqual(1);
|
||||
assert.equal(compose(add1, add1)(0), 2);
|
||||
assert.equal(compose(add1)(0), 1);
|
||||
});
|
||||
const addS = (s) => (a) => a + s;
|
||||
it('should compose left to right', () => {
|
||||
expect(compose(addS('a'), addS('b'))('')).toEqual('ab');
|
||||
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
||||
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
|
||||
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayableNoteValue', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should return object "note" property', () => {
|
||||
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should return object "n" property', () => {
|
||||
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should return object "value" property', () => {
|
||||
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
|
||||
})
|
||||
it('should return frequency value', () => {
|
||||
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
|
||||
})
|
||||
it('should throw an error if value is not an object, number, or string', () => {
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
|
||||
})
|
||||
});
|
||||
@@ -4,21 +4,21 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { strict as assert } from 'assert';
|
||||
import { map, valued, mul } from '../value.mjs';
|
||||
|
||||
describe('Value', () => {
|
||||
it('unionWith', () => {
|
||||
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
|
||||
expect(value).toStrictEqual({ freq: 2000, distortion: 2.4 });
|
||||
assert.deepStrictEqual(value, { freq: 2000, distortion: 2.4 });
|
||||
});
|
||||
|
||||
it('experiments', () => {
|
||||
expect(map(mul(5), valued(3)).value).toEqual(15);
|
||||
expect(map(mul(null), valued(3)).value).toEqual(0);
|
||||
expect(map(mul(3), valued(null)).value).toEqual(null);
|
||||
expect(valued(3).map(mul).ap(3).value).toEqual(9);
|
||||
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
||||
expect(valued(3).mul(3).value).toEqual(9);
|
||||
assert.equal(map(mul(5), valued(3)).value, 15);
|
||||
assert.equal(map(mul(null), valued(3)).value, 0);
|
||||
assert.equal(map(mul(3), valued(null)).value, null);
|
||||
assert.equal(valued(3).map(mul).ap(3).value, 9);
|
||||
assert.equal(valued(mul).ap(3).ap(3).value, 9);
|
||||
assert.equal(valued(3).mul(3).value, 9);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-36
@@ -10,7 +10,7 @@ export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#b]*)([0-9])?$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
@@ -24,48 +24,24 @@ export const toMidi = (note) => {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1 }[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const fromMidi = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
return fromMidi(noteOrMidi);
|
||||
}
|
||||
return fromMidi(toMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||
return pc + oct;
|
||||
};
|
||||
|
||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||
export const mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
export const getPlayableNoteValue = (hap) => {
|
||||
let { value: note, context } = hap;
|
||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||
note = note.note || note.n || note.value;
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = fromMidi(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
throw new Error('not a note: ' + JSON.stringify(note));
|
||||
} else if (typeof note === 'string' && !isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
return note;
|
||||
};
|
||||
@@ -81,7 +57,7 @@ export const getFrequency = (hap) => {
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
throw new Error('not a note or frequency:' + value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -130,10 +106,3 @@ export function curry(func, overload) {
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function objectify(value) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value; // do nothing if its already an object
|
||||
}
|
||||
return { value };
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.out()
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
-->
|
||||
</strudel-repl>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.out()
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
-->
|
||||
</strudel-repl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/embed",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -15,11 +15,9 @@ npm i @strudel.cycles/eval --save
|
||||
|
||||
```js
|
||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
// import other strudel packages here
|
||||
); // add strudel to eval scope
|
||||
extend(strudel); // add strudel to eval scope
|
||||
|
||||
async function run(code) {
|
||||
const { pattern } = await evaluate(code);
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.1.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -8,7 +8,7 @@
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
||||
@@ -34,7 +34,7 @@ export const wrappedAsync = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||
const ast = parseScriptWithLocation(code);
|
||||
const artificialNodes = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
@@ -89,7 +89,7 @@ export default (_code) => {
|
||||
|
||||
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
if (addLocations && node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
@@ -128,22 +128,10 @@ export default (_code) => {
|
||||
if (wrappedAsync) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = undisguiseImports(codegen(shifted));
|
||||
const generated = codegen(shifted);
|
||||
return generated;
|
||||
};
|
||||
|
||||
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
|
||||
// there shouldn't be any side-effects from this as this change does not affect
|
||||
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
|
||||
function disguiseImports(code) {
|
||||
return code.replaceAll('import', '_mport'); // Must be the same length!
|
||||
}
|
||||
|
||||
// Rename the renamed import statements back to "import"
|
||||
function undisguiseImports(code) {
|
||||
return code.replaceAll('_mport', 'import');
|
||||
}
|
||||
|
||||
function wrapAsync(code) {
|
||||
// wrap code in async to make await work on top level => this will create 1 line offset to locations
|
||||
// this is why line offset is -1 in getLocationObject calls below
|
||||
|
||||
@@ -4,29 +4,31 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate, evalScope } from '../evaluate.mjs';
|
||||
import { strict as assert } from 'assert';
|
||||
import { evaluate, extend } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { fastcat } = strudel;
|
||||
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
extend({ mini }, strudel);
|
||||
// TODO: test evalScope
|
||||
|
||||
describe('evaluate', () => {
|
||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
||||
it('Should evaluate strudel functions', async () => {
|
||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
|
||||
assert.deepStrictEqual(await ev("pure('c3')"), ['c3']);
|
||||
assert.deepStrictEqual(await ev('cat(c3)'), ['c3']);
|
||||
assert.deepStrictEqual(await ev('fastcat(c3, d3)'), ['c3', 'd3']);
|
||||
assert.deepStrictEqual(await ev('slowcat(c3, d3)'), ['c3']);
|
||||
});
|
||||
it('Scope should be extendable', async () => {
|
||||
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
it('Should be extendable', async () => {
|
||||
extend({ myFunction: (...x) => fastcat(...x) });
|
||||
assert.deepStrictEqual(await ev('myFunction(c3, d3)'), ['c3', 'd3']);
|
||||
});
|
||||
it('Should evaluate simple double quoted mini notation', async () => {
|
||||
expect(await ev('"c3"')).toEqual(['c3']);
|
||||
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||
assert.deepStrictEqual(await ev('"c3"'), ['c3']);
|
||||
assert.deepStrictEqual(await ev('"c3 d3"'), ['c3', 'd3']);
|
||||
assert.deepStrictEqual(await ev('"<c3 d3>"'), ['c3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,16 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { strict as assert } from 'assert';
|
||||
import shapeshifter from '../shapeshifter.mjs';
|
||||
|
||||
describe('shapeshifter', () => {
|
||||
it('Should shift simple double quote string', () => {
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
});
|
||||
it('Should handle dynamic imports', () => {
|
||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
|
||||
);
|
||||
assert.equal(shapeshifter('"c3"'), '(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
});
|
||||
});
|
||||
|
||||
+41
-44
@@ -4,8 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isNote } from 'tone';
|
||||
import _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, objectify } from '@strudel.cycles/core';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const WebMidi = _WebMidi;
|
||||
@@ -30,58 +32,53 @@ const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
if (output?.constructor?.name && isPattern(output?.constructor?.name)) {
|
||||
if (isPattern(output?.constructor?.name)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap, currentTime) => {
|
||||
let note;
|
||||
try {
|
||||
note = getPlayableNoteValue(hap);
|
||||
} catch (err) {
|
||||
// dont care if no note is found as we can still send cvs
|
||||
}
|
||||
const { velocity, nrpnn, nrpv, ccn, ccv, legato, duration: durationValue } = objectify(hap.value);
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const deadline = (time - currentTime) * 1000;
|
||||
time = WebMidi.time + deadline;
|
||||
/* const timingOffset = WebMidi.time - currentTime * 1000;
|
||||
time = time * 1000 + timingOffset; */
|
||||
const duration = (durationValue ?? hap.duration.valueOf()) * (legato ?? 1) * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
return this._withHap((hap) => {
|
||||
// const onTrigger = (time: number, hap: any) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
if (ccn && ccv) {
|
||||
device.sendControlChange(ccn, ccv, channel, { time });
|
||||
}
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.1.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"repository": {
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
+309
-445
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
// a sequence = a serie of elements placed between quotes
|
||||
// a stack = a serie of vertically aligned slices sharing the same overall length
|
||||
// a slice = a serie of horizontally aligned elements
|
||||
// a choose = a serie of elements, one of which is chosen at random
|
||||
|
||||
|
||||
{
|
||||
@@ -82,18 +81,17 @@ DIGIT = [0-9]
|
||||
// ------------------ delimiters ---------------------------
|
||||
|
||||
ws "whitespace" = [ \n\r\t]*
|
||||
comma = ws "," ws
|
||||
pipe = ws "|" ws
|
||||
comma = ws "," ws;
|
||||
quote = '"' / "'"
|
||||
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
|
||||
// single step definition (e.g bd)
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
|
||||
step = ws chars:step_char+ ws { return chars.join("") }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
|
||||
sub_cycle = ws "[" ws s:stack ws "]" ws { return s}
|
||||
|
||||
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
||||
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
||||
@@ -104,7 +102,7 @@ slice = step / sub_cycle / timeline
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
||||
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate
|
||||
|
||||
slice_weight = "@" a:number
|
||||
{ return { weight: a} }
|
||||
@@ -124,9 +122,6 @@ slice_fast = "*"a:number
|
||||
slice_fixed_step = "%"a:number
|
||||
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
||||
|
||||
slice_degrade = "?"a:number?
|
||||
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_modifier = s:slice o:slice_modifier?
|
||||
{ return new ElementStub(s, o);}
|
||||
@@ -137,22 +132,14 @@ single_cycle = s:(slice_with_modifier)+
|
||||
{ return new PatternStub(s,"h"); }
|
||||
|
||||
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
||||
stack_tail = tail:(comma @single_cycle)+
|
||||
{ return { alignment: 'v', list: tail }; }
|
||||
|
||||
// a choose is a serie of pipe-separated single cycles, one of which is chosen
|
||||
// at random each time through the pattern
|
||||
choose_tail = tail:(pipe @single_cycle)+
|
||||
{ return { alignment: 'r', list: tail }; }
|
||||
|
||||
// if the stack contains only one element, we don't create a stack but return the
|
||||
// underlying element
|
||||
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
|
||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||
stack = c:single_cycle cs:(comma v:single_cycle { return v})*
|
||||
{ if (cs.length == 0 && c instanceof Object) { return c;} else { cs.unshift(c); return new PatternStub(cs,"v");} }
|
||||
|
||||
// a sequence is a quoted stack
|
||||
sequence = ws quote sc:stack_or_choose quote
|
||||
{ return sc; }
|
||||
sequence = ws quote s:stack quote
|
||||
{ return s; }
|
||||
|
||||
// ------------------ operators ---------------------------
|
||||
|
||||
|
||||
@@ -10,13 +10,6 @@ import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
||||
|
||||
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
|
||||
|
||||
var _seedState = 0;
|
||||
const randOffset = 0.0002;
|
||||
|
||||
function _nextSeed() {
|
||||
return _seedState++;
|
||||
}
|
||||
|
||||
const applyOptions = (parent) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
@@ -28,8 +21,6 @@ const applyOptions = (parent) => (pat, i) => {
|
||||
return reify(pat).fast(speed);
|
||||
case 'bjorklund':
|
||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
||||
case 'degradeBy':
|
||||
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
|
||||
// TODO: case 'fixed-step': "%"
|
||||
}
|
||||
console.warn(`operator "${operator.type_}" not implemented`);
|
||||
@@ -91,9 +82,6 @@ export function patternifyAST(ast) {
|
||||
if (alignment === 'v') {
|
||||
return stack(...children);
|
||||
}
|
||||
if (alignment === 'r') {
|
||||
return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||
}
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (!weightedChildren && alignment === 't') {
|
||||
return slowcat(...children);
|
||||
|
||||
Generated
-1031
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs"
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,11 +25,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^2.0.1"
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,104 +4,50 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import { mini } from '../mini.mjs';
|
||||
import '@strudel.cycles/core/euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('mini', () => {
|
||||
const minV = (v) => mini(v)._firstCycleValues;
|
||||
const minS = (v) => mini(v)._showFirstCycle;
|
||||
it('supports single elements', () => {
|
||||
expect(minV('a')).toEqual(['a']);
|
||||
assert.deepStrictEqual(minV('a'), ['a']);
|
||||
});
|
||||
it('supports rest', () => {
|
||||
expect(minV('~')).toEqual([]);
|
||||
assert.deepStrictEqual(minV('~'), []);
|
||||
});
|
||||
it('supports cat', () => {
|
||||
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
||||
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
||||
assert.deepStrictEqual(minS('a b'), ['a: 0 - 1/2', 'b: 1/2 - 1']);
|
||||
assert.deepStrictEqual(minS('a b c'), ['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
||||
});
|
||||
it('supports slowcat', () => {
|
||||
expect(minV('<a b>')).toEqual(['a']);
|
||||
assert.deepStrictEqual(minV('<a b>'), ['a']);
|
||||
});
|
||||
it('supports division', () => {
|
||||
expect(minS('a/2')).toEqual(['a: 0 - 2']);
|
||||
expect(minS('[c3 d3]/2')).toEqual(['c3: 0 - 1']);
|
||||
assert.deepStrictEqual(minS('a/2'), ['a: 0 - 2']);
|
||||
assert.deepStrictEqual(minS('[c3 d3]/2'), ['c3: 0 - 1']);
|
||||
});
|
||||
it('supports multiplication', () => {
|
||||
expect(minS('c3*2')).toEqual(['c3: 0 - 1/2', 'c3: 1/2 - 1']);
|
||||
expect(minV('[c3 d3]*2')).toEqual(['c3', 'd3', 'c3', 'd3']);
|
||||
assert.deepStrictEqual(minS('c3*2'), ['c3: 0 - 1/2', 'c3: 1/2 - 1']);
|
||||
assert.deepStrictEqual(minV('[c3 d3]*2'), ['c3', 'd3', 'c3', 'd3']);
|
||||
});
|
||||
it('supports brackets', () => {
|
||||
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
||||
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
||||
assert.deepStrictEqual(minS('c3 [d3 e3]'), ['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
||||
assert.deepStrictEqual(minS('c3 [d3 [e3 f3]]'), ['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
||||
});
|
||||
it('supports commas', () => {
|
||||
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
||||
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
||||
assert.deepStrictEqual(minS('c3,e3,g3'), ['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
||||
assert.deepStrictEqual(minS('[c3,e3,g3] f3'), ['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
||||
});
|
||||
it('supports elongation', () => {
|
||||
expect(minS('a@3 b')).toEqual(['a: 0 - 3/4', 'b: 3/4 - 1']);
|
||||
expect(minS('a@2 b@3')).toEqual(['a: 0 - 2/5', 'b: 2/5 - 1']);
|
||||
assert.deepStrictEqual(minS('a@3 b'), ['a: 0 - 3/4', 'b: 3/4 - 1']);
|
||||
assert.deepStrictEqual(minS('a@2 b@3'), ['a: 0 - 2/5', 'b: 2/5 - 1']);
|
||||
});
|
||||
it('supports replication', () => {
|
||||
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||
assert.deepStrictEqual(minS('a!3 b'), ['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||
});
|
||||
it('supports euclidean rhythms', () => {
|
||||
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||
});
|
||||
it('supports the ? operator', () => {
|
||||
expect(
|
||||
mini('a?')
|
||||
.queryArc(0, 20)
|
||||
.map((hap) => hap.whole.begin),
|
||||
).toEqual(
|
||||
mini('a')
|
||||
.degradeBy(0.5)
|
||||
.queryArc(0, 20)
|
||||
.map((hap) => hap.whole.begin),
|
||||
);
|
||||
});
|
||||
// testing things that involve pseudo-randomness, so there's a probability we could fail by chance.
|
||||
// these next few tests work with the current PRNG, and are intended to succeed with p > 0.99 even if the PRNG changes
|
||||
// (as long as the PRNG has a relatively-uniform distribution of values)
|
||||
it('supports degradeBy with default of 50%', () => {
|
||||
const haps = mini('a?').queryArc(0, 1000);
|
||||
expect(459 <= haps.length && haps.length <= 541).toBe(true);
|
||||
// 'Number of elements did not fall in 99% confidence interval for binomial with p=0.5',
|
||||
});
|
||||
it('supports degradeBy with an argument', () => {
|
||||
const haps = mini('a?0.8').queryArc(0, 1000);
|
||||
expect(haps.length > 0).toBe(true);
|
||||
// 'Should have had at least one element when degradeBy was set at 0.8');
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||
// Evaluating this distribution with a chi-squared test.
|
||||
// Note: this just evaluates the overall distribution, not things like correlation/runs of values
|
||||
const observed = haps.reduce((acc, hap) => {
|
||||
acc[hap.value] = (acc[hap.value] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const expected = {
|
||||
a: numCycles / 3,
|
||||
b: numCycles / 6,
|
||||
c: numCycles / 6,
|
||||
d: numCycles / 9,
|
||||
e: numCycles / 9,
|
||||
f: numCycles / 9,
|
||||
};
|
||||
let chisq = -numCycles;
|
||||
for (let k in expected) {
|
||||
chisq += (observed[k] * observed[k]) / expected[k];
|
||||
}
|
||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||
// PRNG, this test should succeed
|
||||
expect(chisq <= 15.086).toBe(true);
|
||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||
assert.deepStrictEqual(minS('a(3, 8)'), ['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,15 +22,13 @@ let startedAt = -1;
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime, cps) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => {
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"scripts": {
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+199
-155
@@ -1,85 +1,150 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import { HighlightStyle, tags } from '@codemirror/highlight';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import { TimeSpan, State } from '@strudel.cycles/core';
|
||||
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
|
||||
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
|
||||
|
||||
var strudelTheme = createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
background: '#222',
|
||||
foreground: '#75baff', // whats that?
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#8a91991a',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#676e95',
|
||||
},
|
||||
styles: [
|
||||
{ tag: tags.keyword, color: '#c792ea' },
|
||||
{ tag: tags.operator, color: '#89ddff' },
|
||||
{ tag: tags.special(tags.variableName), color: '#eeffff' },
|
||||
{ tag: tags.typeName, color: '#f07178' },
|
||||
{ tag: tags.atom, color: '#f78c6c' },
|
||||
{ tag: tags.number, color: '#ff5370' },
|
||||
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
|
||||
{ tag: tags.string, color: '#c3e88d' },
|
||||
{ tag: tags.special(tags.string), color: '#f07178' },
|
||||
{ tag: tags.comment, color: '#7d8799' },
|
||||
{ tag: tags.variableName, color: '#f07178' },
|
||||
{ tag: tags.tagName, color: '#ff5370' },
|
||||
{ tag: tags.bracket, color: '#a2a1a4' },
|
||||
{ tag: tags.meta, color: '#ffcb6b' },
|
||||
{ tag: tags.attributeName, color: '#c792ea' },
|
||||
{ tag: tags.propertyName, color: '#c792ea' },
|
||||
{ tag: tags.className, color: '#decb6b' },
|
||||
{ tag: tags.invalid, color: '#ffffff' },
|
||||
],
|
||||
});
|
||||
/*
|
||||
Credits for color palette:
|
||||
|
||||
var style = '';
|
||||
Author: Mattia Astorino (http://github.com/equinusocio)
|
||||
Website: https://material-theme.site/
|
||||
*/
|
||||
|
||||
const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
const ivory = '#abb2bf',
|
||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||
invalid = '#ffffff',
|
||||
darkBackground = '#21252b',
|
||||
highlightBackground = 'rgba(0, 0, 0, 0.5)',
|
||||
// background = '#292d3e',
|
||||
background = 'transparent',
|
||||
tooltipBackground = '#353a42',
|
||||
selection = 'rgba(128, 203, 196, 0.5)',
|
||||
cursor = '#ffcc00';
|
||||
|
||||
/// The editor theme styles for Material Palenight.
|
||||
const materialPalenightTheme = EditorView.theme(
|
||||
{
|
||||
// done
|
||||
'&': {
|
||||
color: '#ffffff',
|
||||
backgroundColor: background,
|
||||
fontSize: '15px',
|
||||
'z-index': 11,
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
'.cm-line': {
|
||||
background: '#2C323699',
|
||||
},
|
||||
// done
|
||||
'&.cm-focused .cm-cursor': {
|
||||
backgroundColor: cursor,
|
||||
width: '3px',
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
// done, use onedarktheme
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-gutters': {
|
||||
background: 'transparent',
|
||||
color: '#676e95',
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
border: 'none',
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
update(flash2, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash2 = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash2;
|
||||
} catch (err) {
|
||||
console.warn("flash error", err);
|
||||
return flash2;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
const flash = (view) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, 200);
|
||||
};
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the Material Palenight theme.
|
||||
const materialPalenightHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: '#c792ea' },
|
||||
{ tag: tags.operator, color: '#89ddff' },
|
||||
{ tag: tags.special(tags.variableName), color: '#eeffff' },
|
||||
{ tag: tags.typeName, color: '#f07178' },
|
||||
{ tag: tags.atom, color: '#f78c6c' },
|
||||
{ tag: tags.number, color: '#ff5370' },
|
||||
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
|
||||
{ tag: tags.string, color: '#c3e88d' },
|
||||
{ tag: tags.special(tags.string), color: '#f07178' },
|
||||
{ tag: tags.comment, color: stone },
|
||||
{ tag: tags.variableName, color: '#f07178' },
|
||||
{ tag: tags.tagName, color: '#ff5370' },
|
||||
{ tag: tags.bracket, color: '#a2a1a4' },
|
||||
{ tag: tags.meta, color: '#ffcb6b' },
|
||||
{ tag: tags.attributeName, color: '#c792ea' },
|
||||
{ tag: tags.propertyName, color: '#c792ea' },
|
||||
{ tag: tags.className, color: '#decb6b' },
|
||||
{ tag: tags.invalid, color: invalid },
|
||||
]);
|
||||
|
||||
/// Extension to enable the Material Palenight theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
// : Extension
|
||||
const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
|
||||
|
||||
const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
create() {
|
||||
@@ -89,57 +154,41 @@ const highlightField = StateField.define({
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const marks = e.value.map(
|
||||
(hap) => (hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || "#FFCA28";
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return;
|
||||
}
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
return mark.range(from, to);
|
||||
})
|
||||
).flat().filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || "#FFCA28";
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return;
|
||||
}
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
return mark.range(from, to);
|
||||
})).filter(Boolean), true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
return Decoration.set([]);
|
||||
return highlights;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
||||
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
||||
const handleOnChange = useCallback(
|
||||
(value2) => {
|
||||
onChange?.(value2);
|
||||
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
|
||||
onViewChange: onViewChanged,
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1 0 auto"
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const handleOnCreateEditor = useCallback(
|
||||
(view) => {
|
||||
onViewChanged?.(view);
|
||||
},
|
||||
[onViewChanged]
|
||||
);
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
},
|
||||
[onSelectionChange]
|
||||
);
|
||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
|
||||
value,
|
||||
onChange: handleOnChange,
|
||||
onCreateEditor: handleOnCreateEditor,
|
||||
onUpdate: handleOnUpdate,
|
||||
extensions
|
||||
onChange,
|
||||
extensions: [
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -251,7 +300,7 @@ let s4 = () => {
|
||||
};
|
||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
||||
|
||||
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState(tune);
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
@@ -270,8 +319,6 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
}
|
||||
}, [activeCode, onDrawProp]);
|
||||
|
||||
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
|
||||
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
|
||||
// cycle hook to control scheduling
|
||||
const cycle = useCycle({
|
||||
onDraw,
|
||||
@@ -282,15 +329,33 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
if (event.context.logs?.length) {
|
||||
event.context.logs.forEach(pushLog);
|
||||
}
|
||||
const { onTrigger = webaudioOutputTrigger } = event.context;
|
||||
onTrigger(time, event, currentTime, 1 /* cps */);
|
||||
const { onTrigger, velocity } = event.context;
|
||||
if (!onTrigger) {
|
||||
if (defaultSynth) {
|
||||
const note = getPlayableNoteValue(event);
|
||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
onTrigger(
|
||||
time,
|
||||
event,
|
||||
currentTime,
|
||||
1 /* cps */,
|
||||
event.wholeOrPart().begin.valueOf(),
|
||||
event.duration.valueOf(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
},
|
||||
[onEvent, pushLog],
|
||||
[onEvent, pushLog, defaultSynth],
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(state) => {
|
||||
@@ -305,7 +370,7 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
},
|
||||
[pattern],
|
||||
),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events), []),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
||||
ready: !!pattern && !!activeCode,
|
||||
});
|
||||
|
||||
@@ -359,8 +424,6 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
};
|
||||
|
||||
return {
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
@@ -431,13 +494,15 @@ function useHighlighting({ view, pattern, active }) {
|
||||
|
||||
var tailwind = '';
|
||||
|
||||
const container = "_container_3i85k_1";
|
||||
const header = "_header_3i85k_5";
|
||||
const buttons = "_buttons_3i85k_9";
|
||||
const button = "_button_3i85k_9";
|
||||
const buttonDisabled = "_buttonDisabled_3i85k_17";
|
||||
const error = "_error_3i85k_21";
|
||||
const body = "_body_3i85k_25";
|
||||
var style = '';
|
||||
|
||||
const container = "_container_10e1g_1";
|
||||
const header = "_header_10e1g_5";
|
||||
const buttons = "_buttons_10e1g_9";
|
||||
const button = "_button_10e1g_9";
|
||||
const buttonDisabled = "_buttonDisabled_10e1g_17";
|
||||
const error = "_error_10e1g_21";
|
||||
const body = "_body_10e1g_25";
|
||||
var styles = {
|
||||
container: container,
|
||||
header: header,
|
||||
@@ -473,15 +538,12 @@ function Icon({ type }) {
|
||||
}[type]);
|
||||
}
|
||||
|
||||
function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
|
||||
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
tune,
|
||||
autolink: false,
|
||||
onEvent
|
||||
defaultSynth,
|
||||
autolink: false
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01
|
||||
@@ -493,25 +555,7 @@ function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
|
||||
useLayoutEffect(() => {
|
||||
if (enableKeyboard) {
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === "Period") {
|
||||
cycle.stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyPress, true);
|
||||
return () => window.removeEventListener("keydown", handleKeyPress, true);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
return /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.container,
|
||||
ref
|
||||
@@ -579,4 +623,4 @@ function useWebMidi(props) {
|
||||
return { loading, outputs, outputByName };
|
||||
}
|
||||
|
||||
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
|
||||
export { CodeMirror, MiniRepl, cx, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
||||
*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.2",
|
||||
"description": "React components for strudel",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
@@ -37,12 +37,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.0.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"@uiw/codemirror-themes": "^4.11.4",
|
||||
"@uiw/react-codemirror": "^4.11.4",
|
||||
"@codemirror/lang-javascript": "^0.19.0",
|
||||
"@strudel.cycles/core": "*",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"react-codemirror6": "^1.1.0",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react';
|
||||
import { MiniRepl } from './components/MiniRepl';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
|
||||
import { evalScope } from '@strudel.cycles/eval';
|
||||
|
||||
const defaultSynth = getDefaultSynth();
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
@@ -16,7 +20,7 @@ evalScope(
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<MiniRepl tune={`"c3"`} />
|
||||
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +1,11 @@
|
||||
import React from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
|
||||
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, 200);
|
||||
};
|
||||
// import { materialPalenight } from 'codemirror6-themes';
|
||||
import { materialPalenight } from '../themes/material-palenight';
|
||||
|
||||
export const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
@@ -49,9 +16,9 @@ const highlightField = StateField.define({
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const marks =
|
||||
highlights = Decoration.set(
|
||||
e.value
|
||||
.map((hap) =>
|
||||
.flatMap((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || '#FFCA28';
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
@@ -60,56 +27,42 @@ const highlightField = StateField.define({
|
||||
if (from > l || to > l) {
|
||||
return; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
.filter(Boolean),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return Decoration.set([]);
|
||||
return highlights;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
||||
|
||||
export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
||||
const handleOnChange = useCallback(
|
||||
(value) => {
|
||||
onChange?.(value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
const handleOnCreateEditor = useCallback(
|
||||
(view) => {
|
||||
onViewChanged?.(view);
|
||||
},
|
||||
[onViewChanged],
|
||||
);
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
||||
return (
|
||||
<>
|
||||
<_CodeMirror
|
||||
onViewChange={onViewChanged}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 0 auto',
|
||||
}}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
onChange={onChange}
|
||||
extensions={[
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField,
|
||||
// theme, language, ...
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import useRepl from '../hooks/useRepl.mjs';
|
||||
import cx from '../cx';
|
||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import CodeMirror6, { flash } from './CodeMirror6';
|
||||
import CodeMirror6 from './CodeMirror6';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import './style.css';
|
||||
import styles from './MiniRepl.module.css';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
||||
useRepl({
|
||||
tune,
|
||||
autolink: false,
|
||||
onEvent,
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
});
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
@@ -30,28 +26,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
if (enableKeyboard) {
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
cycle.stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
||||
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
@@ -65,7 +40,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||
</div>
|
||||
{error && <div className={styles.error}>{error.message}</div>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.body} >
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.container {
|
||||
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#222222];
|
||||
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57];
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
.cm-editor {
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-line > * {
|
||||
background: #00000095;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
import useCycle from './useCycle.mjs';
|
||||
import usePostMessage from './usePostMessage.mjs';
|
||||
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
|
||||
|
||||
let s4 = () => {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
@@ -17,7 +17,7 @@ let s4 = () => {
|
||||
};
|
||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
||||
|
||||
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState(tune);
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
@@ -36,8 +36,6 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
}
|
||||
}, [activeCode, onDrawProp]);
|
||||
|
||||
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
|
||||
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
|
||||
// cycle hook to control scheduling
|
||||
const cycle = useCycle({
|
||||
onDraw,
|
||||
@@ -48,15 +46,33 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
if (event.context.logs?.length) {
|
||||
event.context.logs.forEach(pushLog);
|
||||
}
|
||||
const { onTrigger = webaudioOutputTrigger } = event.context;
|
||||
onTrigger(time, event, currentTime, 1 /* cps */);
|
||||
const { onTrigger, velocity } = event.context;
|
||||
if (!onTrigger) {
|
||||
if (defaultSynth) {
|
||||
const note = getPlayableNoteValue(event);
|
||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
onTrigger(
|
||||
time,
|
||||
event,
|
||||
currentTime,
|
||||
1 /* cps */,
|
||||
event.wholeOrPart().begin.valueOf(),
|
||||
event.duration.valueOf(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
},
|
||||
[onEvent, pushLog],
|
||||
[onEvent, pushLog, defaultSynth],
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(state) => {
|
||||
@@ -127,8 +143,6 @@ function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
};
|
||||
|
||||
return {
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6';
|
||||
export { default as CodeMirror } from './components/CodeMirror6';
|
||||
export * from './components/MiniRepl';
|
||||
export { default as useCycle } from './hooks/useCycle';
|
||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { HighlightStyle, tags as t } from '@codemirror/highlight';
|
||||
|
||||
/*
|
||||
Credits for color palette:
|
||||
|
||||
Author: Mattia Astorino (http://github.com/equinusocio)
|
||||
Website: https://material-theme.site/
|
||||
*/
|
||||
|
||||
const ivory = '#abb2bf',
|
||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||
invalid = '#ffffff',
|
||||
darkBackground = '#21252b',
|
||||
highlightBackground = 'rgba(0, 0, 0, 0.5)',
|
||||
// background = '#292d3e',
|
||||
background = 'transparent',
|
||||
tooltipBackground = '#353a42',
|
||||
selection = 'rgba(128, 203, 196, 0.5)',
|
||||
cursor = '#ffcc00';
|
||||
|
||||
/// The editor theme styles for Material Palenight.
|
||||
export const materialPalenightTheme = EditorView.theme(
|
||||
{
|
||||
// done
|
||||
'&': {
|
||||
color: '#ffffff',
|
||||
backgroundColor: background,
|
||||
fontSize: '15px',
|
||||
'z-index': 11,
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
'.cm-line': {
|
||||
background: '#2C323699',
|
||||
},
|
||||
// done
|
||||
'&.cm-focused .cm-cursor': {
|
||||
backgroundColor: cursor,
|
||||
width: '3px',
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
// done, use onedarktheme
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-gutters': {
|
||||
background: 'transparent',
|
||||
color: '#676e95',
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
border: 'none',
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the Material Palenight theme.
|
||||
export const materialPalenightHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: '#c792ea' },
|
||||
{ tag: t.operator, color: '#89ddff' },
|
||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||
{ tag: t.typeName, color: '#f07178' },
|
||||
{ tag: t.atom, color: '#f78c6c' },
|
||||
{ tag: t.number, color: '#ff5370' },
|
||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
||||
{ tag: t.string, color: '#c3e88d' },
|
||||
{ tag: t.special(t.string), color: '#f07178' },
|
||||
{ tag: t.comment, color: stone },
|
||||
{ tag: t.variableName, color: '#f07178' },
|
||||
{ tag: t.tagName, color: '#ff5370' },
|
||||
{ tag: t.bracket, color: '#a2a1a4' },
|
||||
{ tag: t.meta, color: '#ffcb6b' },
|
||||
{ tag: t.attributeName, color: '#c792ea' },
|
||||
{ tag: t.propertyName, color: '#c792ea' },
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: invalid },
|
||||
]);
|
||||
|
||||
/// Extension to enable the Material Palenight theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
// : Extension
|
||||
export const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
|
||||
@@ -1,36 +0,0 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
background: '#222',
|
||||
foreground: '#75baff', // whats that?
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#8a91991a',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#676e95',
|
||||
},
|
||||
styles: [
|
||||
{ tag: t.keyword, color: '#c792ea' },
|
||||
{ tag: t.operator, color: '#89ddff' },
|
||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||
{ tag: t.typeName, color: '#f07178' },
|
||||
{ tag: t.atom, color: '#f78c6c' },
|
||||
{ tag: t.number, color: '#ff5370' },
|
||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
||||
{ tag: t.string, color: '#c3e88d' },
|
||||
{ tag: t.special(t.string), color: '#f07178' },
|
||||
{ tag: t.comment, color: '#7d8799' },
|
||||
{ tag: t.variableName, color: '#f07178' },
|
||||
{ tag: t.tagName, color: '#ff5370' },
|
||||
{ tag: t.bracket, color: '#a2a1a4' },
|
||||
{ tag: t.meta, color: '#ffcb6b' },
|
||||
{ tag: t.attributeName, color: '#c792ea' },
|
||||
{ tag: t.propertyName, color: '#c792ea' },
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: '#ffffff' },
|
||||
],
|
||||
});
|
||||
@@ -33,14 +33,8 @@ export default defineConfig({
|
||||
'@strudel.cycles/serial',
|
||||
'@strudel.cycles/webaudio',
|
||||
'@codemirror/view',
|
||||
'@codemirror/lang-javascript',
|
||||
'@codemirror/state',
|
||||
'@codemirror/commands',
|
||||
'@lezer/highlight',
|
||||
'@codemirror/language',
|
||||
'@uiw/codemirror-themes',
|
||||
'@uiw/react-codemirror',
|
||||
'@lezer/highlight',
|
||||
'@codemirror/highlight',
|
||||
'@codemirror/state'
|
||||
],
|
||||
},
|
||||
target: 'esnext',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"repository": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
var serialWriter;
|
||||
var choosing = false;
|
||||
|
||||
export async function getWriter(br=38400) {
|
||||
export async function getWriter(br=115200) {
|
||||
if (choosing) {
|
||||
return;
|
||||
}
|
||||
@@ -34,7 +34,8 @@ export async function getWriter(br=38400) {
|
||||
|
||||
const latency = 0.1;
|
||||
|
||||
Pattern.prototype.serial = function (...args) {
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.serial = async function (...args) {
|
||||
return this._withHap((hap) => {
|
||||
if (!serialWriter) {
|
||||
getWriter(...args);
|
||||
@@ -42,27 +43,8 @@ Pattern.prototype.serial = function (...args) {
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
var message = "";
|
||||
if (typeof hap.value === 'object') {
|
||||
if ('action' in hap.value) {
|
||||
message += hap.value['action'] + '(';
|
||||
var first = true;
|
||||
for (const [key, val] of Object.entries(hap.value)) {
|
||||
if (key === 'action') {
|
||||
continue;
|
||||
}
|
||||
if (first) {
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
message +=',';
|
||||
}
|
||||
message += `${key}:${val}`
|
||||
}
|
||||
message += ')';
|
||||
}
|
||||
else {
|
||||
for (const [key, val] of Object.entries(hap.value)) {
|
||||
message += `${key}:${val};`
|
||||
}
|
||||
for (const [key, val] of Object.entries(hap.value).flat()) {
|
||||
message += `${key}:${val};`
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// this script converts a soundfont into a json file, it has not been not used yet
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const name = '0000_JCLive';
|
||||
|
||||
const js = await fetch(`https://felixroos.github.io/webaudiofontdata/sound/${name}_sf2_file.js`).then((res) =>
|
||||
res.text(),
|
||||
);
|
||||
// console.log(js);
|
||||
|
||||
let [_, data] = js.split('_sf2_file=');
|
||||
data = eval(data);
|
||||
console.log(JSON.stringify(data));
|
||||
@@ -1,114 +0,0 @@
|
||||
let loadCache = {};
|
||||
async function loadFont(name) {
|
||||
if (loadCache[name]) {
|
||||
return loadCache[name];
|
||||
}
|
||||
const load = async () => {
|
||||
// TODO: make soundfont source configurable
|
||||
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
||||
console.log('load font', name, url);
|
||||
const preset = await fetch(url).then((res) => res.text());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
};
|
||||
loadCache[name] = load();
|
||||
return loadCache[name];
|
||||
}
|
||||
|
||||
export async function getFontBufferSource(name, pitch, ac) {
|
||||
if (typeof pitch === 'string') {
|
||||
pitch = toMidi(pitch);
|
||||
}
|
||||
const { buffer, zone } = await getFontPitch(name, pitch, ac);
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
||||
// src detune?
|
||||
src.playbackRate.value = playbackRate;
|
||||
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
|
||||
if (!loop) {
|
||||
/* const waveDuration = duration + this.afterTime;
|
||||
if (waveDuration > zone.buffer.duration / playbackRate) {
|
||||
waveDuration = zone.buffer.duration / playbackRate;
|
||||
// TODO: do sth with waveduration
|
||||
} */
|
||||
} else {
|
||||
src.loop = true;
|
||||
src.loopStart = zone.loopStart / zone.sampleRate;
|
||||
src.loopEnd = zone.loopEnd / zone.sampleRate;
|
||||
//+ (zone.delay ? zone.delay : 0);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
let bufferCache = {};
|
||||
export async function getFontPitch(name, pitch, ac) {
|
||||
const key = `${name}:::${pitch}`;
|
||||
if (bufferCache[key]) {
|
||||
return bufferCache[key];
|
||||
}
|
||||
// console.log('load buffer', key);
|
||||
const load = async () => {
|
||||
const preset = await loadFont(name);
|
||||
if (!preset) {
|
||||
throw new Error(`Could not load soundfont ${name}`);
|
||||
}
|
||||
const zone = findZone(preset, pitch);
|
||||
if (!zone) {
|
||||
throw new Error('no soundfont zone found for preset ', name, 'pitch', pitch);
|
||||
}
|
||||
const buffer = await getBuffer(zone, ac);
|
||||
if (!buffer) {
|
||||
throw new Error(`no soundfont buffer found for preset ${name}, pitch: ${pitch}`);
|
||||
}
|
||||
return { buffer, zone };
|
||||
};
|
||||
bufferCache[key] = load(); // dont await here to cache promise immediately!
|
||||
return bufferCache[key];
|
||||
}
|
||||
|
||||
function findZone(preset, pitch) {
|
||||
return preset.find((zone) => {
|
||||
return zone.keyRangeLow <= pitch && zone.keyRangeHigh + 1 >= pitch;
|
||||
});
|
||||
}
|
||||
|
||||
// promisified version of https://github.com/felixroos/webaudiofont/blob/c6f97249b60dcfafc20fca5bb381294a6b2f8f51/npm/dist/WebAudioFontPlayer.js#L740
|
||||
async function getBuffer(zone, audioContext) {
|
||||
if (zone.sample) {
|
||||
console.warn('zone.sample untested!');
|
||||
const decoded = atob(zone.sample);
|
||||
zone.buffer = audioContext.createBuffer(1, decoded.length / 2, zone.sampleRate);
|
||||
const float32Array = zone.buffer.getChannelData(0);
|
||||
let b1, b2, n;
|
||||
for (var i = 0; i < decoded.length / 2; i++) {
|
||||
b1 = decoded.charCodeAt(i * 2);
|
||||
b2 = decoded.charCodeAt(i * 2 + 1);
|
||||
if (b1 < 0) {
|
||||
b1 = 256 + b1;
|
||||
}
|
||||
if (b2 < 0) {
|
||||
b2 = 256 + b2;
|
||||
}
|
||||
n = b2 * 256 + b1;
|
||||
if (n >= 65536 / 2) {
|
||||
n = n - 65536;
|
||||
}
|
||||
float32Array[i] = n / 65536.0;
|
||||
}
|
||||
} else {
|
||||
if (zone.file) {
|
||||
const datalen = zone.file.length;
|
||||
const arraybuffer = new ArrayBuffer(datalen);
|
||||
const view = new Uint8Array(arraybuffer);
|
||||
const decoded = atob(zone.file);
|
||||
let b;
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
b = decoded.charCodeAt(i);
|
||||
view[i] = b;
|
||||
}
|
||||
return new Promise((resolve) => audioContext.decodeAudioData(arraybuffer, resolve));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { getFontBufferSource } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
||||
File diff suppressed because it is too large
Load Diff
Generated
-301
@@ -1,301 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"webaudiofont": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA=="
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz",
|
||||
"integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
|
||||
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
|
||||
"dependencies": {
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"vscode-oniguruma": "^1.6.1",
|
||||
"vscode-textmate": "5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc": {
|
||||
"version": "0.22.17",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
|
||||
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
|
||||
"dependencies": {
|
||||
"glob": "^8.0.3",
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.0.16",
|
||||
"minimatch": "^5.1.0",
|
||||
"shiki": "^0.10.1"
|
||||
},
|
||||
"bin": {
|
||||
"typedoc": "bin/typedoc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-oniguruma": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
|
||||
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA=="
|
||||
},
|
||||
"node_modules/vscode-textmate": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
|
||||
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ=="
|
||||
},
|
||||
"node_modules/webaudiofont": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webaudiofont/-/webaudiofont-3.0.1.tgz",
|
||||
"integrity": "sha512-PwqbR4OdvzbW8/TLTitSU/rw4mxMKyAGTKV42mePCgMoUGLJoQj8HDLWVqtNOpUZ0ghPNhNE6cRFEv6HFFEBQA==",
|
||||
"dependencies": {
|
||||
"typedoc": "^0.22.14"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"jsonc-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA=="
|
||||
},
|
||||
"lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
|
||||
},
|
||||
"marked": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz",
|
||||
"integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"shiki": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
|
||||
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
|
||||
"requires": {
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"vscode-oniguruma": "^1.6.1",
|
||||
"vscode-textmate": "5.2.0"
|
||||
}
|
||||
},
|
||||
"typedoc": {
|
||||
"version": "0.22.17",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.17.tgz",
|
||||
"integrity": "sha512-h6+uXHVVCPDaANzjwzdsj9aePBjZiBTpiMpBBeyh1zcN2odVsDCNajz8zyKnixF93HJeGpl34j/70yoEE5BfNg==",
|
||||
"requires": {
|
||||
"glob": "^8.0.3",
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.0.16",
|
||||
"minimatch": "^5.1.0",
|
||||
"shiki": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"peer": true
|
||||
},
|
||||
"vscode-oniguruma": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
|
||||
"integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA=="
|
||||
},
|
||||
"vscode-textmate": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
|
||||
"integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ=="
|
||||
},
|
||||
"webaudiofont": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webaudiofont/-/webaudiofont-3.0.1.tgz",
|
||||
"integrity": "sha512-PwqbR4OdvzbW8/TLTitSU/rw4mxMKyAGTKV42mePCgMoUGLJoQj8HDLWVqtNOpUZ0ghPNhNE6cRFEv6HFFEBQA==",
|
||||
"requires": {
|
||||
"typedoc": "^0.22.14"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.2.1",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/webaudio": "^0.2.0",
|
||||
"sfumato": "^0.1.2",
|
||||
"soundfont2": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.2.6"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
return this.onTrigger((t, h, ct) => {
|
||||
const ctx = getAudioContext();
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
const deadline = ctx.currentTime + t - ct;
|
||||
const args = [ctx, preset, toMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
stop(deadline + h.duration);
|
||||
});
|
||||
};
|
||||
|
||||
const soundfontCache = new Map();
|
||||
export function loadSoundfont(url) {
|
||||
if (soundfontCache.get(url)) {
|
||||
return soundfontCache.get(url);
|
||||
}
|
||||
const sf = _loadSoundfont(url);
|
||||
soundfontCache.set(url, sf);
|
||||
return sf;
|
||||
}
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.1.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// import { strict as assert } from 'assert';
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import '../tonal.mjs'; // need to import this to add prototypes
|
||||
import { pure } from '@strudel.cycles/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('tonal', () => {
|
||||
it('Should run tonal functions ', () => {
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues).toEqual(['D3']);
|
||||
assert.deepStrictEqual(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues, ['D3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,9 +69,9 @@ function scaleOffset(scale, offset, note) {
|
||||
* @memberof Pattern
|
||||
* @name transpose
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
|
||||
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(0)
|
||||
*/
|
||||
|
||||
Pattern.prototype._transpose = function (intervalOrSemitones) {
|
||||
@@ -107,7 +107,6 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
|
||||
* "-8 [2,4,6]"
|
||||
* .scale('C4 bebop major')
|
||||
* .scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")
|
||||
* .note()
|
||||
*/
|
||||
|
||||
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
||||
@@ -135,10 +134,9 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
||||
* @name scale
|
||||
* @param {string} scale Name of scale
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* @example
|
||||
* "0 2 4 6 4 2"
|
||||
* .scale(seq('C2 major', 'C2 minor').slow(2))
|
||||
* .note()
|
||||
*/
|
||||
|
||||
Pattern.prototype._scale = function (scale /* : string */) {
|
||||
|
||||
@@ -40,7 +40,7 @@ Pattern.prototype.fmapNested = function (func) {
|
||||
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
|
||||
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
|
||||
*/
|
||||
|
||||
Pattern.prototype.voicings = function (range) {
|
||||
|
||||
@@ -36,7 +36,8 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
const begin = currentCycle + from;
|
||||
const end = currentCycle + to;
|
||||
setTimeout(() => {
|
||||
events = this.query(new State(new TimeSpan(begin, end)))
|
||||
events = this._asNumber(true) // true = silent error
|
||||
.query(new State(new TimeSpan(begin, end)))
|
||||
.filter(Boolean)
|
||||
.filter((event) => event.part.begin.equals(event.whole.begin));
|
||||
onQuery?.(events);
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.1.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
||||
+16
-32
@@ -7,13 +7,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
|
||||
if (typeof value === 'string') {
|
||||
value = toMidi(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
Pattern.prototype.pianoroll = function ({
|
||||
cycles = 4,
|
||||
@@ -22,14 +15,10 @@ Pattern.prototype.pianoroll = function ({
|
||||
flipTime = 0,
|
||||
flipValues = 0,
|
||||
hideNegative = false,
|
||||
// inactive = '#C9E597',
|
||||
// inactive = '#FFCA28',
|
||||
inactive = '#7491D2',
|
||||
inactive = '#C9E597',
|
||||
active = '#FFCA28',
|
||||
// background = '#2A3236',
|
||||
background = 'transparent',
|
||||
smear = 0,
|
||||
playheadColor = 'white',
|
||||
minMidi = 10,
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
@@ -62,14 +51,12 @@ Pattern.prototype.pianoroll = function ({
|
||||
flipTime && timeRange.reverse();
|
||||
flipValues && valueRange.reverse();
|
||||
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
this.draw(
|
||||
(ctx, events, t) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||
events.filter(inFrame).forEach((event) => {
|
||||
@@ -77,11 +64,20 @@ Pattern.prototype.pianoroll = function ({
|
||||
ctx.fillStyle = event.context?.color || inactive;
|
||||
ctx.strokeStyle = event.context?.color || active;
|
||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||
ctx.beginPath();
|
||||
if (vertical) {
|
||||
ctx.moveTo(0, playheadPosition);
|
||||
ctx.lineTo(valueAxis, playheadPosition);
|
||||
} else {
|
||||
ctx.moveTo(playheadPosition, 0);
|
||||
ctx.lineTo(playheadPosition, valueAxis);
|
||||
}
|
||||
ctx.stroke();
|
||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
|
||||
const valuePx = scale(
|
||||
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||
fold ? foldValues.indexOf(event.value) / foldValues.length : (Number(event.value) - minMidi) / valueExtent,
|
||||
...valueRange,
|
||||
);
|
||||
let margin = 0;
|
||||
@@ -104,24 +100,12 @@ Pattern.prototype.pianoroll = function ({
|
||||
}
|
||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||
});
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
// draw playhead
|
||||
ctx.strokeStyle = playheadColor;
|
||||
ctx.beginPath();
|
||||
if (vertical) {
|
||||
ctx.moveTo(0, playheadPosition);
|
||||
ctx.lineTo(valueAxis, playheadPosition);
|
||||
} else {
|
||||
ctx.moveTo(playheadPosition, 0);
|
||||
ctx.lineTo(playheadPosition, valueAxis);
|
||||
}
|
||||
ctx.stroke();
|
||||
},
|
||||
{
|
||||
from: from - overscan,
|
||||
to: to + overscan,
|
||||
onQuery: (events) => {
|
||||
const getValue = (e) => Number(e.value);
|
||||
const { min, max, values } = events.reduce(
|
||||
({ min, max, values }, e) => {
|
||||
const v = getValue(e);
|
||||
|
||||
@@ -4,15 +4,15 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import '../tone.mjs';
|
||||
import { pure } from '@strudel.cycles/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Tone from 'tone';
|
||||
|
||||
describe('tone', () => {
|
||||
it('Should have working tone function', () => {
|
||||
// const s = synth().chain(out()); // TODO: mock audio context?
|
||||
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
||||
const s = {};
|
||||
expect(pure('c3').tone(s)._firstCycleValues).toEqual(['c3']);
|
||||
const s = synth().chain(out());
|
||||
assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
||||
assert.deepStrictEqual(pure('c3').tone(s)._firstCycleValues, ['c3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,13 +15,13 @@ npm i @strudel.cycles/webaudio --save
|
||||
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
|
||||
|
||||
const scheduler = new Scheduler({
|
||||
audioContext: getAudioContext(),
|
||||
interval: 0.1,
|
||||
onEvent: (e) => e.context?.createAudioNode?.(e),
|
||||
});
|
||||
const pattern = sequence([55, 99], 110).osc('sawtooth');
|
||||
audioContext: getAudioContext(),
|
||||
interval: 0.1,
|
||||
onEvent: (e) => e.context?.createAudioNode?.(e),
|
||||
});
|
||||
const pattern = sequence([55, 99], 110).osc('sawtooth').out()
|
||||
scheduler.setPattern(pattern);
|
||||
scheduler.start();
|
||||
scheduler.start()
|
||||
//scheduler.stop()
|
||||
```
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ const stringifyFunction = (func) => '(' + func + ')();';
|
||||
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
// this is just a setInterval with a counter, running in a worker
|
||||
// this class is basically the tale of two clocks
|
||||
export class ClockWorker {
|
||||
worker;
|
||||
interval = 0.1; // query span
|
||||
tick = 0;
|
||||
constructor(callback, interval = this.interval) {
|
||||
audioContext;
|
||||
interval = 0.2; // query span
|
||||
lastEnd = 0;
|
||||
constructor(audioContext, callback, interval = this.interval) {
|
||||
this.audioContext = audioContext;
|
||||
this.interval = interval;
|
||||
this.worker = createWorker(() => {
|
||||
// we cannot use closures here!
|
||||
@@ -31,7 +33,6 @@ export class ClockWorker {
|
||||
if (!interval) {
|
||||
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
|
||||
}
|
||||
postMessage('tick');
|
||||
timerID = setInterval(() => postMessage('tick'), interval * 1000);
|
||||
};
|
||||
self.onmessage = function (e) {
|
||||
@@ -49,23 +50,25 @@ export class ClockWorker {
|
||||
});
|
||||
this.worker.postMessage({ interval });
|
||||
// const round = (n, d) => Math.round(n * d) / d;
|
||||
const precision = 100;
|
||||
this.worker.onmessage = (e) => {
|
||||
if (e.data === 'tick') {
|
||||
const begin = this.lastEnd || this.audioContext.currentTime;
|
||||
const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
|
||||
this.lastEnd = end;
|
||||
// callback with query span, using clock #2 (the audio clock)
|
||||
callback(this.tick++, this.interval);
|
||||
callback(begin, end);
|
||||
}
|
||||
};
|
||||
}
|
||||
start() {
|
||||
// console.log('start...');
|
||||
console.log('start...');
|
||||
this.audioContext.resume();
|
||||
this.worker.postMessage('start');
|
||||
}
|
||||
stop() {
|
||||
// console.log('stop...');
|
||||
console.log('stop...');
|
||||
this.worker.postMessage('stop');
|
||||
this.tick = 0;
|
||||
}
|
||||
setInterval(interval) {
|
||||
this.worker.postMessage({ interval });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<div style="position: absolute; top: 0; right: 0; padding: 4px">
|
||||
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
|
||||
<button id="stop" style="margin-bottom: 4px; font-size: 2em">stop</button><br />
|
||||
<button id="slower" style="font-size: 2em">-</button>
|
||||
<button id="faster" style="font-size: 2em">+</button>
|
||||
<button id="stop" style="font-size: 2em">stop</button>
|
||||
</div>
|
||||
<textarea
|
||||
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
|
||||
style="font-size: 2em; background: #bce865; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
|
||||
id="text"
|
||||
spellcheck="false"
|
||||
>
|
||||
@@ -13,9 +11,11 @@ Loading...</textarea
|
||||
>
|
||||
<script type="module">
|
||||
document.body.style = 'margin: 0';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import * as util from '@strudel.cycles/core/util.mjs';
|
||||
// import * as strudel from '@strudel.cycles/core';
|
||||
import * as strudel from '../../core/index.mjs';
|
||||
import * as util from '../../core/util.mjs';
|
||||
import '@strudel.cycles/core/euclid.mjs';
|
||||
// import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
|
||||
import { Scheduler, getAudioContext } from '../index.mjs';
|
||||
|
||||
const { cat, State, TimeSpan } = strudel;
|
||||
@@ -33,12 +33,11 @@ Loading...</textarea
|
||||
.mul(slowcat(1,2))
|
||||
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
|
||||
.fast(3)
|
||||
.freq()
|
||||
.velocity(.5)
|
||||
.s('sawtooth')
|
||||
.cutoff(800)
|
||||
.out()
|
||||
`;
|
||||
.wave(cat('sawtooth','square').fast(2))
|
||||
.adsr(0.01,.02,.5,0.1)
|
||||
.filter('lowshelf',800,25)
|
||||
.out()`;
|
||||
|
||||
try {
|
||||
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
|
||||
@@ -62,6 +61,4 @@ Loading...</textarea
|
||||
|
||||
document.getElementById('start').addEventListener('click', () => scheduler.start());
|
||||
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
|
||||
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
|
||||
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
|
||||
</script>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
if (typeof DelayNode !== 'undefined') {
|
||||
class FeedbackDelayNode extends DelayNode {
|
||||
constructor(ac, wet, time, feedback) {
|
||||
super(ac);
|
||||
wet = Math.abs(wet);
|
||||
this.delayTime.value = time;
|
||||
|
||||
const feedbackGain = ac.createGain();
|
||||
feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
|
||||
this.feedback = feedbackGain.gain;
|
||||
|
||||
const delayGain = ac.createGain();
|
||||
delayGain.gain.value = wet;
|
||||
this.delayGain = delayGain;
|
||||
|
||||
this.connect(feedbackGain);
|
||||
this.connect(delayGain);
|
||||
feedbackGain.connect(this);
|
||||
|
||||
this.connect = (target) => delayGain.connect(target);
|
||||
return this;
|
||||
}
|
||||
start(t) {
|
||||
this.delayGain.gain.setValueAtTime(this.delayGain.gain.value, t + this.delayTime.value);
|
||||
}
|
||||
}
|
||||
|
||||
AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) {
|
||||
return new FeedbackDelayNode(this, wet, time, feedback);
|
||||
};
|
||||
}
|
||||
@@ -7,4 +7,3 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
export * from './clockworker.mjs';
|
||||
export * from './scheduler.mjs';
|
||||
export * from './webaudio.mjs';
|
||||
export * from './sampler.mjs';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -28,6 +28,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0"
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.impulseResponse = function (duration) {
|
||||
const length = this.sampleRate * duration;
|
||||
const impulse = this.createBuffer(2, length, this.sampleRate);
|
||||
const IR = impulse.getChannelData(0);
|
||||
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
|
||||
return impulse;
|
||||
};
|
||||
|
||||
AudioContext.prototype.createReverb = function (duration) {
|
||||
const convolver = this.createConvolver();
|
||||
convolver.setDuration = (d) => {
|
||||
convolver.buffer = this.impulseResponse(d);
|
||||
convolver.duration = duration;
|
||||
return convolver;
|
||||
};
|
||||
convolver.setDuration(duration);
|
||||
return convolver;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: make the reverb more exciting
|
||||
// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html
|
||||
@@ -5,43 +5,33 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { ClockWorker } from './clockworker.mjs';
|
||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
||||
|
||||
export class Scheduler {
|
||||
worker;
|
||||
pattern;
|
||||
phase;
|
||||
audioContext;
|
||||
cps = 1;
|
||||
constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) {
|
||||
this.audioContext = audioContext;
|
||||
this.worker = new ClockWorker((tick, interval) => {
|
||||
const begin = this.phase;
|
||||
const end = this.phase + interval * this.cps;
|
||||
this.phase = end;
|
||||
const haps = this.pattern.queryArc(begin, end);
|
||||
haps.forEach((e) => {
|
||||
if (typeof e.value?.cps === 'number') {
|
||||
this.setCps(e.value?.cps);
|
||||
}
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (e.context.onTrigger) {
|
||||
const ctxTime = (e.whole.begin - begin) / this.cps + this.audioContext.currentTime + latency;
|
||||
e.context.onTrigger(ctxTime, e, this.audioContext.currentTime, this.cps);
|
||||
}
|
||||
if (onEvent) {
|
||||
onEvent?.(e);
|
||||
}
|
||||
});
|
||||
}, interval);
|
||||
constructor({ audioContext, interval = 0.2, onEvent }) {
|
||||
this.worker = new ClockWorker(
|
||||
audioContext,
|
||||
(begin, end) => {
|
||||
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (onEvent) {
|
||||
onEvent?.(e);
|
||||
} else {
|
||||
console.warn('unplayable event: no audio node nor onEvent callback', e);
|
||||
}
|
||||
});
|
||||
},
|
||||
interval,
|
||||
);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
this.audioContext.resume();
|
||||
this.phase = 0;
|
||||
this.worker.start();
|
||||
}
|
||||
stop() {
|
||||
@@ -50,7 +40,4 @@ export class Scheduler {
|
||||
setPattern(pat) {
|
||||
this.pattern = pat;
|
||||
}
|
||||
setCps(cps = 1) {
|
||||
this.cps = cps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
|
||||
export var vowelFormant = {
|
||||
a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
|
||||
e: { freqs: [440, 1800, 2700, 3000, 3300], gains: [1, 0.1995, 0.1259, 0.1, 0.1], qs: [70, 80, 100, 120, 120] },
|
||||
i: { freqs: [270, 1850, 2900, 3350, 3590], gains: [1, 0.0631, 0.0631, 0.0158, 0.0158], qs: [40, 90, 100, 120, 120] },
|
||||
o: { freqs: [430, 820, 2700, 3000, 3300], gains: [1, 0.3162, 0.0501, 0.0794, 0.01995], qs: [40, 80, 100, 120, 120] },
|
||||
u: { freqs: [370, 630, 2750, 3000, 3400], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] },
|
||||
};
|
||||
if (typeof GainNode !== 'undefined') {
|
||||
class VowelNode extends GainNode {
|
||||
constructor(ac, letter) {
|
||||
super(ac);
|
||||
if (!vowelFormant[letter]) {
|
||||
throw new Error('vowel: unknown vowel ' + letter);
|
||||
}
|
||||
const { gains, qs, freqs } = vowelFormant[letter];
|
||||
const makeupGain = ac.createGain();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const gain = ac.createGain();
|
||||
gain.gain.value = gains[i];
|
||||
const filter = ac.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.Q.value = qs[i];
|
||||
filter.frequency.value = freqs[i];
|
||||
this.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(makeupGain);
|
||||
}
|
||||
makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
this.connect = (target) => makeupGain.connect(target);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
AudioContext.prototype.createVowelFilter = function (letter) {
|
||||
return new VowelNode(this, letter);
|
||||
};
|
||||
}
|
||||
+60
-404
@@ -6,15 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { fromMidi, isNote, toMidi } from '@strudel.cycles/core';
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import { loadBuffer, reverseBuffer } from './sampler.mjs';
|
||||
const { Pattern } = strudel;
|
||||
import './vowel.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
|
||||
// export const getAudioContext = () => Tone.getContext().rawContext;
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
const { Pattern, getFrequency, patternify2 } = strudel;
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
@@ -24,30 +17,9 @@ export const getAudioContext = () => {
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
let destination;
|
||||
const getDestination = () => {
|
||||
const ctx = getAudioContext();
|
||||
if (!destination) {
|
||||
destination = ctx.createGain();
|
||||
destination.connect(ctx.destination);
|
||||
}
|
||||
return destination;
|
||||
};
|
||||
const lookahead = 0.2;
|
||||
|
||||
export const panic = () => {
|
||||
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||
destination = null;
|
||||
};
|
||||
|
||||
const getFilter = (type, frequency, Q) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.Q.value = Q;
|
||||
return filter;
|
||||
};
|
||||
|
||||
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
@@ -55,384 +27,68 @@ const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||
// for some reason, using exponential ramping creates little cracklings
|
||||
/* let t = begin;
|
||||
gainNode.gain.setValueAtTime(0, t);
|
||||
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
|
||||
const sustainGain = Math.max(sustain * velocity, 0.001);
|
||||
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
|
||||
if (end - begin < attack + decay) {
|
||||
gainNode.gain.cancelAndHoldAtTime(end);
|
||||
} else {
|
||||
gainNode.gain.setValueAtTime(sustainGain, end);
|
||||
}
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
const getOscillator = ({ s, freq, t, duration, release }) => {
|
||||
// make oscillator
|
||||
const o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
o.stop(t + duration + release);
|
||||
return o;
|
||||
};
|
||||
|
||||
const getSoundfontKey = (s) => {
|
||||
if (!globalThis.soundfontList) {
|
||||
// soundfont package not loaded
|
||||
return false;
|
||||
}
|
||||
if (globalThis.soundfontList?.instruments?.includes(s)) {
|
||||
return s;
|
||||
}
|
||||
// check if s is one of the soundfonts, which are loaded into globalThis, to avoid coupling both packages
|
||||
const nameIndex = globalThis.soundfontList?.instrumentNames?.indexOf(s);
|
||||
// convert number nameIndex (0-128) to 3 digit string (001-128)
|
||||
const name = nameIndex < 10 ? `00${nameIndex}` : nameIndex < 100 ? `0${nameIndex}` : nameIndex;
|
||||
if (nameIndex !== -1) {
|
||||
// TODO: indices of instrumentNames do not seem to match instruments
|
||||
return globalThis.soundfontList.instruments.find((instrument) => instrument.startsWith(name));
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const getSampleBufferSource = async (s, n, note, speed) => {
|
||||
let transpose = 0;
|
||||
let midi;
|
||||
|
||||
if (note !== undefined) {
|
||||
midi = typeof note === 'string' ? toMidi(note) : note;
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
}
|
||||
|
||||
const ac = getAudioContext();
|
||||
// is sample from loaded samples(..)
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples) {
|
||||
throw new Error('no samples loaded');
|
||||
}
|
||||
const bank = samples?.[s];
|
||||
if (!bank) {
|
||||
throw new Error(
|
||||
`sample not found: "${s}", try one of ${Object.keys(samples)
|
||||
.map((s) => `"${s}"`)
|
||||
.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
if (typeof bank !== 'object') {
|
||||
throw new Error('wrong format for sample bank:', s);
|
||||
}
|
||||
let sampleUrl;
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = bank[n % bank.length];
|
||||
} else {
|
||||
if (!note) {
|
||||
throw new Error('no note(...) set for sound', s);
|
||||
}
|
||||
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
.reduce(
|
||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||
null,
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
sampleUrl = bank[closest][n % bank[closest].length];
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac);
|
||||
if (speed < 0) {
|
||||
// should this be cached?
|
||||
buffer = reverseBuffer(buffer);
|
||||
}
|
||||
const bufferSource = ac.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
return bufferSource;
|
||||
};
|
||||
|
||||
const splitSN = (s, n) => {
|
||||
if (!s.includes(':')) {
|
||||
return [s, n];
|
||||
}
|
||||
let [s2, n2] = s.split(':');
|
||||
if (isNaN(Number(n2))) {
|
||||
return [s, n];
|
||||
}
|
||||
return [s2, n2];
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (workletsLoading) {
|
||||
return workletsLoading;
|
||||
}
|
||||
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
function getWorklet(ac, processor, params) {
|
||||
const node = new AudioWorkletNode(ac, processor);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
node.parameters.get(key).value = value;
|
||||
Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withHap((hap) => {
|
||||
return hap.setContext({
|
||||
...hap.context,
|
||||
createAudioNode: (t, e) => createAudioNode(t, e, hap.context.createAudioNode?.(t, hap)),
|
||||
});
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
loadWorklets();
|
||||
} catch (err) {
|
||||
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
||||
}
|
||||
}
|
||||
|
||||
function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
node.gain.value = value;
|
||||
return node;
|
||||
}
|
||||
const cutGroups = [];
|
||||
|
||||
let delays = {};
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (!delays[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start(t);
|
||||
dly.connect(getDestination());
|
||||
delays[orbit] = dly;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||
return delays[orbit];
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
function getReverb(orbit, duration = 2) {
|
||||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration);
|
||||
reverb.connect(getDestination());
|
||||
reverbs[orbit] = reverb;
|
||||
}
|
||||
if (reverbs[orbit].duration !== duration) {
|
||||
reverbs[orbit] = reverbs[orbit].setDuration(duration);
|
||||
reverbs[orbit].duration = duration;
|
||||
}
|
||||
return reverbs[orbit];
|
||||
}
|
||||
|
||||
function effectSend(input, effect, wet) {
|
||||
const send = gainNode(wet);
|
||||
input.connect(send);
|
||||
send.connect(effect);
|
||||
return send;
|
||||
}
|
||||
|
||||
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
try {
|
||||
const ac = getAudioContext();
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
hap.value = { note: hap.value };
|
||||
} */
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error(
|
||||
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||
);
|
||||
}
|
||||
// calculate correct time (tone.js workaround)
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure value
|
||||
let {
|
||||
freq,
|
||||
s,
|
||||
sf,
|
||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
gain = 0.8,
|
||||
cutoff,
|
||||
resonance = 1,
|
||||
hcutoff,
|
||||
hresonance = 1,
|
||||
bandf,
|
||||
bandq = 1,
|
||||
coarse,
|
||||
crush,
|
||||
shape,
|
||||
pan,
|
||||
speed = 1, // sample playback speed
|
||||
begin = 0,
|
||||
end = 1,
|
||||
vowel,
|
||||
delay = 0,
|
||||
delayfeedback = 0.5,
|
||||
delaytime = 0.25,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
roomsize = size,
|
||||
} = hap.value;
|
||||
const { velocity = 1 } = hap.context;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
// the chain will hold all audio nodes that connect to each other
|
||||
const chain = [];
|
||||
if (typeof s === 'string') {
|
||||
[s, n] = splitSN(s, n);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
[note, n] = splitSN(note, n);
|
||||
}
|
||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
|
||||
// with synths, n and note are the same thing
|
||||
n = note || n || 36;
|
||||
if (typeof n === 'string') {
|
||||
n = toMidi(n); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = fromMidi(n); // + 48);
|
||||
}
|
||||
// make oscillator
|
||||
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
|
||||
chain.push(o);
|
||||
// level down oscillators as they are really loud compared to samples i've tested
|
||||
chain.push(gainNode(0.3));
|
||||
// TODO: make adsr work with samples without pops
|
||||
// envelope
|
||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
|
||||
chain.push(adsr);
|
||||
} else {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
if (!s) {
|
||||
console.warn('no sample specified');
|
||||
return;
|
||||
}
|
||||
const soundfont = getSoundfontKey(s);
|
||||
let bufferSource;
|
||||
|
||||
try {
|
||||
if (soundfont) {
|
||||
// is soundfont
|
||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
||||
} else {
|
||||
// is sample from loaded samples(..)
|
||||
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
return;
|
||||
}
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
if (!bufferSource) {
|
||||
console.warn('no buffer source');
|
||||
return;
|
||||
}
|
||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||
if (unit === 'c') {
|
||||
// are there other units?
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
|
||||
}
|
||||
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * duration * bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * duration;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = offset;
|
||||
bufferSource.loopEnd = offset + duration;
|
||||
duration = loop * duration;
|
||||
}
|
||||
t += nudge;
|
||||
|
||||
bufferSource.start(t, offset);
|
||||
if (cut !== undefined) {
|
||||
cutGroups[cut]?.stop(t); // fade out?
|
||||
cutGroups[cut] = bufferSource;
|
||||
}
|
||||
chain.push(bufferSource);
|
||||
bufferSource.stop(t + duration + release);
|
||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
||||
chain.push(adsr);
|
||||
}
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
// filters
|
||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
||||
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = gainNode(1);
|
||||
chain.push(post);
|
||||
post.connect(getDestination());
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
if (room > 0 && roomsize > 0) {
|
||||
const reverbNode = getReverb(orbit, roomsize);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// disconnect all nodes when source node has ended:
|
||||
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
|
||||
} catch (e) {
|
||||
console.warn('.out error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
|
||||
Pattern.prototype._wave = function (type) {
|
||||
return this.withAudioNode((t, e) => {
|
||||
const osc = getAudioContext().createOscillator();
|
||||
osc.type = type;
|
||||
const f = getFrequency(e);
|
||||
osc.frequency.value = f; // expects frequency..
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration.valueOf();
|
||||
osc.start(begin);
|
||||
osc.stop(end); // release?
|
||||
return osc;
|
||||
});
|
||||
};
|
||||
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration.valueOf() + lookahead;
|
||||
const envelope = adsr(a, d, s, r, velocity, begin, end);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) {
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
node?.connect(filter);
|
||||
return filter;
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.filter = function (type, frequency) {
|
||||
return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this);
|
||||
};
|
||||
|
||||
Pattern.prototype.out = function () {
|
||||
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||
return this.onTrigger(webaudioOutputTrigger);
|
||||
const master = getAudioContext().createGain();
|
||||
master.gain.value = 0.1;
|
||||
master.connect(getAudioContext().destination);
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
if (!node) {
|
||||
console.warn('out: no source! call .osc() first');
|
||||
}
|
||||
node?.connect(master);
|
||||
})._withHap((hap) => {
|
||||
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
||||
// all the credit goes to dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
||||
// <3
|
||||
|
||||
class CoarseProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{ name: 'coarse', defaultValue: 1 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.notStarted = true;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const coarse = parameters.coarse;
|
||||
const blockSize = 128;
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (hasInput) {
|
||||
this.notStarted = false;
|
||||
output[0][0] = input[0][0];
|
||||
for (let n = 1; n < blockSize; n++) {
|
||||
if (n % coarse == 0) output[0][n] = input[0][n];
|
||||
else output[0][n] = output[0][n - 1];
|
||||
}
|
||||
}
|
||||
return this.notStarted || hasInput;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('coarse-processor', CoarseProcessor);
|
||||
|
||||
class CrushProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{ name: 'crush', defaultValue: 0 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.notStarted = true;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const crush = parameters.crush;
|
||||
const blockSize = 128;
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (hasInput) {
|
||||
this.notStarted = false;
|
||||
if (crush.length === 1) {
|
||||
const x = Math.pow(2, crush[0] - 1);
|
||||
for (let n = 0; n < blockSize; n++) output[0][n] = Math.round(input[0][n] * x) / x;
|
||||
} else {
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
let x = Math.pow(2, crush[n] - 1);
|
||||
output[0][n] = Math.round(input[0][n] * x) / x;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.notStarted || hasInput;
|
||||
}
|
||||
}
|
||||
registerProcessor('crush-processor', CrushProcessor);
|
||||
|
||||
class ShapeProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{ name: 'shape', defaultValue: 0 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.notStarted = true;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const shape0 = parameters.shape[0];
|
||||
const shape1 = shape0 < 1 ? shape0 : 1.0 - 4e-10;
|
||||
const shape = (2.0 * shape1) / (1.0 - shape1);
|
||||
const blockSize = 128;
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (hasInput) {
|
||||
this.notStarted = false;
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
output[0][n] = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n]));
|
||||
}
|
||||
}
|
||||
return this.notStarted || hasInput;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('shape-processor', ShapeProcessor);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './webdirt.mjs';
|
||||
export * from './sampler.mjs';
|
||||
|
||||
Generated
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
}
|
||||
},
|
||||
"node_modules/WebDirt": {
|
||||
"name": "webdirt",
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
|
||||
"license": "ISC"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"WebDirt": {
|
||||
"version": "git+ssh://git@github.com/dktr0/WebDirt.git#425dc8fd023440d9c61ffdb8642e44e2710faea0",
|
||||
"from": "WebDirt@github:dktr0/WebDirt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
export const getCachedBuffer = (url) => bufferCache[url];
|
||||
|
||||
export const loadBuffer = (url, ac) => {
|
||||
if (!loadCache[url]) {
|
||||
loadCache[url] = fetch(url)
|
||||
@@ -16,15 +14,6 @@ export const loadBuffer = (url, ac) => {
|
||||
return loadCache[url];
|
||||
};
|
||||
|
||||
export function reverseBuffer(buffer) {
|
||||
const ac = getAudioContext();
|
||||
const reversed = ac.createBuffer(buffer.numberOfChannels, buffer.length, ac.sampleRate);
|
||||
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||
reversed.copyToChannel(buffer.getChannelData(channel).slice().reverse(), channel, channel);
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
export const getLoadedBuffer = (url) => {
|
||||
return bufferCache[url];
|
||||
};
|
||||
@@ -91,43 +80,27 @@ export const loadGithubSamples = async (path, nameFn) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* load the given sample map for webdirt
|
||||
*
|
||||
* @example
|
||||
* samples({
|
||||
* bd: '808bd/BD0000.WAV',
|
||||
* sd: '808sd/SD0010.WAV'
|
||||
* loadSamples({
|
||||
* bd: '808bd/BD0000.WAV',
|
||||
* sd: ['808sd/SD0000.WAV','808sd/SD0010.WAV','808sd/SD0050.WAV']
|
||||
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
||||
* s("bd <sd!7 sd(3,4,2)>").n(2).webdirt()
|
||||
*
|
||||
*/
|
||||
|
||||
export const samples = (sampleMap, baseUrl = sampleMap._base || '') => {
|
||||
export const samples = (sampleMap, baseUrl = '') => {
|
||||
sampleCache.current = {
|
||||
...sampleCache.current,
|
||||
...Object.fromEntries(
|
||||
Object.entries(sampleMap).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
value = [value];
|
||||
}
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('wrong sample map format for ' + key);
|
||||
}
|
||||
baseUrl = value._base || baseUrl;
|
||||
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
|
||||
if (Array.isArray(value)) {
|
||||
return [key, value.map(replaceUrl)];
|
||||
}
|
||||
// must be object
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(value).map(([note, samples]) => {
|
||||
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
|
||||
}),
|
||||
),
|
||||
];
|
||||
}),
|
||||
Object.entries(sampleMap).map(([key, value]) => [
|
||||
key,
|
||||
(typeof value === 'string' ? [value] : value).map((v) =>
|
||||
(baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { Pattern } = strudel;
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from './sampler.mjs';
|
||||
|
||||
let webDirt;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,6 +24,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0"
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import { edo } from '../xen.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('xen', () => {
|
||||
it('edo', () => {
|
||||
expect(edo('3edo')).toEqual([1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
|
||||
assert.deepStrictEqual(edo('3edo'), [1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
{
|
||||
"_info": "Auto-generated by pandoc-url2cite. Feel free to modify, keys will never be overwritten.",
|
||||
"urls": {
|
||||
"https://www.tandfonline.com/doi/abs/10.1080/14794713.2016.1227602?journalCode=rpdm20": {
|
||||
"fetched": "2022-03-24T12:17:27.481Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@article{roberts_code_2016,",
|
||||
" title = {Code as information and code as spectacle},",
|
||||
" volume = {12},",
|
||||
" issn = {1479-4713},",
|
||||
" url = {https://doi.org/10.1080/14794713.2016.1227602},",
|
||||
" doi = {10.1080/14794713.2016.1227602},",
|
||||
" abstract = {In this artist statement, I will discuss the tension between source code as an interactive system for performers and source code as information and entertainment for audiences in live-coding performances. I then describe augmentations I developed for the presentation of source code in the live-coding environment Gibber, including animations and annotations that visually reveal aspects of system state during performances. I briefly describe audience responses to these techniques and, more importantly, how they are critical to my own artistic practice.},",
|
||||
" number = {2},",
|
||||
" urldate = {2022-03-24},",
|
||||
" journal = {International Journal of Performance Arts and Digital Media},",
|
||||
" author = {Roberts, Charles},",
|
||||
" month = jul,",
|
||||
" year = {2016},",
|
||||
" keywords = {Live coding, psychology of programming, notation, audiences, algorithms},",
|
||||
" pages = {201--206},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"DOI": "10.1080/14794713.2016.1227602",
|
||||
"ISSN": "1479-4713",
|
||||
"URL": "https://doi.org/10.1080/14794713.2016.1227602",
|
||||
"abstract": "In this artist statement, I will discuss the tension between source code as an interactive system for performers and source code as information and entertainment for audiences in live-coding performances. I then describe augmentations I developed for the presentation of source code in the live-coding environment Gibber, including animations and annotations that visually reveal aspects of system state during performances. I briefly describe audience responses to these techniques and, more importantly, how they are critical to my own artistic practice.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
3,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
}
|
||||
],
|
||||
"container-title": "International Journal of Performance Arts and Digital Media",
|
||||
"id": "https://www.tandfonline.com/doi/abs/10.1080/14794713.2016.1227602?journalCode_x61_rpdm20",
|
||||
"issue": "2",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2016,
|
||||
7
|
||||
]
|
||||
]
|
||||
},
|
||||
"keyword": "Live coding, psychology of programming, notation, audiences, algorithms",
|
||||
"page": "201-206",
|
||||
"title": "Code as information and code as spectacle",
|
||||
"type": "article-journal",
|
||||
"volume": "12"
|
||||
}
|
||||
},
|
||||
"https://zenodo.org/record/5788732": {
|
||||
"fetched": "2022-03-24T12:20:17.218Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{mclean_alternate_2021,",
|
||||
" address = {Valdivia, Chile},",
|
||||
" title = {Alternate {Timelines} for {TidalCycles}},",
|
||||
" url = {https://zenodo.org/record/5788732},",
|
||||
" abstract = {The TidalCycles (or Tidal for short) live coding environment has been developed since around 2009, via several rewrites of its core representation. Rather than having fixed goals, this development has been guided by use, motivated by the open aim to make music. This development process can be seen as a long-form improvisation, with insights into the nature of Tidal gained through the process of writing it, feeding back to guide the next steps of development. This brings the worrying thought that key insights will have been missed along this development journey, that would otherwise have lead to very different software. Indeed participants at beginners’ workshops that I have lead or co-lead have often asked questions without good answers, because they made deficiencies or missing features in the software clear. It is well known that a beginner’s mind is able to see much that an expert has become blind to. Running workshops are an excellent way to find new development ideas, but the present paper explores a different technique – the rewrite.},",
|
||||
" urldate = {2022-03-24},",
|
||||
" collaborator = {McLean, Alex},",
|
||||
" month = dec,",
|
||||
" year = {2021},",
|
||||
" keywords = {live coding, algorithmic pattern, tidalcycles, haskell, python},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://zenodo.org/record/5788732",
|
||||
"abstract": "The TidalCycles (or Tidal for short) live coding environment has been developed since around 2009, via several rewrites of its core representation. Rather than having fixed goals, this development has been guided by use, motivated by the open aim to make music. This development process can be seen as a long-form improvisation, with insights into the nature of Tidal gained through the process of writing it, feeding back to guide the next steps of development. This brings the worrying thought that key insights will have been missed along this development journey, that would otherwise have lead to very different software. Indeed participants at beginners’ workshops that I have lead or co-lead have often asked questions without good answers, because they made deficiencies or missing features in the software clear. It is well known that a beginner’s mind is able to see much that an expert has become blind to. Running workshops are an excellent way to find new development ideas, but the present paper explores a different technique – the rewrite.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
3,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "https://zenodo.org/record/5788732",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2021,
|
||||
12
|
||||
]
|
||||
]
|
||||
},
|
||||
"keyword": "live coding, algorithmic pattern, tidalcycles, haskell, python",
|
||||
"publisher-place": "Valdivia, Chile",
|
||||
"title": "Alternate Timelines for TidalCycles",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://www.semanticscholar.org/paper/Bringing-the-TidalCycles-Mini-Notation-to-the-Roberts/74965efadd572ae3f40d14c633a5c8581c1b9f42": {
|
||||
"fetched": "2022-04-12T22:47:17.063Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{roberts_bringing_2019,",
|
||||
" title = {Bringing the {TidalCycles} {Mini}-{Notation} to the {Browser}},",
|
||||
" url = {https://www.semanticscholar.org/paper/Bringing-the-TidalCycles-Mini-Notation-to-the-Roberts/74965efadd572ae3f40d14c633a5c8581c1b9f42},",
|
||||
" abstract = {A JavaScript dialect of its mini-notation for pattern is created, enabling easy integration with creative coding tools and an accompanying technique for visually annotating the playback of TidalCycles patterns over time. TidalCycles has rapidly become the most popular system for many styles of live coding performance, in particular Algoraves. We created a JavaScript dialect of its mini-notation for pattern, enabling easy integration with creative coding tools. Our research pairs a formalism describing the mini-notation with a small JavaScript library for generating events over time; this library is suitable for generating events inside of an AudioWorkletProcessor thread and for assisting with scheduling in JavaScript environments more generally. We describe integrating the library into the two live coding systems, Gibber and Hydra, and discuss an accompanying technique for visually annotating the playback of TidalCycles patterns over time.},",
|
||||
" language = {en},",
|
||||
" urldate = {2022-04-12},",
|
||||
" journal = {www.semanticscholar.org},",
|
||||
" author = {Roberts, Charles},",
|
||||
" year = {2019},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://www.semanticscholar.org/paper/Bringing-the-TidalCycles-Mini-Notation-to-the-Roberts/74965efadd572ae3f40d14c633a5c8581c1b9f42",
|
||||
"abstract": "A JavaScript dialect of its mini-notation for pattern is created, enabling easy integration with creative coding tools and an accompanying technique for visually annotating the playback of TidalCycles patterns over time. TidalCycles has rapidly become the most popular system for many styles of live coding performance, in particular Algoraves. We created a JavaScript dialect of its mini-notation for pattern, enabling easy integration with creative coding tools. Our research pairs a formalism describing the mini-notation with a small JavaScript library for generating events over time; this library is suitable for generating events inside of an AudioWorkletProcessor thread and for assisting with scheduling in JavaScript environments more generally. We describe integrating the library into the two live coding systems, Gibber and Hydra, and discuss an accompanying technique for visually annotating the playback of TidalCycles patterns over time.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
12
|
||||
]
|
||||
]
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
}
|
||||
],
|
||||
"container-title": "www.semanticscholar.org",
|
||||
"id": "https://www.semanticscholar.org/paper/Bringing-the-TidalCycles-Mini-Notation-to-the-Roberts/74965efadd572ae3f40d14c633a5c8581c1b9f42",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2019
|
||||
]
|
||||
]
|
||||
},
|
||||
"title": "Bringing the TidalCycles Mini-Notation to the Browser",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://zenodo.org/record/6456380": {
|
||||
"fetched": "2022-04-14T21:26:21.302Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{mclean_tidalvortex_2022,",
|
||||
" address = {Limerick, Ireland},",
|
||||
" title = {{TidalVortex} {Zero}},",
|
||||
" url = {https://zenodo.org/record/6456380},",
|
||||
" abstract = {In this paper we introduce ‘version zero’ of TidalVortex, an alternative implementation of the TidalCycles live coding system, using the Python programming language. This is open-ended work, exploring what happens when we try to extract the 'essence' of a system like TidalCycles and translate it into another programming language, while taking advantage of the affordance of its new host. First, we review the substantial prior art in porting TidalCycles, and in representing musical patterns in Python. We then compare equivalent patterns written in Haskell (TidalCycles) and Python (TidalVortex), and relate implementation details of how functional reactive paradigms have translated from the pure functional, strongly typed Haskell to the more multi-paradigm, dynamically typed Python. Finally, we conclude with reflections and generalisable outcomes.},",
|
||||
" urldate = {2022-04-14},",
|
||||
" collaborator = {McLean, Alex and Forment, Raphaël and Le Beux, Sylvain and Silvani, Damián},",
|
||||
" month = apr,",
|
||||
" year = {2022},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://zenodo.org/record/6456380",
|
||||
"abstract": "In this paper we introduce “version zero” of TidalVortex, an alternative implementation of the TidalCycles live coding system, using the Python programming language. This is open-ended work, exploring what happens when we try to extract the ’essence’ of a system like TidalCycles and translate it into another programming language, while taking advantage of the affordance of its new host. First, we review the substantial prior art in porting TidalCycles, and in representing musical patterns in Python. We then compare equivalent patterns written in Haskell (TidalCycles) and Python (TidalVortex), and relate implementation details of how functional reactive paradigms have translated from the pure functional, strongly typed Haskell to the more multi-paradigm, dynamically typed Python. Finally, we conclude with reflections and generalisable outcomes.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "https://zenodo.org/record/6456380",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4
|
||||
]
|
||||
]
|
||||
},
|
||||
"publisher-place": "Limerick, Ireland",
|
||||
"title": "TidalVortex Zero",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://zenodo.org/record/4299661": {
|
||||
"fetched": "2022-04-15T07:40:08.702Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{mclean_algorithmic_2020,",
|
||||
" address = {Birmingham UK},",
|
||||
" title = {Algorithmic {Pattern}},",
|
||||
" url = {https://zenodo.org/record/4299661},",
|
||||
" abstract = {This paper brings together two main perspectives on algorithmic pattern. First, the writing of musical patterns in live coding performance, and second, the weaving of patterns in textiles. In both cases, algorithmic pattern is an interface between the human and the outcome, where small changes have far-reaching impact on the results. By bringing contemporary live coding and ancient textile approaches together, we reach a common view of pattern as algorithmic movement (e.g. looping, shifting, reflecting, interfering) in the making of things. This works beyond the usual definition of pattern used in musical interfaces, of mere repeating sequences. We conclude by considering the place of algorithmic pattern in a wider activity of making.},",
|
||||
" urldate = {2022-04-15},",
|
||||
" collaborator = {McLean, Alex},",
|
||||
" month = jul,",
|
||||
" year = {2020},",
|
||||
" keywords = {pattern, tidalcycles, algorithmic music, textiles, live coding, algorave},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://zenodo.org/record/4299661",
|
||||
"abstract": "This paper brings together two main perspectives on algorithmic pattern. First, the writing of musical patterns in live coding performance, and second, the weaving of patterns in textiles. In both cases, algorithmic pattern is an interface between the human and the outcome, where small changes have far-reaching impact on the results. By bringing contemporary live coding and ancient textile approaches together, we reach a common view of pattern as algorithmic movement (e.g. looping, shifting, reflecting, interfering) in the making of things. This works beyond the usual definition of pattern used in musical interfaces, of mere repeating sequences. We conclude by considering the place of algorithmic pattern in a wider activity of making.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
15
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "https://zenodo.org/record/4299661",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2020,
|
||||
7
|
||||
]
|
||||
]
|
||||
},
|
||||
"keyword": "pattern, tidalcycles, algorithmic music, textiles, live coding, algorave",
|
||||
"publisher-place": "Birmingham UK",
|
||||
"title": "Algorithmic Pattern",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://quod.lib.umich.edu/i/icmc/bbp2372.2012.011/2/–gibber-live-coding-audio-in-the-browser?page=root;size=150;view=text": {
|
||||
"fetched": "2022-04-15T07:40:15.037Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@article{charlie_gibber:_2012,",
|
||||
" title = {{GIBBER}: {LIVE} {CODING} {AUDIO} {IN} {THE} {BROWSER}},",
|
||||
" volume = {2012},",
|
||||
" issn = {2223-3881},",
|
||||
" url = {https://quod.lib.umich.edu/i/icmc/bbp2372.2012.011/2/%E2%80%93gibber-live-coding-audio-in-the-browser?page=root;size=150;view=text},",
|
||||
" language = {en},",
|
||||
" urldate = {2022-04-15},",
|
||||
" journal = {International Computer Music Conference Proceedings},",
|
||||
" author = {Charlie, , Roberts and Joann, , Kuchera-Morin},",
|
||||
" year = {2012},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"ISSN": "2223-3881",
|
||||
"URL": "https://quod.lib.umich.edu/i/icmc/bbp2372.2012.011/2/%E2%80%93gibber-live-coding-audio-in-the-browser?page=root;size=150;view=text",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
15
|
||||
]
|
||||
]
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"family": "Charlie",
|
||||
"given": "Roberts"
|
||||
},
|
||||
{
|
||||
"family": "Joann",
|
||||
"given": "Kuchera-Morin"
|
||||
}
|
||||
],
|
||||
"container-title": "International Computer Music Conference Proceedings",
|
||||
"id": "https://quod.lib.umich.edu/i/icmc/bbp2372.2012.011/2/–gibber-live-coding-audio-in-the-browser?page_x61_root;size_x61_150;view_x61_text",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2012
|
||||
]
|
||||
]
|
||||
},
|
||||
"title": "GIBBER: LIVE CODING AUDIO IN THE BROWSER",
|
||||
"title-short": "GIBBER",
|
||||
"type": "article-journal",
|
||||
"volume": "2012"
|
||||
}
|
||||
},
|
||||
"https://www.semanticscholar.org/paper/Estuary%3A-Browser-based-Collaborative-Projectional-Ogborn-Beverley/c6b5d34575d6230dfd8751ca4af8e5f6e44d916b": {
|
||||
"fetched": "2022-04-15T07:40:17.179Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{ogborn_estuary:_2017,",
|
||||
" title = {Estuary: {Browser}-based {Collaborative} {Projectional} {Live} {Coding} of {Musical} {Patterns}},",
|
||||
" shorttitle = {Estuary},",
|
||||
" url = {https://www.semanticscholar.org/paper/Estuary%3A-Browser-based-Collaborative-Projectional-Ogborn-Beverley/c6b5d34575d6230dfd8751ca4af8e5f6e44d916b},",
|
||||
" abstract = {Estuary is a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern that includes a strict form of structure editing, a click-only border-free approach to interface design, and explicit notations to modulate the liveness of different parts of the code. This paper describes the initial design and development of Estuary, a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern. Key features of Estuary include a strict form of structure editing (making syntactical errors impossible), a click-only border-free approach to interface design, explicit notations to modulate the liveness of different parts of the code, and a server-based network collaboration system that can be used for many simultaneous collaborative live coding performances, as well as to present different views of the same live coding activity. Estuary has been developed using Reflex-DOM, a Haskell-based framework for web development whose strictness promises robustness and security advantages.},",
|
||||
" language = {en},",
|
||||
" urldate = {2022-04-15},",
|
||||
" journal = {www.semanticscholar.org},",
|
||||
" author = {Ogborn, David and Beverley, J.},",
|
||||
" year = {2017},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://www.semanticscholar.org/paper/Estuary%3A-Browser-based-Collaborative-Projectional-Ogborn-Beverley/c6b5d34575d6230dfd8751ca4af8e5f6e44d916b",
|
||||
"abstract": "Estuary is a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern that includes a strict form of structure editing, a click-only border-free approach to interface design, and explicit notations to modulate the liveness of different parts of the code. This paper describes the initial design and development of Estuary, a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern. Key features of Estuary include a strict form of structure editing (making syntactical errors impossible), a click-only border-free approach to interface design, explicit notations to modulate the liveness of different parts of the code, and a server-based network collaboration system that can be used for many simultaneous collaborative live coding performances, as well as to present different views of the same live coding activity. Estuary has been developed using Reflex-DOM, a Haskell-based framework for web development whose strictness promises robustness and security advantages.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
15
|
||||
]
|
||||
]
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"family": "Ogborn",
|
||||
"given": "David"
|
||||
},
|
||||
{
|
||||
"family": "Beverley",
|
||||
"given": "J."
|
||||
}
|
||||
],
|
||||
"container-title": "www.semanticscholar.org",
|
||||
"id": "https://www.semanticscholar.org/paper/Estuary_x37_3A-Browser-based-Collaborative-Projectional-Ogborn-Beverley/c6b5d34575d6230dfd8751ca4af8e5f6e44d916b",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2017
|
||||
]
|
||||
]
|
||||
},
|
||||
"title": "Estuary: Browser-based Collaborative Projectional Live Coding of Musical Patterns",
|
||||
"title-short": "Estuary",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://zenodo.org/record/6353969": {
|
||||
"fetched": "2022-04-15T07:40:20.966Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{mclean_feedforward_2020,",
|
||||
" address = {Birmingham},",
|
||||
" title = {Feedforward},",
|
||||
" url = {https://zenodo.org/record/6353969},",
|
||||
" abstract = {This is an improvised, from-scratch live coding performance. The NIME interface which this performance showcases is the new Feedfoward editor for the TidalCycles live coding environment. Feedforward is written in Haskell using the ncurses library for terminal-based user interfaces. It runs on low-powered hardware including the Raspberry Pi Zero, with formative testing of prototypes conducted with several groups of children between the ages of 8 and 14. Feedforward has a number of features designed to support improvised, multi-pattern live coding. Individual Tidal patterns are addressable with hotkeys for fast mute and unmuting. Each pattern has a stereo VU meter, to aid the quick matching of sound to pattern within a mix. In addition, TidalCycles has been extended to store context with each event, so that source code positions in its polyrhythmic sequence mini-notation are tracked. This allows steps to be highlighted in the source code when- ever they are active. This works even when Tidal combinators have been applied to manipulate the timeline. Formal evaluation has yet to take place, but this feature appears to support learning of how pattern manipulations work in Tidal. Feedforward and TidalCycles is free/open source software under a GPL licence version 3.0.},",
|
||||
" urldate = {2022-04-15},",
|
||||
" collaborator = {McLean, Alex},",
|
||||
" month = jul,",
|
||||
" year = {2020},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://zenodo.org/record/6353969",
|
||||
"abstract": "This is an improvised, from-scratch live coding performance. The NIME interface which this performance showcases is the new Feedfoward editor for the TidalCycles live coding environment. Feedforward is written in Haskell using the ncurses library for terminal-based user interfaces. It runs on low-powered hardware including the Raspberry Pi Zero, with formative testing of prototypes conducted with several groups of children between the ages of 8 and 14. Feedforward has a number of features designed to support improvised, multi-pattern live coding. Individual Tidal patterns are addressable with hotkeys for fast mute and unmuting. Each pattern has a stereo VU meter, to aid the quick matching of sound to pattern within a mix. In addition, TidalCycles has been extended to store context with each event, so that source code positions in its polyrhythmic sequence mini-notation are tracked. This allows steps to be highlighted in the source code when- ever they are active. This works even when Tidal combinators have been applied to manipulate the timeline. Formal evaluation has yet to take place, but this feature appears to support learning of how pattern manipulations work in Tidal. Feedforward and TidalCycles is free/open source software under a GPL licence version 3.0.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
15
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "https://zenodo.org/record/6353969",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2020,
|
||||
7
|
||||
]
|
||||
]
|
||||
},
|
||||
"publisher-place": "Birmingham",
|
||||
"title": "Feedforward",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.72.1340": {
|
||||
"fetched": "2022-04-24T21:09:16.724Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@inproceedings{toussaint_euclidean_2005,",
|
||||
" title = {The {Euclidean} algorithm generates traditional musical rhythms},",
|
||||
" url = {https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.72.1340},",
|
||||
" abstract = {The Euclidean algorithm (which comes down to us from Euclid’s Elements) computes the greatest common divisor of two given integers. It is shown here that the structure of the Euclidean algorithm may be used to automatically generate, very efficiently, a large family of rhythms used as timelines (rhythmic ostinatos), in traditional world music. These rhythms, here dubbed Euclidean rhythms, have the property that their onset patterns are distributed as evenly as possible in a mathematically precise sense, and optimal manner. Euclidean rhythms are closely related to the family of Aksak rhythms studied by ethnomusicologists, and occur in a wide variety of other disciplines as well. For example they characterize algorithms for drawing digital straight lines in computer graphics, as well as algorithms for calculating leap years in calendar design. Euclidean rhythms also find application in nuclear physics accelerators and in computer science, and are closely related to several families of words and sequences of interest in the study of the combinatorics of words, such as mechanical words, Sturmian words, two-distance sequences, and Euclidean strings, to which the Euclidean rhythms are compared. 1.},",
|
||||
" urldate = {2022-04-24},",
|
||||
" booktitle = {In {Proceedings} of {BRIDGES}: {Mathematical} {Connections} in {Art}, {Music} and {Science}},",
|
||||
" author = {Toussaint, Godfried},",
|
||||
" year = {2005},",
|
||||
" pages = {47--56},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.72.1340",
|
||||
"abstract": "The Euclidean algorithm (which comes down to us from Euclid’s Elements) computes the greatest common divisor of two given integers. It is shown here that the structure of the Euclidean algorithm may be used to automatically generate, very efficiently, a large family of rhythms used as timelines (rhythmic ostinatos), in traditional world music. These rhythms, here dubbed Euclidean rhythms, have the property that their onset patterns are distributed as evenly as possible in a mathematically precise sense, and optimal manner. Euclidean rhythms are closely related to the family of Aksak rhythms studied by ethnomusicologists, and occur in a wide variety of other disciplines as well. For example they characterize algorithms for drawing digital straight lines in computer graphics, as well as algorithms for calculating leap years in calendar design. Euclidean rhythms also find application in nuclear physics accelerators and in computer science, and are closely related to several families of words and sequences of interest in the study of the combinatorics of words, such as mechanical words, Sturmian words, two-distance sequences, and Euclidean strings, to which the Euclidean rhythms are compared. 1.",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"family": "Toussaint",
|
||||
"given": "Godfried"
|
||||
}
|
||||
],
|
||||
"container-title": "In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science",
|
||||
"id": "https://citeseerx.ist.psu.edu/viewdoc/summary?doi_x61_10.1.1.72.1340",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2005
|
||||
]
|
||||
]
|
||||
},
|
||||
"page": "47-56",
|
||||
"title": "The Euclidean algorithm generates traditional musical rhythms",
|
||||
"type": "paper-conference"
|
||||
}
|
||||
},
|
||||
"https://webaudioconf.com/posts/2021_8/": {
|
||||
"fetched": "2022-04-24T21:14:10.409Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_wac_nodate,",
|
||||
" title = {{WAC} {\\textbar} {Glicol}: {A} {Graph}-oriented {Live} {Coding} {Language} {Developed} with {Rust}, {WebAssembly} and {AudioWorklet}},",
|
||||
" url = {https://webaudioconf.com/posts/2021_8/},",
|
||||
" urldate = {2022-04-24},",
|
||||
" journal = {webaudioconf.com},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://webaudioconf.com/posts/2021_8/",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"container-title": "webaudioconf.com",
|
||||
"id": "https://webaudioconf.com/posts/2021_8/",
|
||||
"title": "WAC Glicol: A Graph-oriented Live Coding Language Developed with Rust, WebAssembly and AudioWorklet",
|
||||
"title-short": "WAC Glicol",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://webaudioconf.com/posts/2019_38/": {
|
||||
"fetched": "2022-04-24T21:14:46.954Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_wac_nodate,",
|
||||
" title = {{WAC} {\\textbar} {FAUST} online {IDE}: dynamically compile and publish {FAUST} code as {WebAudio} {Plugins}},",
|
||||
" url = {https://webaudioconf.com/posts/2019_38/},",
|
||||
" urldate = {2022-04-24},",
|
||||
" journal = {webaudioconf.com},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://webaudioconf.com/posts/2019_38/",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"container-title": "webaudioconf.com",
|
||||
"id": "https://webaudioconf.com/posts/2019_38/",
|
||||
"title": "WAC FAUST online IDE: Dynamically compile and publish FAUST code as WebAudio Plugins",
|
||||
"title-short": "WAC FAUST online IDE",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://strudel.tidalcycles.org": {
|
||||
"fetched": "2022-04-24T21:14:47.822Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_strudel_nodate,",
|
||||
" title = {Strudel {REPL}},",
|
||||
" url = {https://strudel.tidalcycles.org/},",
|
||||
" abstract = {Strudel REPL},",
|
||||
" urldate = {2022-04-24},",
|
||||
" journal = {strudel.tidalcycles.org},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://strudel.tidalcycles.org/",
|
||||
"abstract": "Strudel REPL",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"container-title": "strudel.tidalcycles.org",
|
||||
"id": "https://strudel.tidalcycles.org",
|
||||
"title": "Strudel REPL",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://hydra.ojack.xyz/docs/#/": {
|
||||
"fetched": "2022-04-25T09:03:25.132Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_hydra_nodate,",
|
||||
" title = {Hydra},",
|
||||
" url = {https://hydra.ojack.xyz/docs/#/},",
|
||||
" abstract = {Description},",
|
||||
" urldate = {2022-04-25},",
|
||||
" journal = {hydra.ojack.xyz},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://hydra.ojack.xyz/docs/#/",
|
||||
"abstract": "Description",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
25
|
||||
]
|
||||
]
|
||||
},
|
||||
"container-title": "hydra.ojack.xyz",
|
||||
"id": "https://hydra.ojack.xyz/docs/_x35_/",
|
||||
"title": "Hydra",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://mikesol.github.io/purescript-wags/": {
|
||||
"fetched": "2022-04-25T09:03:26.456Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_wags_nodate,",
|
||||
" title = {Wags documentation},",
|
||||
" url = {https://mikesol.github.io/purescript-wags/},",
|
||||
" urldate = {2022-04-25},",
|
||||
" journal = {mikesol.github.io},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://mikesol.github.io/purescript-wags/",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
25
|
||||
]
|
||||
]
|
||||
},
|
||||
"container-title": "mikesol.github.io",
|
||||
"id": "https://mikesol.github.io/purescript-wags/",
|
||||
"title": "Wags documentation",
|
||||
"type": ""
|
||||
}
|
||||
},
|
||||
"https://github.com/tidalcycles/strudel": {
|
||||
"fetched": "2022-04-25T09:15:32.518Z",
|
||||
"bibtex": [
|
||||
"",
|
||||
"@misc{noauthor_strudel_2022,",
|
||||
" title = {strudel},",
|
||||
" copyright = {GPL-3.0},",
|
||||
" url = {https://github.com/tidalcycles/strudel},",
|
||||
" abstract = {Experimental port of tidalcycles to Javascript},",
|
||||
" urldate = {2022-04-25},",
|
||||
" publisher = {TidalCycles},",
|
||||
" month = apr,",
|
||||
" year = {2022},",
|
||||
" note = {original-date: 2022-01-22T20:24:35Z},",
|
||||
" keywords = {javascript, livecoding, tidal, tidalcycles, algorave, algorithmic-patterns},",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"csl": {
|
||||
"URL": "https://github.com/tidalcycles/strudel",
|
||||
"abstract": "Experimental port of tidalcycles to Javascript",
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4,
|
||||
25
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "https://github.com/tidalcycles/strudel",
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
2022,
|
||||
4
|
||||
]
|
||||
]
|
||||
},
|
||||
"keyword": "javascript, livecoding, tidal, tidalcycles, algorave, algorithmic-patterns",
|
||||
"note": "original-date: 2022-01-22T20:24:35Z",
|
||||
"publisher": "TidalCycles",
|
||||
"title": "Strudel",
|
||||
"type": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user