Compare commits

..

66 Commits

Author SHA1 Message Date
Felix Roos 3abe6db3de Publish
- @strudel.cycles/core@0.7.0
 - @strudel.cycles/csound@0.7.0
 - @strudel.cycles/eval@0.7.0
 - @strudel.cycles/midi@0.7.0
 - @strudel.cycles/mini@0.7.0
 - @strudel.cycles/osc@0.7.0
 - @strudel.cycles/react@0.7.0
 - @strudel.cycles/serial@0.7.0
 - @strudel.cycles/soundfonts@0.7.0
 - @strudel.cycles/tonal@0.7.0
 - @strudel.cycles/tone@0.7.0
 - @strudel.cycles/transpiler@0.7.0
 - @strudel.cycles/webaudio@0.7.0
 - @strudel.cycles/webdirt@0.7.0
 - @strudel.cycles/xen@0.7.0
2023-03-23 11:21:02 +01:00
Felix Roos b9ca9d3b06 update lerna 2023-03-23 11:17:21 +01:00
Felix Roos 9bbc04843d Merge pull request #534 from tidalcycles/soundfont-freq-support
feat: add freq support to gm soundfonts
2023-03-23 10:21:55 +01:00
Felix Roos ddf61a6438 Merge pull request #531 from ijc8/main
Maintain random seed state in parser, not globally
2023-03-23 10:18:58 +01:00
Felix Roos ba35a81e9b - feat: add freq support to gm soundfonts
- refactor: toMidi -> noteToMidi
- refactor: fromMidi -> midiToFreq
2023-03-23 10:18:24 +01:00
Felix Roos 13133583ca Merge pull request #527 from bwagner/main
FIXES: alias pm for polymeter
2023-03-21 22:35:17 +01:00
Felix Roos e91b981740 Merge pull request #529 from revolunet/patch-1
fix(footer): fix link to tidalcycles
2023-03-21 22:29:06 +01:00
Bernhard Wagner 1458ee64ec FIXES: alias pm for polymeter 2023-03-21 21:20:57 +01:00
Ian Clester 4e2a5e1663 Add pairwise independence test, tweak randOffset 2023-03-20 22:27:35 -04:00
Ian Clester c98b0e9687 Format code with prettier 2023-03-20 19:19:42 -04:00
Ian Clester 4e25990de9 Uncomment test for random choice operator (|) 2023-03-20 17:46:57 -04:00
Ian Clester 4b51f1a3fc Sync up ? with degrade() 2023-03-20 15:26:27 -04:00
Ian Clester 5490b01004 Maintain random seed state in parser, not globally 2023-03-20 14:59:43 -04:00
Julien Bouquillon 43176e1f1d fix(footer): fix link to tidalcycles 2023-03-20 01:38:21 +01:00
Bernhard Wagner fffd3c07ca FIXES: alias pm for polymeter
(return statement was missing)
2023-03-18 22:12:12 +01:00
Felix Roos 952bd38e70 Merge pull request #525 from bwagner/patch-4
Update intro.mdx
2023-03-18 20:25:21 +01:00
Felix Roos 8589f5cc89 Merge pull request #524 from bwagner/patch-3
Update samples.mdx
2023-03-18 20:24:38 +01:00
Bernhard Wagner ec594d52f8 Update intro.mdx
shortly -> concisely
2023-03-18 19:31:57 +01:00
Bernhard Wagner 848cbf9c60 Update samples.mdx
fixes few typos and the former `samples` tab is now `sounds`
2023-03-18 15:37:59 +01:00
Felix Roos e02be7dd9a Merge pull request #521 from tidalcycles/fix-envelope
fix: envelopes in chrome
2023-03-17 09:03:10 +01:00
Felix Roos 33d35e47a6 fix: envelope in chrome 2023-03-17 08:56:05 +01:00
Felix Roos 18d62b32c2 Merge pull request #516 from tidalcycles/source-nodes
registerSound API + improved sounds tab + regroup soundfonts
2023-03-16 16:13:30 +01:00
Felix Roos 58ed51b49c snapshot 2023-03-16 15:49:56 +01:00
Felix Roos 9f9fe529e4 fix only tune that uses soundfonts 2023-03-16 15:49:08 +01:00
Felix Roos aad721890a pass duration to source callback 2023-03-16 15:48:59 +01:00
Felix Roos e63a5d10c8 regroup sounds
- extra tab for drum-machines
- combine synths and soundfonts
2023-03-16 15:21:16 +01:00
Felix Roos 15681262de use only _ for easier copying via double click 2023-03-16 15:19:38 +01:00
Felix Roos b5d3aa81f9 format 2023-03-16 12:25:08 +01:00
Felix Roos a688061616 group soundfonts into readable names 2023-03-16 12:23:58 +01:00
Felix Roos f788d347ec add envelope to soundfonts 2023-03-15 23:02:05 +01:00
Felix Roos a33ce67dd4 fix: endless preview 2023-03-15 23:01:56 +01:00
Felix Roos 8a22c5627a fix: cut glitches due to envelope release trouble 2023-03-15 22:42:03 +01:00
Felix Roos 7b529a30f4 style sounds tab 2023-03-14 22:54:12 +01:00
Felix Roos 4910b34f26 Merge pull request #518 from tidalcycles/fonts
add 2 illegible fonts
2023-03-14 21:54:52 +01:00
Felix Roos 6a73b43e3a add 2 illegible fonts 2023-03-12 21:16:46 +01:00
Felix Roos b59b26460d soundfont list curation part 2 2023-03-12 20:48:29 +01:00
Felix Roos aa324ae25d start curating the soundfont list 2023-03-10 11:33:55 +01:00
Felix Roos ff412f6396 level down soundfonts
+ do not load in prebake (breaks static rendering)
+ instead load on import
2023-03-09 23:19:19 +01:00
Felix Roos ac148b2f32 register soundfonts as sounds too 2023-03-09 22:48:40 +01:00
Felix Roos 6f5d096e6d rename setSound to registerSound 2023-03-09 21:32:40 +01:00
Felix Roos ff1e6714c6 document setSound api 2023-03-09 21:27:24 +01:00
Felix Roos 41d82eaadf add cleanup to sound tab 2023-03-09 10:53:35 +01:00
Felix Roos be6b011204 proper audio node cleanup
+ cleanup onTriggerSample
2023-03-09 10:46:45 +01:00
Felix Roos 65e48c05f0 sounds onset and offset can be triggered independently
+ sounds tab now supports mousedown / mouseup to listen
2023-03-09 08:35:20 +01:00
Felix Roos cee08ea67d sounds list can now filter out defaults 2023-03-08 00:33:21 +01:00
Felix Roos 35ef26c013 can now pass meta data to setSound
+ added types for synth and sample
+ list sample count for samples in sounds tab (as before)
2023-03-07 21:43:01 +01:00
Felix Roos b08a0b8102 use nanostore for soundmap
+ rename tab samples to sounds
+ listed sounds are now reactive
2023-03-06 23:11:09 +01:00
Felix Roos 220342154b Merge pull request #474 from bwagner/main
Update README.md
2023-03-06 22:55:00 +01:00
Bernhard Wagner ef7182cd76 Merge branch 'tidalcycles:main' into main 2023-03-06 20:15:16 +01:00
Felix Roos 6059c69995 first draft of soundMap to register keys for s
+ refactor sampler to use it
+ refactor synth to use it
+ add 'source' control
+ wip: samples tab
+ wip: webadirt ?
+ wip: soundfonts
2023-03-05 23:27:37 +01:00
Felix Roos 892d8ba152 Merge pull request #508 from tidalcycles/arrange
add arrange function
2023-03-05 14:52:30 +01:00
Felix Roos da11069f75 Merge pull request #514 from tidalcycles/react-update
update react to 18
2023-03-05 14:29:08 +01:00
Alex McLean bf72908dc9 Support list syntax in mininotation (#512)
fixes #504

* support list syntax in mininotation
* support compound controls
* remove redundant : splitting of s/n and note/n from webaudio
* patternable scale names
2023-03-04 18:06:18 +00:00
Felix Roos 81ab5c78e2 update react to 18 2023-03-03 20:38:20 +01:00
Felix Roos de503c7d63 add snapshot 2023-03-03 20:30:35 +01:00
Felix Roos 487ccb90a1 add arrange function 2023-03-03 20:17:23 +01:00
Felix Roos 4bfeaa47bf Merge pull request #502 from tidalcycles/scale-space-alternative
can now use : as a replacement for space in scales
2023-03-03 12:40:23 +01:00
Felix Roos 9a4d8036a6 add test 2023-03-03 12:38:11 +01:00
Felix Roos a50bd6375d use : instead of _ 2023-03-03 12:37:25 +01:00
Felix Roos 14508e91a9 can now use _ as a replacement for space in scales 2023-03-02 20:39:06 +01:00
Alex McLean 2d5064bd5b Reinstate slice and splice (#500)
Reinstate slice/splice, fixing up "unit" param using new global cps
2023-03-02 14:44:41 +00:00
Felix Roos fdc4819e39 Merge pull request #501 from tidalcycles/nano-repl-highlighting-fix
fix: nano-repl highlighting
2023-03-02 14:49:30 +01:00
Felix Roos efddbb616b fix: nano-repl highlighting 2023-03-02 14:47:15 +01:00
Alex McLean db4d0f26a8 Add control aliases (#497)
* add and simplify control aliases, fixes #496
* roomsize now alias of room
* simplify webaudio param handling
---------

Co-authored-by: Felix Roos <flix91@gmail.com>
2023-03-02 13:17:12 +00:00
Felix Roos 06cffbc31b add readme to nano repl 2023-03-01 12:57:33 +01:00
Bernhard Wagner 5506d18bc5 Update README.md
npm -> pnpm
2023-02-23 18:28:11 +01:00
76 changed files with 5386 additions and 3547 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
npm run setup
npm run repl
pnpm run setup
pnpm run repl
```
## Using Strudel In Your Project
+1 -1
View File
@@ -67,7 +67,7 @@
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0",
"lerna": "^6.5.1",
"prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1"
}
+251 -294
View File
@@ -5,19 +5,33 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Pattern, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs';
const controls = {};
const generic_params = [
/**
* Select a sound / sample by name.
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
* separated by ':'.
*
* @name s
* @param {string | Pattern} sound The sound / pattern of sounds to pick
* @synonyms sound
* @example
* s("bd hh")
* @example
* s("bd:0 bd:1 bd:0:0.3 bd:1:1.4")
*
*/
['s', 's', 'sound'],
[['s', 'n', 'gain'], 'sound'],
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @param {function} getSource
* @synonyms src
*
*/
['source', 'src'],
/**
* Selects the given index from the sample map.
* Numbers too high will wrap around.
@@ -29,7 +43,7 @@ const generic_params = [
* s("bd sd,hh*3").n("<0 1>")
*/
// also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The sample number to choose for a synth or sampleset'],
['n'],
/**
* Plays the given note name or midi number. A note name consists of
*
@@ -49,10 +63,8 @@ const generic_params = [
* @example
* note("60 69 65 64")
*/
['f', 'note', 'The note or pitch to play a sound or synth with'],
//['s', 'toArg', 'for internal sound routing'],
// ["f", "from", "for internal sound routing"),
//['f', 'to', 'for internal sound routing'],
[['note', 'n']],
/**
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
*
@@ -63,7 +75,7 @@ const generic_params = [
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
*
*/
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
['accelerate'],
/**
* Controls the gain by an exponential amount.
*
@@ -73,11 +85,7 @@ const generic_params = [
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
*
*/
[
'f',
'gain',
'a pattern of numbers that specify volume. Values less than 1 make the sound quieter. Values greater than 1 make the sound louder. For the linear equivalent, see @amp@.',
],
['gain'],
/**
* Like {@link gain}, but linear.
*
@@ -88,17 +96,18 @@ const generic_params = [
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
*
*/
['f', 'amp', 'like @gain@, but linear.'],
['amp'],
/**
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
*
* @name attack
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
* note("c3 e3").attack("<0 .1 .5>")
*
*/
['f', 'attack'],
['attack', 'att'],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -109,7 +118,7 @@ const generic_params = [
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['f', 'bank', 'selects sound bank to use'],
['bank'],
/**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
@@ -121,52 +130,46 @@ const generic_params = [
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
*
*/
['f', 'decay', ''],
['decay'],
/**
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
* @name sustain
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['f', 'sustain', ''],
['sustain', 'sus'],
/**
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
*
*/
[
'f',
'release',
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
],
[
'f',
'hold',
'a pattern of numbers to specify the hold time (in seconds) of an envelope applied to each sample. Only takes effect if `attack` and `release` are also specified.',
],
['release', 'rel'],
['hold'],
// TODO: in tidal, it seems to be normalized
/**
* Sets the center frequency of the **b**and-**p**ass **f**ilter.
* Sets the center frequency of the **b**and-**p**ass **f**ilter. When using mininotation, you
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
* @param {number | Pattern} frequency center frequency
* @synonyms bandf
* @synonyms bandf, bp
* @example
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
*
*/
['f', 'bpf', ''],
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
[['bandf', 'bandq'], 'bpf', 'bp'],
// TODO: in tidal, it seems to be normalized
/**
* Sets the **b**and-**p**ass **q**-factor (resonance)
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
* @param {number | Pattern} q q factor
@@ -175,8 +178,9 @@ const generic_params = [
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
*
*/
['f', 'bpq', ''],
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
// ['bpq'],
['bandq', 'bpq'],
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
@@ -188,11 +192,7 @@ const generic_params = [
* s("rave").begin("<0 .25 .5 .75>")
*
*/
[
'f',
'begin',
'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.',
],
['begin'],
/**
* The same as .begin, but cuts off the end off each sample.
*
@@ -203,11 +203,7 @@ const generic_params = [
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
*
*/
[
'f',
'end',
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
],
['end'],
/**
* Loops the sample (from `begin` to `end`) the specified number of times.
* Note that the tempo of the loop is not synced with the cycle tempo.
@@ -218,7 +214,7 @@ const generic_params = [
* s("bd").loop("<1 2 3 4>").osc()
*
*/
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
['loop'],
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
@@ -230,8 +226,8 @@ const generic_params = [
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
*/
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
// ['f', 'clhatdecay', ''],
// ['legato'],
// ['clhatdecay'],
/**
* bit crusher effect.
*
@@ -241,11 +237,7 @@ const generic_params = [
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
[
'f',
'crush',
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
['crush'],
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
@@ -255,11 +247,7 @@ const generic_params = [
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
*
*/
[
'f',
'coarse',
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
],
['coarse'],
/**
* choose the channel the pattern is sent to in superdirt
@@ -268,7 +256,7 @@ const generic_params = [
* @param {number | Pattern} channel channel number
*
*/
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
['channel'],
/**
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
*
@@ -278,35 +266,39 @@ const generic_params = [
* s("rd*4").cut(1)
*
*/
[
'i',
'cut',
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
],
['cut'],
/**
* Applies the cutoff frequency of the **l**ow-**p**ass **f**ilter.
*
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff
* @synonyms cutoff, ctf, lp
* @example
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
*
*/
['f', 'lpf'],
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
*
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hcutoff
* @synonyms hp, hcutoff
* @example
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd sd,hh*4").hpf("<2000 2000:25>")
*
*/
['f', 'hpf', ''],
['f', 'hcutoff', ''],
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
*
@@ -317,8 +309,7 @@ const generic_params = [
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
*
*/
['f', 'hresonance', ''],
['f', 'hpq', ''],
['hresonance', 'hpq'],
/**
* Controls the **l**ow-**p**ass **q**-value.
*
@@ -329,8 +320,8 @@ const generic_params = [
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
*
*/
['f', 'lpq'],
['f', 'resonance', ''],
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
['resonance', 'lpq'],
/**
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
@@ -340,40 +331,48 @@ const generic_params = [
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
*
*/
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
// ['f', 'cutoffegint', ''],
['djf'],
// ['cutoffegint'],
// TODO: does not seem to work
/**
* Sets the level of the delay signal.
*
* When using mininotation, you can also optionally add the 'delaytime' and 'delayfeedback' parameter,
* separated by ':'.
*
*
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .25 .5 1>")
* @example
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
*
*/
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
[['delay', 'delaytime', 'delayfeedback']],
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
*
*/
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
['delayfeedback', 'delayfb', 'dfb'],
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
*
*/
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
['delaytime', 'delayt', 'dt'],
/* // TODO: test
* Specifies whether delaytime is calculated relative to cps.
*
@@ -383,22 +382,19 @@ const generic_params = [
* s("sd").delay().lock(1).osc()
*
*/
[
'f',
'lock',
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
],
['lock'],
/**
* 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 detune
* @param {number | Pattern} amount between 0 and 1
* @synonyms det
* @superdirtOnly
* @example
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
*
*/
['f', 'detune', ''],
['detune', 'det'],
/**
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
*
@@ -409,11 +405,7 @@ const generic_params = [
* @superdirtOnly
*
*/
[
'f',
'dry',
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
],
['dry'],
// TODO: does not seem to do anything
/*
* Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope.
@@ -424,17 +416,9 @@ const generic_params = [
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
*
*/
[
'f',
'fadeTime',
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
],
['fadeTime', 'fadeOutTime'],
// TODO: see above
[
'f',
'fadeInTime',
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
],
['fadeInTime'],
/**
* Set frequency of sound.
*
@@ -446,15 +430,15 @@ const generic_params = [
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
*
*/
['f', 'freq', ''],
['freq'],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['f', 'gate', ''],
// ['f', 'hatgrain', ''],
// ['f', 'lagogo', ''],
// ['f', 'lclap', ''],
// ['f', 'lclaves', ''],
// ['f', 'lclhat', ''],
// ['f', 'lcrash', ''],
['gate', 'gat'],
// ['hatgrain'],
// ['lagogo'],
// ['lclap'],
// ['lclaves'],
// ['lclhat'],
// ['lcrash'],
// TODO:
// https://tidalcycles.org/docs/reference/audio_effects/#leslie-1
// https://tidalcycles.org/docs/reference/audio_effects/#leslie
@@ -468,7 +452,7 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'leslie', ''],
['leslie'],
/**
* Rate of modulation / rotation for leslie effect
*
@@ -480,7 +464,7 @@ const generic_params = [
*
*/
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
['f', 'lrate', ''],
['lrate'],
/**
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
*
@@ -491,31 +475,28 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'lsize', ''],
// ['f', 'lfo', ''],
// ['f', 'lfocutoffint', ''],
// ['f', 'lfodelay', ''],
// ['f', 'lfoint', ''],
// ['f', 'lfopitchint', ''],
// ['f', 'lfoshape', ''],
// ['f', 'lfosync', ''],
// ['f', 'lhitom', ''],
// ['f', 'lkick', ''],
// ['f', 'llotom', ''],
// ['f', 'lophat', ''],
// ['f', 'lsnare', ''],
['f', 'degree', ''], // TODO: what is this? not found in tidal doc
['f', 'mtranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'ctranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'harmonic', ''], // TODO: what is this? not found in tidal doc
['f', 'stepsPerOctave', ''], // TODO: what is this? not found in tidal doc
['f', 'octaveR', ''], // TODO: what is this? not found in tidal doc
// TODO: why is this needed? what's the difference to late / early?
[
'f',
'nudge',
'Nudges events into the future by the specified number of seconds. Negative numbers work up to a point as well (due to internal latency)',
],
['lsize'],
// ['lfo'],
// ['lfocutoffint'],
// ['lfodelay'],
// ['lfoint'],
// ['lfopitchint'],
// ['lfoshape'],
// ['lfosync'],
// ['lhitom'],
// ['lkick'],
// ['llotom'],
// ['lophat'],
// ['lsnare'],
['degree'], // TODO: what is this? not found in tidal doc
['mtranspose'], // TODO: what is this? not found in tidal doc
['ctranspose'], // TODO: what is this? not found in tidal doc
['harmonic'], // TODO: what is this? not found in tidal doc
['stepsPerOctave'], // TODO: what is this? not found in tidal doc
['octaveR'], // TODO: what is this? not found in tidal doc
// TODO: why is this needed? what's the difference to late / early? Answer: it's in seconds, and delays the message at
// OSC time (so can't be negative, at least not beyond the latency value)
['nudge'],
// TODO: the following doc is just a guess, it's not documented in tidal doc.
/**
* Sets the default octave of a synth.
@@ -526,9 +507,9 @@ const generic_params = [
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
* @superDirtOnly
*/
['i', 'octave', ''],
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
// ['f', 'ophatdecay', ''],
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
@@ -541,13 +522,9 @@ const generic_params = [
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
[
'i',
'orbit',
'a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.',
],
['f', 'overgain', ''], // TODO: what is this? not found in tidal doc
['f', 'overshape', ''], // TODO: what is this? not found in tidal doc
['orbit'],
['overgain'], // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
['overshape'], // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
/**
* Sets position in stereo.
*
@@ -557,11 +534,7 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>")
*
*/
[
'f',
'pan',
'a pattern of numbers between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)',
],
['pan'],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is fanned out
@@ -572,11 +545,7 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc()
*
*/
[
'f',
'panspan',
'a pattern of numbers between -inf and inf, which controls how much multichannel output is fanned out (negative is backwards ordering)',
],
['panspan'],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is spread
@@ -587,50 +556,42 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc()
*
*/
[
'f',
'pansplay',
'a pattern of numbers between 0.0 and 1.0, which controls the multichannel spread range (multichannel only)',
],
[
'f',
'panwidth',
'a pattern of numbers between 0.0 and inf, which controls how much each channel is distributed over neighbours (multichannel only)',
],
[
'f',
'panorient',
'a pattern of numbers between -1.0 and 1.0, which controls the relative position of the centre pan in a pair of adjacent speakers (multichannel only)',
],
// ['f', 'pitch1', ''],
// ['f', 'pitch2', ''],
// ['f', 'pitch3', ''],
// ['f', 'portamento', ''],
['pansplay'],
['panwidth'],
['panorient'],
// ['pitch1'],
// ['pitch2'],
// ['pitch3'],
// ['portamento'],
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'rate', "used in SuperDirt softsynths as a control rate or 'speed'"],
['rate'],
// TODO: slide param for certain synths
['f', 'slide', ''],
['slide'],
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'semitone', ''],
['semitone'],
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['f', 'velocity', ''],
['f', 'voice', ''], // TODO: synth param
// ['velocity'],
['voice'], // TODO: synth param
/**
* Sets the level of reverb.
*
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
*
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
* @example
* s("bd sd").room("<0.9:1 0.9:4>")
*
*/
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
[['room', 'size']],
/**
* Sets the room size of the reverb, see {@link room}.
*
* @name roomsize
* @synonyms size
* @param {number | Pattern} size between 0 and 10
* @synonyms size, sz
* @example
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
@@ -638,20 +599,11 @@ const generic_params = [
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
[
'f',
'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', ''],
// ['f', 'scrash', ''],
['size', 'sz', 'roomsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
// ['scrash'],
/**
* Wave shaping distortion. CAUTION: it might get loud
*
@@ -661,11 +613,7 @@ const generic_params = [
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
*
*/
[
'f',
'shape',
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
],
['shape'],
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
@@ -677,11 +625,7 @@ const generic_params = [
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
*
*/
[
'f',
'speed',
'a pattern of numbers which changes the speed of sample playback, i.e. a cheap way of changing pitch. Negative values will play the sample backwards!',
],
['speed'],
/**
* Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
@@ -692,11 +636,7 @@ const generic_params = [
* @superdirtOnly
*
*/
[
's',
'unit',
'used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.',
],
['unit'],
/**
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
*
@@ -709,14 +649,14 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'squiz', ''],
['f', 'stutterdepth', ''], // TODO: what is this? not found in tidal doc
['f', 'stuttertime', ''], // TODO: what is this? not found in tidal doc
['f', 'timescale', ''], // TODO: what is this? not found in tidal doc
['f', 'timescalewin', ''], // TODO: what is this? not found in tidal doc
// ['f', 'tomdecay', ''],
// ['f', 'vcfegint', ''],
// ['f', 'vcoegint', ''],
['squiz'],
// ['stutterdepth'], // TODO: what is this? not found in tidal doc
// ['stuttertime'], // TODO: what is this? not found in tidal doc
// ['timescale'], // TODO: what is this? not found in tidal doc
// ['timescalewin'], // TODO: what is this? not found in tidal doc
// ['tomdecay'],
// ['vcfegint'],
// ['vcoegint'],
// TODO: Use a rest (~) to override the effect <- vowel
/**
*
@@ -729,11 +669,7 @@ const generic_params = [
* .vowel("<a e i <o u>>")
*
*/
[
's',
'vowel',
'formant filter to make things sound like vowels, a pattern of either `a`, `e`, `i`, `o` or `u`. Use a rest (`~`) for no effect.',
],
['vowel'],
/* // TODO: find out how it works
* Made by Calum Gunn. Divides an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discards a fraction of them. Takes a number between 1 and 100, denoted the percentage of segments to drop. The SuperCollider manual describes the Waveloss effect this way:
*
@@ -744,12 +680,12 @@ const generic_params = [
*
* @name waveloss
*/
['f', 'waveloss', ''],
['waveloss'],
// TODO: midi effects?
['f', 'dur', ''],
// ['f', 'modwheel', ''],
['f', 'expression', ''],
['f', 'sustainpedal', ''],
['dur'],
// ['modwheel'],
['expression'],
['sustainpedal'],
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
@@ -760,59 +696,58 @@ const generic_params = [
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
// TODO: tremdp alias
['f', 'tremolodepth', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['f', 'tremolorate', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['tremolodepth', 'tremdp'],
['tremolorate', 'tremr'],
// TODO: doesn't seem to do anything
['f', 'phaserdepth', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'phaserrate', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['phaserdepth', 'phasdp'],
['phaserrate', 'phasr'],
['f', 'fshift', 'frequency shifter'],
['f', 'fshiftnote', 'frequency shifter'],
['f', 'fshiftphase', 'frequency shifter'],
['fshift'],
['fshiftnote'],
['fshiftphase'],
['f', 'triode', 'tube distortion'],
['f', 'krush', 'shape/bass enhancer'],
['f', 'kcutoff', ''],
['f', 'octer', 'octaver effect'],
['f', 'octersub', 'octaver effect'],
['f', 'octersubsub', 'octaver effect'],
['f', 'ring', 'ring modulation'],
['f', 'ringf', 'ring modulation'],
['f', 'ringdf', 'ring modulation'],
['f', 'distort', 'noisy fuzzy distortion'],
['f', 'freeze', 'Spectral freeze'],
['f', 'xsdelay', ''],
['f', 'tsdelay', ''],
['f', 'real', 'Spectral conform'],
['f', 'imag', ''],
['f', 'enhance', 'Spectral enhance'],
['f', 'partials', ''],
['f', 'comb', 'Spectral comb'],
['f', 'smear', 'Spectral smear'],
['f', 'scram', 'Spectral scramble'],
['f', 'binshift', 'Spectral binshift'],
['f', 'hbrick', 'High pass sort of spectral filter'],
['f', 'lbrick', 'Low pass sort of spectral filter'],
['f', 'midichan', ''],
['f', 'control', ''],
['f', 'ccn', ''],
['f', 'ccv', ''],
['f', 'polyTouch', ''],
['f', 'midibend', ''],
['f', 'miditouch', ''],
['f', 'ctlNum', ''],
['f', 'frameRate', ''],
['f', 'frames', ''],
['f', 'hours', ''],
['s', 'midicmd', ''],
['f', 'minutes', ''],
['f', 'progNum', ''],
['f', 'seconds', ''],
['f', 'songPtr', ''],
['f', 'uid', ''],
['f', 'val', ''],
['f', 'cps', ''],
['triode'],
['krush'],
['kcutoff'],
['octer'],
['octersub'],
['octersubsub'],
['ring'],
['ringf'],
['ringdf'],
['distort'],
['freeze'],
['xsdelay'],
['tsdelay'],
['real'],
['imag'],
['enhance'],
['partials'],
['comb'],
['smear'],
['scram'],
['binshift'],
['hbrick'],
['lbrick'],
['midichan'],
['control'],
['ccn'],
['ccv'],
['polyTouch'],
['midibend'],
['miditouch'],
['ctlNum'],
['frameRate'],
['frames'],
['hours'],
['midicmd'],
['minutes'],
['progNum'],
['seconds'],
['songPtr'],
['uid'],
['val'],
['cps'],
/**
* If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
@@ -823,33 +758,55 @@ const generic_params = [
* note("c a f e ~").s("piano").clip(1)
*
*/
['f', 'clip', ''],
['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 }));
controls.createParam = function (names) {
const name = Array.isArray(names) ? names[0] : names;
const _setter = (func, name) =>
function (...pats) {
var withVal;
if (Array.isArray(names)) {
withVal = (xs) => {
if (Array.isArray(xs)) {
const result = {};
xs.forEach((x, i) => {
if (i < names.length) {
result[names[i]] = x;
}
});
return result;
} else {
return { [name]: xs };
}
};
} else {
withVal = (x) => ({ [name]: x });
}
const func = (...pats) => sequence(...pats).withValue(withVal);
const setter = function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
return this.fmap(withVal);
}
return this.set(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(controls[name], name);
});
// create custom param
controls.createParam = (name) => {
const func = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(func, name);
return (...pats) => _name(name, ...pats);
Pattern.prototype[name] = setter;
return func;
};
generic_params.forEach(([names, ...aliases]) => {
const name = Array.isArray(names) ? names[0] : names;
controls[name] = controls.createParam(names);
aliases.forEach((alias) => {
controls[alias] = controls[name];
Pattern.prototype[alias] = Pattern.prototype[name];
});
});
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.6.8",
"version": "0.7.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+47 -27
View File
@@ -1127,6 +1127,8 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack;
export const pr = stack;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
Pattern.prototype.factories = {
@@ -1284,6 +1286,20 @@ export function timeCat(...timepats) {
return stack(...pats);
}
/**
* Allows to arrange multiple patterns together over multiple cycles.
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return timeCat(...sections).slow(total);
}
export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
@@ -1360,11 +1376,6 @@ export function polymeter(...args) {
return polymeterSteps(0, ...args);
}
// alias
export function pm(...args) {
polymeter(...args);
}
export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
@@ -1415,25 +1426,32 @@ export function register(name, func, patternify = true) {
const arity = func.length;
var pfunc; // the patternified function
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
if (patternify) {
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
} else {
pfunc = function (...args) {
args = args.map(reify);
return func(...args);
};
}
Pattern.prototype[name] = function (...args) {
// For methods that take a single argument (plus 'this'), allow
@@ -2231,11 +2249,13 @@ const _loopAt = function (factor, pat, cps = 1) {
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
*/
const slice = register(
'slice',
function (npat, ipat, opat) {
return npat.innerBind((n) =>
ipat.outerBind((i) =>
ipat.outerBind((i) =>
opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o };
@@ -2248,7 +2268,6 @@ const slice = register(
},
false, // turns off auto-patternification
);
*/
/*
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
@@ -2258,6 +2277,8 @@ const slice = register(
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
const splice = register(
'splice',
function (npat, ipat, opat) {
@@ -2273,8 +2294,7 @@ const splice = register(
});
},
false, // turns off auto-patternification
);
*/
);
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
+2 -2
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
@@ -18,7 +18,7 @@ const getValue = (e) => {
}
note = note ?? n;
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
+1 -1
View File
@@ -21,7 +21,7 @@ export function repl({
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration);
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
+28
View File
@@ -0,0 +1,28 @@
/*
controls.test.mjs - <short description TODO>
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import controls from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
describe('controls', () => {
it('should support controls', () => {
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
});
it('should support compound controls', () => {
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 },
]);
});
it('should support ignore extra elements in compound controls', () => {
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 },
]);
});
});
+2 -1
View File
@@ -976,6 +976,7 @@ describe('Pattern', () => {
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
});
});
*/
describe('slice', () => {
it('Can slice a sample', () => {
sameFirst(
@@ -1001,5 +1002,5 @@ describe('Pattern', () => {
),
);
});
});*/
});
});
+17 -17
View File
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
@@ -75,27 +75,27 @@ describe('isNote', () => {
expect(tokenizeNote(123)).toStrictEqual([]);
});
});
describe('toMidi', () => {
describe('noteToMidi', () => {
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);
expect(noteToMidi('A4')).toEqual(69);
expect(noteToMidi('C4')).toEqual(60);
expect(noteToMidi('Db4')).toEqual(61);
expect(noteToMidi('C3')).toEqual(48);
expect(noteToMidi('Cb3')).toEqual(47);
expect(noteToMidi('Cbb3')).toEqual(46);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('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"`);
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
});
});
describe('fromMidi', () => {
describe('midiToFreq', () => {
it('should turn midi into frequency', () => {
expect(fromMidi(69)).toEqual(440);
expect(fromMidi(57)).toEqual(220);
expect(midiToFreq(69)).toEqual(440);
expect(midiToFreq(57)).toEqual(220);
});
});
describe('freqToMidi', () => {
+9 -9
View File
@@ -19,7 +19,7 @@ export const tokenizeNote = (note) => {
};
// turns the given note into its midi number representation
export const toMidi = (note) => {
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
@@ -28,7 +28,7 @@ export const toMidi = (note) => {
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const fromMidi = (n) => {
export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
return freqToMidi(freq);
}
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -62,9 +62,9 @@ export const valueToMidi = (value, fallbackValue) => {
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
return midiToFreq(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
return midiToFreq(noteToMidi(noteOrMidi));
};
/**
@@ -91,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
}
// 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);
note = midiToFreq(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)) {
@@ -110,9 +110,9 @@ export const getFrequency = (hap) => {
return getFreq(value.note || value.n || value.value);
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value);
value = midiToFreq(hap.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(hap.value));
value = midiToFreq(noteToMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value);
}
@@ -170,7 +170,7 @@ export function parseNumeral(numOrString) {
return asNumber;
}
if (isNote(numOrString)) {
return toMidi(numOrString);
return noteToMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.6.2",
"version": "0.7.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.6.2",
"version": "0.7.0",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"publishConfig": {
-25
View File
@@ -1,25 +0,0 @@
# @strudel.cycles/link
This is an experiment to find out how to hook up ableton link with strudel.
## Usage
```sh
cd packages/link/ && npm i
node link-server.mjs # tested with node 18
```
This will play the pattern in `link-server.mjs`, which will be synced automatically via Ableton Link!
## Notes
- The sound generation uses [node-web-audio-api](https://github.com/audiojs/web-audio-api), supporting only oscillators with a fixed envelope
- All functions from `@strudel.cycles/webaudio` could be supported when the [node-webaudio branch](https://github.com/tidalcycles/strudel/tree/node-webaudio) is further developed. This includes refactoring the webaudio stuff to allow passing an audio context + making sure it can be imported in node
- To make this live codable, I see 2 options:
- headless: add an express server (or similar) that allows sending `PUT` requests to change the pattern.
- browser: find a way combine the REPL with the ableton clock.
- the server could pass the callback from ableton link via http or websocket
- the client could then use these messages to control the scheduler
- could try implementing getTime as a LERP between received beat values
- or do not use the scheduler and just query within the received beat values
- highlighting is another thing..
-1
View File
@@ -1 +0,0 @@
export * from './link-client.mjs';
-26
View File
@@ -1,26 +0,0 @@
/*
link-server.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/osc/osc.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 { connectOSC } from '@strudel.cycles/osc';
import { Pattern, silence } from '@strudel.cycles/core';
/**
*
* Connects to Ableton Link
*
* @name link
* @memberof Pattern
* @returns Pattern
*/
console.log('link-client');
Pattern.prototype.link = async function () {
const osc = await connectOSC();
osc.on('*', (msg) => {
const [begin, end, cps] = msg.address.split('/');
console.log(begin, end, cps);
});
return silence;
};
-63
View File
@@ -1,63 +0,0 @@
import { evaluate } from '@strudel.cycles/transpiler';
import { evalScope, controls, getFrequency } from '@strudel.cycles/core';
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
import abletonlink from 'abletonlink';
//// prepare pattern
await evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
// import('@strudel.cycles/xen'),
// import('@strudel.cycles/osc'),
);
const { pattern } = await evaluate(
`note("<Dm7 G7 C^7!2>".voicings('lefthand')).ply(8).gain(.0625).s('sawtooth').early(.06)`,
);
//// web audio logic
const ac = new AudioContext();
export function playHaps(haps, beat, cps = 1) {
const ct = ac.currentTime;
haps
.filter((hap) => hap.hasOnset())
.forEach((hap) => {
const deadline = (hap.whole.begin - beat) / cps;
const t = ct + deadline;
const freq = getFrequency(hap) ?? 220;
const { gain = 1, s = 'triangle' } = hap.value;
const env = new GainNode(ac);
env.connect(ac.destination);
env.gain.value = 0;
env.gain.setValueAtTime(0, t);
env.gain.linearRampToValueAtTime(gain, t + 0.02);
env.gain.exponentialRampToValueAtTime(0.0001, t + hap.duration);
const osc = new OscillatorNode(ac);
osc.frequency.value = freq;
osc.type = s;
osc.connect(env);
osc.start(t);
osc.stop(t + hap.duration);
});
}
//// link scheduling
const link = new abletonlink();
let lastBeat;
link.startUpdate(60, (beat, phase, bpm) => {
if (lastBeat !== undefined) {
const cps = bpm / 60;
const haps = pattern.slow(2).queryArc(lastBeat, beat);
playHaps(haps, lastBeat, cps);
}
lastBeat = beat;
});
-38
View File
@@ -1,38 +0,0 @@
{
"name": "@strudel.cycles/link",
"version": "0.6.0",
"description": "Ableton Link Support for Strudel",
"main": "link-server.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node link-server.mjs"
},
"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",
"devDependencies": {
"abletonlink": "^0.1.3",
"node-web-audio-api": "^0.5.0"
},
"dependencies": {
"@strudel.cycles/core": "workspace:^0.6.8",
"@strudel.cycles/mini": "workspace:^0.6.0",
"@strudel.cycles/osc": "workspace:^0.6.0",
"@strudel.cycles/tonal": "workspace:^0.6.0",
"@strudel.cycles/transpiler": "workspace:^0.6.0"
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { toMidi } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = toMidi(note);
const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.6.0",
"version": "0.7.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
+150 -123
View File
@@ -182,21 +182,21 @@ function peg$parse(input, options) {
var peg$c8 = "#";
var peg$c9 = "^";
var peg$c10 = "_";
var peg$c11 = ":";
var peg$c12 = "[";
var peg$c13 = "]";
var peg$c14 = "{";
var peg$c15 = "}";
var peg$c16 = "%";
var peg$c17 = "<";
var peg$c18 = ">";
var peg$c19 = "@";
var peg$c20 = "!";
var peg$c21 = "(";
var peg$c22 = ")";
var peg$c23 = "/";
var peg$c24 = "*";
var peg$c25 = "?";
var peg$c11 = "[";
var peg$c12 = "]";
var peg$c13 = "{";
var peg$c14 = "}";
var peg$c15 = "%";
var peg$c16 = "<";
var peg$c17 = ">";
var peg$c18 = "@";
var peg$c19 = "!";
var peg$c20 = "(";
var peg$c21 = ")";
var peg$c22 = "/";
var peg$c23 = "*";
var peg$c24 = "?";
var peg$c25 = ":";
var peg$c26 = "struct";
var peg$c27 = "target";
var peg$c28 = "euclid";
@@ -237,21 +237,21 @@ function peg$parse(input, options) {
var peg$e15 = peg$literalExpectation("#", false);
var peg$e16 = peg$literalExpectation("^", false);
var peg$e17 = peg$literalExpectation("_", false);
var peg$e18 = peg$literalExpectation(":", false);
var peg$e19 = peg$literalExpectation("[", false);
var peg$e20 = peg$literalExpectation("]", false);
var peg$e21 = peg$literalExpectation("{", false);
var peg$e22 = peg$literalExpectation("}", false);
var peg$e23 = peg$literalExpectation("%", false);
var peg$e24 = peg$literalExpectation("<", false);
var peg$e25 = peg$literalExpectation(">", false);
var peg$e26 = peg$literalExpectation("@", false);
var peg$e27 = peg$literalExpectation("!", false);
var peg$e28 = peg$literalExpectation("(", false);
var peg$e29 = peg$literalExpectation(")", false);
var peg$e30 = peg$literalExpectation("/", false);
var peg$e31 = peg$literalExpectation("*", false);
var peg$e32 = peg$literalExpectation("?", false);
var peg$e18 = peg$literalExpectation("[", false);
var peg$e19 = peg$literalExpectation("]", false);
var peg$e20 = peg$literalExpectation("{", false);
var peg$e21 = peg$literalExpectation("}", false);
var peg$e22 = peg$literalExpectation("%", false);
var peg$e23 = peg$literalExpectation("<", false);
var peg$e24 = peg$literalExpectation(">", false);
var peg$e25 = peg$literalExpectation("@", false);
var peg$e26 = peg$literalExpectation("!", false);
var peg$e27 = peg$literalExpectation("(", false);
var peg$e28 = peg$literalExpectation(")", false);
var peg$e29 = peg$literalExpectation("/", false);
var peg$e30 = peg$literalExpectation("*", false);
var peg$e31 = peg$literalExpectation("?", false);
var peg$e32 = peg$literalExpectation(":", false);
var peg$e33 = peg$literalExpectation("struct", false);
var peg$e34 = peg$literalExpectation("target", false);
var peg$e35 = peg$literalExpectation("euclid", false);
@@ -279,36 +279,37 @@ function peg$parse(input, options) {
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
};
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f18 = function(sc) { return sc; };
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f27 = function(s, v) { return v};
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f29 = function(sg) {return sg};
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f31 = function(sc) { return sc };
var peg$f32 = function(c) { return c };
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f35 = function() { return new CommandStub("hush")};
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f19 = function(sc) { return sc; };
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f28 = function(s, v) { return v};
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f30 = function(sg) {return sg};
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f32 = function(sc) { return sc };
var peg$f33 = function(c) { return c };
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f36 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -848,15 +849,6 @@ function peg$parse(input, options) {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 58) {
s0 = peg$c11;
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
}
}
}
}
@@ -899,11 +891,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s2 = peg$c12;
s2 = peg$c11;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -911,11 +903,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s6 = peg$c13;
s6 = peg$c12;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -943,11 +935,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 123) {
s2 = peg$c14;
s2 = peg$c13;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e21); }
if (peg$silentFails === 0) { peg$fail(peg$e20); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -955,11 +947,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 125) {
s6 = peg$c15;
s6 = peg$c14;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e22); }
if (peg$silentFails === 0) { peg$fail(peg$e21); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsepolymeter_steps();
@@ -990,11 +982,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 37) {
s1 = peg$c16;
s1 = peg$c15;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e23); }
if (peg$silentFails === 0) { peg$fail(peg$e22); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1019,11 +1011,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 60) {
s2 = peg$c17;
s2 = peg$c16;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e24); }
if (peg$silentFails === 0) { peg$fail(peg$e23); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -1031,11 +1023,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 62) {
s6 = peg$c18;
s6 = peg$c17;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e25); }
if (peg$silentFails === 0) { peg$fail(peg$e24); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -1088,6 +1080,9 @@ function peg$parse(input, options) {
s0 = peg$parseop_replicate();
if (s0 === peg$FAILED) {
s0 = peg$parseop_degrade();
if (s0 === peg$FAILED) {
s0 = peg$parseop_tail();
}
}
}
}
@@ -1102,11 +1097,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 64) {
s1 = peg$c19;
s1 = peg$c18;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); }
if (peg$silentFails === 0) { peg$fail(peg$e25); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1130,11 +1125,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 33) {
s1 = peg$c20;
s1 = peg$c19;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); }
if (peg$silentFails === 0) { peg$fail(peg$e26); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1158,11 +1153,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c21;
s1 = peg$c20;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
if (peg$silentFails === 0) { peg$fail(peg$e27); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1186,11 +1181,11 @@ function peg$parse(input, options) {
}
s12 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 41) {
s13 = peg$c22;
s13 = peg$c21;
peg$currPos++;
} else {
s13 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1224,11 +1219,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s1 = peg$c23;
s1 = peg$c22;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e30); }
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1252,11 +1247,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 42) {
s1 = peg$c24;
s1 = peg$c23;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e31); }
if (peg$silentFails === 0) { peg$fail(peg$e30); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1280,11 +1275,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 63) {
s1 = peg$c25;
s1 = peg$c24;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
if (peg$silentFails === 0) { peg$fail(peg$e31); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1301,6 +1296,34 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseop_tail() {
var s0, s1, s2;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 58) {
s1 = peg$c25;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseslice_with_ops() {
var s0, s1, s2, s3;
@@ -1314,7 +1337,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f12(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1339,7 +1362,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f13(s1);
s1 = peg$f14(s1);
}
s0 = s1;
@@ -1388,7 +1411,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f14(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@@ -1437,7 +1460,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f15(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@@ -1458,7 +1481,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f16(s1, s2);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1478,7 +1501,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f17(s1, s2);
s0 = peg$f18(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1499,7 +1522,7 @@ function peg$parse(input, options) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f18(s3);
s0 = peg$f19(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1561,7 +1584,7 @@ function peg$parse(input, options) {
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f20(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1594,7 +1617,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f20(s4);
s0 = peg$f21(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1639,7 +1662,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f21(s3, s5, s7);
s0 = peg$f22(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1672,7 +1695,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f23(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1701,7 +1724,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s3);
s0 = peg$f24(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1730,7 +1753,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f24(s3);
s0 = peg$f25(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1759,7 +1782,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f25(s3);
s0 = peg$f26(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1801,7 +1824,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f26(s4);
s0 = peg$f27(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1876,11 +1899,11 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s3 = peg$c12;
s3 = peg$c11;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
@@ -1893,7 +1916,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1910,7 +1933,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1922,15 +1945,15 @@ function peg$parse(input, options) {
}
s7 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s8 = peg$c13;
s8 = peg$c12;
peg$currPos++;
} else {
s8 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f28(s5, s6);
s0 = peg$f29(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1976,7 +1999,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f29(s1);
s0 = peg$f30(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1998,7 +2021,7 @@ function peg$parse(input, options) {
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f30(s1, s5);
s0 = peg$f31(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2023,7 +2046,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f31(s1);
s1 = peg$f32(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2056,7 +2079,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f32(s2);
s0 = peg$f33(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2081,7 +2104,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s3);
s0 = peg$f34(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2110,7 +2133,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f34(s3);
s0 = peg$f35(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2136,7 +2159,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f35();
s1 = peg$f36();
}
s0 = s1;
@@ -2162,10 +2185,13 @@ function peg$parse(input, options) {
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -2191,6 +2217,7 @@ function peg$parse(input, options) {
this.options_ = options;
}
var seed = 0;
peg$result = peg$startRuleFunction();
+14 -7
View File
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
this.options_ = options;
}
var seed = 0;
}
start = statement
@@ -96,7 +100,7 @@ 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 new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
@@ -119,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// 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_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -137,7 +141,10 @@ op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
@@ -159,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
// a choose is a series of pipe-separated sequence, one of which is
// chosen at random, each cycle
choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail }; }
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
+11 -30
View File
@@ -7,17 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
/* var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
} */
const randOffset = 0.0003;
const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
@@ -43,27 +39,14 @@ const applyOptions = (parent, code) => (pat, i) => {
break;
}
case 'degradeBy': {
// TODO: find out what is right here
// example:
/*
stack(
s("hh*8").degrade(),
s("[ht*8]?")
)
*/
// above example will only be in sync when _degradeBy is used...
// it also seems that the nextSeed will create undeterministic behaviour
// as it uses a global _seedState. This is probably the reason for
// https://github.com/tidalcycles/strudel/issues/245
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
op.arguments_.amount ?? 0.5,
);
*/
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
pat = strudel
.reify(pat)
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
break;
}
case 'tail': {
const friend = patternifyAST(op.arguments_.element, code);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
default: {
@@ -108,9 +91,7 @@ export function patternifyAST(ast, code) {
return strudel.stack(...aligned);
}
if (alignment === 'rand') {
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children);
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.6.0",
"version": "0.7.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+20 -3
View File
@@ -140,7 +140,22 @@ describe('mini', () => {
expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8');
});
/*it('supports the random choice operator ("|") with nesting', () => {
it('supports multiple independent uses of the random choice operator ("|")', () => {
const numCycles = 1000;
const values = mini('[a|b] [a|b]')
.queryArc(0, numCycles)
.map((e) => e.value);
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
for (let i = 0; i < values.length; i += 2) {
const chunk = values.slice(i, i + 2);
observed[chunk.join('')]++;
}
for (const count of Object.values(observed)) {
// Should fall within 99% confidence interval for binomial with p=0.25.
expect(215 <= count && count <= 286).toBe(true);
}
});
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.
@@ -165,6 +180,8 @@ describe('mini', () => {
// 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');
});*/
});
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.6.0",
"version": "0.7.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
@@ -0,0 +1,15 @@
# nano-repl
this is an example of how to create a repl with strudel and react.
## Usage
after cloning the strudel repo:
```sh
pnpm i
cd packages/react/examples/nano-repl
pnpm dev
```
you should now have a repl running at `http://localhost:5173/`
@@ -66,7 +66,7 @@ function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
@@ -75,8 +75,8 @@ function App() {
useHighlighting({
view,
pattern,
active: !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
const error = evalError || schedulerError;
@@ -2,6 +2,18 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
body {
background: #123;
}
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}'],
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
};
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.6.4",
"version": "0.7.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -47,17 +47,17 @@
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.18",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
}
-2
View File
@@ -8,8 +8,6 @@ evalScope(
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
+2 -3
View File
@@ -1,10 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createRoot } from 'react-dom/client';
ReactDOM.render(
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.6.0",
"version": "0.7.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
+48 -7
View File
@@ -1,4 +1,6 @@
import { toMidi } from '@strudel.cycles/core';
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
let loadCache = {};
async function loadFont(name) {
@@ -8,7 +10,6 @@ async function loadFont(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);
@@ -17,15 +18,24 @@ async function loadFont(name) {
return loadCache[name];
}
export async function getFontBufferSource(name, pitch, ac) {
if (typeof pitch === 'string') {
pitch = toMidi(pitch);
export async function getFontBufferSource(name, value, ac) {
let { note = 'c3', freq } = value;
let midi;
if (freq) {
midi = freqToMidi(freq);
} else if (typeof note === 'string') {
midi = noteToMidi(note);
} else if (typeof note === 'number') {
midi = note;
} else {
throw new Error(`unexpected "note" type "${typeof note}"`);
}
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const { buffer, zone } = await getFontPitch(name, midi, 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);
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
// src detune?
src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
@@ -114,3 +124,34 @@ async function getBuffer(zone, audioContext) {
}
}
}
export function registerSoundfonts() {
Object.entries(gm).forEach(([name, fonts]) => {
registerSound(
name,
async (time, value, onended) => {
const { n = 0 } = value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n % fonts.length];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
onended();
};
return { node: envelope, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);
});
}
registerSoundfonts();
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource } from './fontloader.mjs';
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.6.0",
"version": "0.7.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+27 -3
View File
@@ -1,5 +1,5 @@
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
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 args = [ctx, preset, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
stop(deadline + h.duration);
});
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
}
const sf = _loadSoundfont(url);
soundfontCache.set(url, sf);
/*sf.then((font) => {
font.presets.forEach((preset) => {
console.log('preset', preset.header.name);
registerSound(
preset.header.name.replaceAll(' ', '_'),
(time, value, onended) => {
const ctx = getAudioContext();
let { note } = value; // freq ?
const p = font.presets.find((p) => p.header.name === preset.header.name);
if (!p) {
throw new Error('preset not found');
}
const deadline = time; // - ctx.currentTime;
const args = [ctx, p, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
return { node: undefined, stop };
},
{ type: 'soundfont' },
);
});
//console.log('f', f);
});*/
return sf;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.6.0",
"version": "0.7.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+15
View File
@@ -9,6 +9,7 @@ This program is free software: you can redistribute it and/or modify it under th
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, controls, seq } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
const { n } = controls;
describe('tonal', () => {
@@ -30,4 +31,18 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(0, 1, 2)
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(0, 1, 2)
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
});
+14 -6
View File
@@ -123,29 +123,37 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
* The root note defaults to octave 3, if no octave number is given.
* Note that you currently cannot pattern `scale` with the mini notation, because the scale name includes a space.
* This will be improved in the future. Until then, use a sequence function like `cat` or `seq`.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2".scale('C2 major').note()
* "0 2 4 6 4 2".scale("C2:major").note()
* @example
* "0 2 4 6 4 2"
* .scale(seq('C2 major', 'C2 minor').slow(2))
* .scale("C2:<major minor>")
* .note()
* @example
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* .s("folkharp")
*/
export const scale = register('scale', function (scale /* : string */, pat) {
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
scale = scale.replaceAll(':', ' ');
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.6.0",
"version": "0.7.0",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.6.0",
"version": "0.7.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
+70
View File
@@ -0,0 +1,70 @@
import { getAudioContext } from './webaudio.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
// sustain end
return {
node: gainNode,
stop: (t) => {
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
//} else {
// firefox: this will glitch when the sustain has not been reached yet at the time of release
gainNode.gain.setValueAtTime(sustain * velocity, t);
//}
gainNode.gain.linearRampToValueAtTime(0, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
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;
};
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
+2
View File
@@ -6,3 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.6.1",
"version": "0.7.0",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -34,7 +34,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"nanostores": "^0.7.4"
},
"devDependencies": {
"vite": "^3.2.2"
+129 -56
View File
@@ -1,5 +1,6 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext } from './index.mjs';
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -20,7 +21,7 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
@@ -29,28 +30,11 @@ export const getSampleBufferSource = async (s, n, note, speed, freq) => {
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 {
const midiDiff = (noteA) => toMidi(noteA) - midi;
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
@@ -107,8 +91,6 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
let sampleCache = { current: undefined };
/**
* Loads a collection of samples to use with `s`
* @example
@@ -123,7 +105,7 @@ let sampleCache = { current: undefined };
*
*/
export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:');
@@ -141,43 +123,134 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base))
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
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)];
}),
),
];
}),
),
const { prebake, tag } = options;
Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl,
prebake,
tag,
});
});
};
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank) {
const {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
const ac = getAudioContext();
// 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 } = value;
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
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 * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
envelope.disconnect();
out.disconnect();
onended();
};
};
const stop = (endTime, playWholeBuffer = !clip) => {
let releaseTime = endTime;
if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
const handle = { node: out, bufferSource, stop };
export const resetLoadedSamples = () => {
sampleCache.current = undefined;
};
// cut groups
if (cut !== undefined) {
const prev = cutGroups[cut];
if (prev) {
prev.node.gain.setValueAtTime(1, time);
prev.node.gain.linearRampToValueAtTime(0, time + 0.01);
}
cutGroups[cut] = handle;
}
export const getLoadedSamples = () => sampleCache.current;
return handle;
}
+44
View File
@@ -0,0 +1,44 @@
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// 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 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const g = gainNode(0.3);
// envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
stop(releaseTime + release);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
+53 -201
View File
@@ -5,15 +5,22 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs';
import './reverb.mjs';
import { getSampleBufferSource } from './sampler.mjs';
const { Pattern } = strudel;
const { Pattern, logger } = strudel;
import './vowel.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
// export const getAudioContext = () => Tone.getContext().rawContext;
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const getAudioContext = () => {
@@ -38,76 +45,6 @@ export const panic = () => {
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 gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
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 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) {
@@ -147,13 +84,6 @@ export async function initAudioOnFirstClick() {
});
}
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]) {
@@ -191,153 +121,74 @@ function effectSend(input, effect, wet) {
}
// export const webaudioOutput = async (t, hap, ct, cps) => {
export const webaudioOutput = async (hap, deadline, hapDuration) => {
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
const ac = getAudioContext();
hap.ensureObjectValue();
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure value
// destructure
let {
freq,
s,
s = 'triangle',
bank,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
source,
gain = 0.8,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
cutoff,
resonance = 1,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
hcutoff,
hresonance = 1,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
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 = [];
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
}
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 source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, hap.value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, hap.value, onended);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
// 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;
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// 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);
throw new Error(`sound ${s} not found! Is it loaded?`);
}
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));
@@ -373,19 +224,20 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(orbit, roomsize);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
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());
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.6.0",
"version": "0.7.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
+4 -3
View File
@@ -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 { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
let webDirt;
@@ -62,8 +62,9 @@ export function loadWebDirt(config) {
* @noAutocomplete
*/
Pattern.prototype.webdirt = function () {
throw new Error('webdirt support has been dropped..');
// create a WebDirt object and initialize Web Audio context
return this.onTrigger(async (time, e, currentTime) => {
/* return this.onTrigger(async (time, e, currentTime) => {
if (!webDirt) {
throw new Error('WebDirt not initialized!');
}
@@ -92,5 +93,5 @@ Pattern.prototype.webdirt = function () {
webDirt.playSample(msg, deadline);
}
}
});
}); */
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.6.0",
"version": "0.7.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {
+1075 -1567
View File
File diff suppressed because it is too large Load Diff
+303 -241
View File
@@ -848,6 +848,23 @@ exports[`runs examples > example "arpWith" example index 0 1`] = `
]
`;
exports[`runs examples > example "arrange" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c ]",
"[ 3/8 → 1/2 | note:c ]",
"[ 3/4 → 7/8 | note:c ]",
"[ 1/1 → 9/8 | note:a ]",
"[ 11/8 → 3/2 | note:a ]",
"[ 7/4 → 15/8 | note:a ]",
"[ 2/1 → 17/8 | note:f ]",
"[ 19/8 → 5/2 | note:f ]",
"[ 11/4 → 23/8 | note:f ]",
"[ 3/1 → 25/8 | note:e ]",
"[ 27/8 → 7/2 | note:e ]",
"[ 15/4 → 31/8 | note:e ]",
]
`;
exports[`runs examples > example "attack" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:c3 attack:0 ]",
@@ -885,39 +902,39 @@ exports[`runs examples > example "begin" example index 0 1`] = `
exports[`runs examples > example "bpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bpf:1000 ]",
"[ 1/2 → 1/1 | s:sd bpf:1000 ]",
"[ 0/1 → 1/3 | s:hh bpf:1000 ]",
"[ 1/3 → 2/3 | s:hh bpf:1000 ]",
"[ 2/3 → 1/1 | s:hh bpf:1000 ]",
"[ 1/1 → 3/2 | s:bd bpf:2000 ]",
"[ 3/2 → 2/1 | s:sd bpf:2000 ]",
"[ 1/1 → 4/3 | s:hh bpf:2000 ]",
"[ 4/3 → 5/3 | s:hh bpf:2000 ]",
"[ 5/3 → 2/1 | s:hh bpf:2000 ]",
"[ 2/1 → 5/2 | s:bd bpf:4000 ]",
"[ 5/2 → 3/1 | s:sd bpf:4000 ]",
"[ 2/1 → 7/3 | s:hh bpf:4000 ]",
"[ 7/3 → 8/3 | s:hh bpf:4000 ]",
"[ 8/3 → 3/1 | s:hh bpf:4000 ]",
"[ 3/1 → 7/2 | s:bd bpf:8000 ]",
"[ 7/2 → 4/1 | s:sd bpf:8000 ]",
"[ 3/1 → 10/3 | s:hh bpf:8000 ]",
"[ 10/3 → 11/3 | s:hh bpf:8000 ]",
"[ 11/3 → 4/1 | s:hh bpf:8000 ]",
"[ 0/1 → 1/2 | s:bd bandf:1000 ]",
"[ 1/2 → 1/1 | s:sd bandf:1000 ]",
"[ 0/1 → 1/3 | s:hh bandf:1000 ]",
"[ 1/3 → 2/3 | s:hh bandf:1000 ]",
"[ 2/3 → 1/1 | s:hh bandf:1000 ]",
"[ 1/1 → 3/2 | s:bd bandf:2000 ]",
"[ 3/2 → 2/1 | s:sd bandf:2000 ]",
"[ 1/1 → 4/3 | s:hh bandf:2000 ]",
"[ 4/3 → 5/3 | s:hh bandf:2000 ]",
"[ 5/3 → 2/1 | s:hh bandf:2000 ]",
"[ 2/1 → 5/2 | s:bd bandf:4000 ]",
"[ 5/2 → 3/1 | s:sd bandf:4000 ]",
"[ 2/1 → 7/3 | s:hh bandf:4000 ]",
"[ 7/3 → 8/3 | s:hh bandf:4000 ]",
"[ 8/3 → 3/1 | s:hh bandf:4000 ]",
"[ 3/1 → 7/2 | s:bd bandf:8000 ]",
"[ 7/2 → 4/1 | s:sd bandf:8000 ]",
"[ 3/1 → 10/3 | s:hh bandf:8000 ]",
"[ 10/3 → 11/3 | s:hh bandf:8000 ]",
"[ 11/3 → 4/1 | s:hh bandf:8000 ]",
]
`;
exports[`runs examples > example "bpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bpf:500 bpq:0 ]",
"[ 1/2 → 1/1 | s:sd bpf:500 bpq:0 ]",
"[ 1/1 → 3/2 | s:bd bpf:500 bpq:1 ]",
"[ 3/2 → 2/1 | s:sd bpf:500 bpq:1 ]",
"[ 2/1 → 5/2 | s:bd bpf:500 bpq:2 ]",
"[ 5/2 → 3/1 | s:sd bpf:500 bpq:2 ]",
"[ 3/1 → 7/2 | s:bd bpf:500 bpq:3 ]",
"[ 7/2 → 4/1 | s:sd bpf:500 bpq:3 ]",
"[ 0/1 → 1/2 | s:bd bandf:500 bandq:0 ]",
"[ 1/2 → 1/1 | s:sd bandf:500 bandq:0 ]",
"[ 1/1 → 3/2 | s:bd bandf:500 bandq:1 ]",
"[ 3/2 → 2/1 | s:sd bandf:500 bandq:1 ]",
"[ 2/1 → 5/2 | s:bd bandf:500 bandq:2 ]",
"[ 5/2 → 3/1 | s:sd bandf:500 bandq:2 ]",
"[ 3/1 → 7/2 | s:bd bandf:500 bandq:3 ]",
"[ 7/2 → 4/1 | s:sd bandf:500 bandq:3 ]",
]
`;
@@ -1399,6 +1416,19 @@ exports[`runs examples > example "delay" example index 0 1`] = `
]
`;
exports[`runs examples > example "delay" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 1/2 → 1/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 1/1 → 3/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 3/2 → 2/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 2/1 → 5/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 5/2 → 3/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 3/1 → 7/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 7/2 → 4/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
]
`;
exports[`runs examples > example "delayfeedback" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 2/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
@@ -1896,78 +1926,107 @@ exports[`runs examples > example "gain" example index 0 1`] = `
exports[`runs examples > example "hpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd hpf:4000 ]",
"[ 1/2 → 1/1 | s:sd hpf:4000 ]",
"[ 0/1 → 1/4 | s:hh hpf:4000 ]",
"[ 1/4 → 1/2 | s:hh hpf:4000 ]",
"[ 1/2 → 3/4 | s:hh hpf:4000 ]",
"[ 3/4 → 1/1 | s:hh hpf:4000 ]",
"[ 1/1 → 3/2 | s:bd hpf:2000 ]",
"[ 3/2 → 2/1 | s:sd hpf:2000 ]",
"[ 1/1 → 5/4 | s:hh hpf:2000 ]",
"[ 5/4 → 3/2 | s:hh hpf:2000 ]",
"[ 3/2 → 7/4 | s:hh hpf:2000 ]",
"[ 7/4 → 2/1 | s:hh hpf:2000 ]",
"[ 2/1 → 5/2 | s:bd hpf:1000 ]",
"[ 5/2 → 3/1 | s:sd hpf:1000 ]",
"[ 2/1 → 9/4 | s:hh hpf:1000 ]",
"[ 9/4 → 5/2 | s:hh hpf:1000 ]",
"[ 5/2 → 11/4 | s:hh hpf:1000 ]",
"[ 11/4 → 3/1 | s:hh hpf:1000 ]",
"[ 3/1 → 7/2 | s:bd hpf:500 ]",
"[ 7/2 → 4/1 | s:sd hpf:500 ]",
"[ 3/1 → 13/4 | s:hh hpf:500 ]",
"[ 13/4 → 7/2 | s:hh hpf:500 ]",
"[ 7/2 → 15/4 | s:hh hpf:500 ]",
"[ 15/4 → 4/1 | s:hh hpf:500 ]",
"[ 0/1 → 1/2 | s:bd hcutoff:4000 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:4000 ]",
"[ 0/1 → 1/4 | s:hh hcutoff:4000 ]",
"[ 1/4 → 1/2 | s:hh hcutoff:4000 ]",
"[ 1/2 → 3/4 | s:hh hcutoff:4000 ]",
"[ 3/4 → 1/1 | s:hh hcutoff:4000 ]",
"[ 1/1 → 3/2 | s:bd hcutoff:2000 ]",
"[ 3/2 → 2/1 | s:sd hcutoff:2000 ]",
"[ 1/1 → 5/4 | s:hh hcutoff:2000 ]",
"[ 5/4 → 3/2 | s:hh hcutoff:2000 ]",
"[ 3/2 → 7/4 | s:hh hcutoff:2000 ]",
"[ 7/4 → 2/1 | s:hh hcutoff:2000 ]",
"[ 2/1 → 5/2 | s:bd hcutoff:1000 ]",
"[ 5/2 → 3/1 | s:sd hcutoff:1000 ]",
"[ 2/1 → 9/4 | s:hh hcutoff:1000 ]",
"[ 9/4 → 5/2 | s:hh hcutoff:1000 ]",
"[ 5/2 → 11/4 | s:hh hcutoff:1000 ]",
"[ 11/4 → 3/1 | s:hh hcutoff:1000 ]",
"[ 3/1 → 7/2 | s:bd hcutoff:500 ]",
"[ 7/2 → 4/1 | s:sd hcutoff:500 ]",
"[ 3/1 → 13/4 | s:hh hcutoff:500 ]",
"[ 13/4 → 7/2 | s:hh hcutoff:500 ]",
"[ 7/2 → 15/4 | s:hh hcutoff:500 ]",
"[ 15/4 → 4/1 | s:hh hcutoff:500 ]",
]
`;
exports[`runs examples > example "hpf" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd hcutoff:2000 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:2000 ]",
"[ 0/1 → 1/4 | s:hh hcutoff:2000 ]",
"[ 1/4 → 1/2 | s:hh hcutoff:2000 ]",
"[ 1/2 → 3/4 | s:hh hcutoff:2000 ]",
"[ 3/4 → 1/1 | s:hh hcutoff:2000 ]",
"[ 1/1 → 3/2 | s:bd hcutoff:2000 hresonance:25 ]",
"[ 3/2 → 2/1 | s:sd hcutoff:2000 hresonance:25 ]",
"[ 1/1 → 5/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 5/4 → 3/2 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 3/2 → 7/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 7/4 → 2/1 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 2/1 → 5/2 | s:bd hcutoff:2000 ]",
"[ 5/2 → 3/1 | s:sd hcutoff:2000 ]",
"[ 2/1 → 9/4 | s:hh hcutoff:2000 ]",
"[ 9/4 → 5/2 | s:hh hcutoff:2000 ]",
"[ 5/2 → 11/4 | s:hh hcutoff:2000 ]",
"[ 11/4 → 3/1 | s:hh hcutoff:2000 ]",
"[ 3/1 → 7/2 | s:bd hcutoff:2000 hresonance:25 ]",
"[ 7/2 → 4/1 | s:sd hcutoff:2000 hresonance:25 ]",
"[ 3/1 → 13/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 13/4 → 7/2 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 7/2 → 15/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 15/4 → 4/1 | s:hh hcutoff:2000 hresonance:25 ]",
]
`;
exports[`runs examples > example "hpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd hpf:2000 hpq:0 ]",
"[ 1/2 → 1/1 | s:sd hpf:2000 hpq:0 ]",
"[ 0/1 → 1/4 | s:hh hpf:2000 hpq:0 ]",
"[ 1/4 → 1/2 | s:hh hpf:2000 hpq:0 ]",
"[ 1/2 → 3/4 | s:hh hpf:2000 hpq:0 ]",
"[ 3/4 → 1/1 | s:hh hpf:2000 hpq:0 ]",
"[ 1/1 → 3/2 | s:bd hpf:2000 hpq:10 ]",
"[ 3/2 → 2/1 | s:sd hpf:2000 hpq:10 ]",
"[ 1/1 → 5/4 | s:hh hpf:2000 hpq:10 ]",
"[ 5/4 → 3/2 | s:hh hpf:2000 hpq:10 ]",
"[ 3/2 → 7/4 | s:hh hpf:2000 hpq:10 ]",
"[ 7/4 → 2/1 | s:hh hpf:2000 hpq:10 ]",
"[ 2/1 → 5/2 | s:bd hpf:2000 hpq:20 ]",
"[ 5/2 → 3/1 | s:sd hpf:2000 hpq:20 ]",
"[ 2/1 → 9/4 | s:hh hpf:2000 hpq:20 ]",
"[ 9/4 → 5/2 | s:hh hpf:2000 hpq:20 ]",
"[ 5/2 → 11/4 | s:hh hpf:2000 hpq:20 ]",
"[ 11/4 → 3/1 | s:hh hpf:2000 hpq:20 ]",
"[ 3/1 → 7/2 | s:bd hpf:2000 hpq:30 ]",
"[ 7/2 → 4/1 | s:sd hpf:2000 hpq:30 ]",
"[ 3/1 → 13/4 | s:hh hpf:2000 hpq:30 ]",
"[ 13/4 → 7/2 | s:hh hpf:2000 hpq:30 ]",
"[ 7/2 → 15/4 | s:hh hpf:2000 hpq:30 ]",
"[ 15/4 → 4/1 | s:hh hpf:2000 hpq:30 ]",
"[ 0/1 → 1/2 | s:bd hcutoff:2000 hresonance:0 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:2000 hresonance:0 ]",
"[ 0/1 → 1/4 | s:hh hcutoff:2000 hresonance:0 ]",
"[ 1/4 → 1/2 | s:hh hcutoff:2000 hresonance:0 ]",
"[ 1/2 → 3/4 | s:hh hcutoff:2000 hresonance:0 ]",
"[ 3/4 → 1/1 | s:hh hcutoff:2000 hresonance:0 ]",
"[ 1/1 → 3/2 | s:bd hcutoff:2000 hresonance:10 ]",
"[ 3/2 → 2/1 | s:sd hcutoff:2000 hresonance:10 ]",
"[ 1/1 → 5/4 | s:hh hcutoff:2000 hresonance:10 ]",
"[ 5/4 → 3/2 | s:hh hcutoff:2000 hresonance:10 ]",
"[ 3/2 → 7/4 | s:hh hcutoff:2000 hresonance:10 ]",
"[ 7/4 → 2/1 | s:hh hcutoff:2000 hresonance:10 ]",
"[ 2/1 → 5/2 | s:bd hcutoff:2000 hresonance:20 ]",
"[ 5/2 → 3/1 | s:sd hcutoff:2000 hresonance:20 ]",
"[ 2/1 → 9/4 | s:hh hcutoff:2000 hresonance:20 ]",
"[ 9/4 → 5/2 | s:hh hcutoff:2000 hresonance:20 ]",
"[ 5/2 → 11/4 | s:hh hcutoff:2000 hresonance:20 ]",
"[ 11/4 → 3/1 | s:hh hcutoff:2000 hresonance:20 ]",
"[ 3/1 → 7/2 | s:bd hcutoff:2000 hresonance:30 ]",
"[ 7/2 → 4/1 | s:sd hcutoff:2000 hresonance:30 ]",
"[ 3/1 → 13/4 | s:hh hcutoff:2000 hresonance:30 ]",
"[ 13/4 → 7/2 | s:hh hcutoff:2000 hresonance:30 ]",
"[ 7/2 → 15/4 | s:hh hcutoff:2000 hresonance:30 ]",
"[ 15/4 → 4/1 | s:hh hcutoff:2000 hresonance:30 ]",
]
`;
exports[`runs examples > example "hurry" example index 0 1`] = `
[
"[ 0/1 → 3/4 | s:bd speed:1 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:2 speed:1 ]",
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd:2 speed:1 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]",
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]",
"[ 3/2 → 15/8 | s:bd speed:2 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd:2 speed:2 ]",
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd:2 speed:2 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]",
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]",
"[ 9/4 → 21/8 | s:bd speed:2 ]",
"[ 21/8 → 3/1 | s:sd:2 speed:2 ]",
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
"[ 3/1 → 51/16 | s:bd speed:4 ]",
"[ 51/16 → 27/8 | s:sd:2 speed:4 ]",
"[ 51/16 → 27/8 | s:sd n:2 speed:4 ]",
"[ 27/8 → 57/16 | s:bd speed:4 ]",
"[ 57/16 → 15/4 | s:sd:2 speed:4 ]",
"[ 57/16 → 15/4 | s:sd n:2 speed:4 ]",
"[ 15/4 → 63/16 | s:bd speed:4 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd:2 speed:4 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd n:2 speed:4 ]",
]
`;
@@ -2383,55 +2442,92 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
exports[`runs examples > example "lpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd lpf:4000 ]",
"[ 1/2 → 1/1 | s:sd lpf:4000 ]",
"[ 0/1 → 1/3 | s:hh lpf:4000 ]",
"[ 1/3 → 2/3 | s:hh lpf:4000 ]",
"[ 2/3 → 1/1 | s:hh lpf:4000 ]",
"[ 1/1 → 3/2 | s:bd lpf:2000 ]",
"[ 3/2 → 2/1 | s:sd lpf:2000 ]",
"[ 1/1 → 4/3 | s:hh lpf:2000 ]",
"[ 4/3 → 5/3 | s:hh lpf:2000 ]",
"[ 5/3 → 2/1 | s:hh lpf:2000 ]",
"[ 2/1 → 5/2 | s:bd lpf:1000 ]",
"[ 5/2 → 3/1 | s:sd lpf:1000 ]",
"[ 2/1 → 7/3 | s:hh lpf:1000 ]",
"[ 7/3 → 8/3 | s:hh lpf:1000 ]",
"[ 8/3 → 3/1 | s:hh lpf:1000 ]",
"[ 3/1 → 7/2 | s:bd lpf:500 ]",
"[ 7/2 → 4/1 | s:sd lpf:500 ]",
"[ 3/1 → 10/3 | s:hh lpf:500 ]",
"[ 10/3 → 11/3 | s:hh lpf:500 ]",
"[ 11/3 → 4/1 | s:hh lpf:500 ]",
"[ 0/1 → 1/2 | s:bd cutoff:4000 ]",
"[ 1/2 → 1/1 | s:sd cutoff:4000 ]",
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
"[ 1/3 → 2/3 | s:hh cutoff:4000 ]",
"[ 2/3 → 1/1 | s:hh cutoff:4000 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 ]",
"[ 1/1 → 4/3 | s:hh cutoff:2000 ]",
"[ 4/3 → 5/3 | s:hh cutoff:2000 ]",
"[ 5/3 → 2/1 | s:hh cutoff:2000 ]",
"[ 2/1 → 5/2 | s:bd cutoff:1000 ]",
"[ 5/2 → 3/1 | s:sd cutoff:1000 ]",
"[ 2/1 → 7/3 | s:hh cutoff:1000 ]",
"[ 7/3 → 8/3 | s:hh cutoff:1000 ]",
"[ 8/3 → 3/1 | s:hh cutoff:1000 ]",
"[ 3/1 → 7/2 | s:bd cutoff:500 ]",
"[ 7/2 → 4/1 | s:sd cutoff:500 ]",
"[ 3/1 → 10/3 | s:hh cutoff:500 ]",
"[ 10/3 → 11/3 | s:hh cutoff:500 ]",
"[ 11/3 → 4/1 | s:hh cutoff:500 ]",
]
`;
exports[`runs examples > example "lpf" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 1/8 → 1/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 1/4 → 3/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 3/8 → 1/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 1/2 → 5/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 5/8 → 3/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 3/4 → 7/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 7/8 → 1/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 1/1 → 9/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 9/8 → 5/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 5/4 → 11/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 11/8 → 3/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 13/8 → 7/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 7/4 → 15/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 15/8 → 2/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 17/8 → 9/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 9/4 → 19/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 19/8 → 5/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 21/8 → 11/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 11/4 → 23/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 23/8 → 3/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 3/1 → 25/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 25/8 → 13/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 13/4 → 27/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 27/8 → 7/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 7/2 → 29/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 29/8 → 15/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 15/4 → 31/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 31/8 → 4/1 | s:bd cutoff:1000 resonance:30 ]",
]
`;
exports[`runs examples > example "lpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd lpf:2000 lpq:0 ]",
"[ 1/2 → 1/1 | s:sd lpf:2000 lpq:0 ]",
"[ 0/1 → 1/4 | s:hh lpf:2000 lpq:0 ]",
"[ 1/4 → 1/2 | s:hh lpf:2000 lpq:0 ]",
"[ 1/2 → 3/4 | s:hh lpf:2000 lpq:0 ]",
"[ 3/4 → 1/1 | s:hh lpf:2000 lpq:0 ]",
"[ 1/1 → 3/2 | s:bd lpf:2000 lpq:10 ]",
"[ 3/2 → 2/1 | s:sd lpf:2000 lpq:10 ]",
"[ 1/1 → 5/4 | s:hh lpf:2000 lpq:10 ]",
"[ 5/4 → 3/2 | s:hh lpf:2000 lpq:10 ]",
"[ 3/2 → 7/4 | s:hh lpf:2000 lpq:10 ]",
"[ 7/4 → 2/1 | s:hh lpf:2000 lpq:10 ]",
"[ 2/1 → 5/2 | s:bd lpf:2000 lpq:20 ]",
"[ 5/2 → 3/1 | s:sd lpf:2000 lpq:20 ]",
"[ 2/1 → 9/4 | s:hh lpf:2000 lpq:20 ]",
"[ 9/4 → 5/2 | s:hh lpf:2000 lpq:20 ]",
"[ 5/2 → 11/4 | s:hh lpf:2000 lpq:20 ]",
"[ 11/4 → 3/1 | s:hh lpf:2000 lpq:20 ]",
"[ 3/1 → 7/2 | s:bd lpf:2000 lpq:30 ]",
"[ 7/2 → 4/1 | s:sd lpf:2000 lpq:30 ]",
"[ 3/1 → 13/4 | s:hh lpf:2000 lpq:30 ]",
"[ 13/4 → 7/2 | s:hh lpf:2000 lpq:30 ]",
"[ 7/2 → 15/4 | s:hh lpf:2000 lpq:30 ]",
"[ 15/4 → 4/1 | s:hh lpf:2000 lpq:30 ]",
"[ 0/1 → 1/2 | s:bd cutoff:2000 resonance:0 ]",
"[ 1/2 → 1/1 | s:sd cutoff:2000 resonance:0 ]",
"[ 0/1 → 1/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/4 → 1/2 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/2 → 3/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 3/4 → 1/1 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 resonance:10 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 resonance:10 ]",
"[ 1/1 → 5/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 5/4 → 3/2 | s:hh cutoff:2000 resonance:10 ]",
"[ 3/2 → 7/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 7/4 → 2/1 | s:hh cutoff:2000 resonance:10 ]",
"[ 2/1 → 5/2 | s:bd cutoff:2000 resonance:20 ]",
"[ 5/2 → 3/1 | s:sd cutoff:2000 resonance:20 ]",
"[ 2/1 → 9/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 9/4 → 5/2 | s:hh cutoff:2000 resonance:20 ]",
"[ 5/2 → 11/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 11/4 → 3/1 | s:hh cutoff:2000 resonance:20 ]",
"[ 3/1 → 7/2 | s:bd cutoff:2000 resonance:30 ]",
"[ 7/2 → 4/1 | s:sd cutoff:2000 resonance:30 ]",
"[ 3/1 → 13/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 13/4 → 7/2 | s:hh cutoff:2000 resonance:30 ]",
"[ 7/2 → 15/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 15/4 → 4/1 | s:hh cutoff:2000 resonance:30 ]",
]
`;
@@ -3231,16 +3327,29 @@ exports[`runs examples > example "room" example index 0 1`] = `
]
`;
exports[`runs examples > example "room" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.9 size:1 ]",
"[ 1/2 → 1/1 | s:sd room:0.9 size:1 ]",
"[ 1/1 → 3/2 | s:bd room:0.9 size:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.9 size:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.9 size:1 ]",
"[ 5/2 → 3/1 | s:sd room:0.9 size:1 ]",
"[ 3/1 → 7/2 | s:bd room:0.9 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.9 size:4 ]",
]
`;
exports[`runs examples > example "roomsize" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
]
`;
@@ -3304,6 +3413,27 @@ exports[`runs examples > example "s" example index 0 1`] = `
]
`;
exports[`runs examples > example "s" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:bd n:0 ]",
"[ 1/4 → 1/2 | s:bd n:1 ]",
"[ 1/2 → 3/4 | s:bd n:0 gain:0.3 ]",
"[ 3/4 → 1/1 | s:bd n:1 gain:1.4 ]",
"[ 1/1 → 5/4 | s:bd n:0 ]",
"[ 5/4 → 3/2 | s:bd n:1 ]",
"[ 3/2 → 7/4 | s:bd n:0 gain:0.3 ]",
"[ 7/4 → 2/1 | s:bd n:1 gain:1.4 ]",
"[ 2/1 → 9/4 | s:bd n:0 ]",
"[ 9/4 → 5/2 | s:bd n:1 ]",
"[ 5/2 → 11/4 | s:bd n:0 gain:0.3 ]",
"[ 11/4 → 3/1 | s:bd n:1 gain:1.4 ]",
"[ 3/1 → 13/4 | s:bd n:0 ]",
"[ 13/4 → 7/2 | s:bd n:1 ]",
"[ 7/2 → 15/4 | s:bd n:0 gain:0.3 ]",
"[ 15/4 → 4/1 | s:bd n:1 gain:1.4 ]",
]
`;
exports[`runs examples > example "samples" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -3454,6 +3584,43 @@ exports[`runs examples > example "scale" example index 1 1`] = `
]
`;
exports[`runs examples > example "scale" example index 2 1`] = `
[
"[ 7/8 → 1/1 | note:C2 s:folkharp ]",
"[ 3/4 → 7/8 | note:D2 s:folkharp ]",
"[ 5/8 → 3/4 | note:E2 s:folkharp ]",
"[ 1/2 → 5/8 | note:F2 s:folkharp ]",
"[ 3/8 → 1/2 | note:G2 s:folkharp ]",
"[ 1/4 → 3/8 | note:A2 s:folkharp ]",
"[ 1/8 → 1/4 | note:B2 s:folkharp ]",
"[ 0/1 → 1/8 | note:C3 s:folkharp ]",
"[ 15/8 → 2/1 | note:C2 s:folkharp ]",
"[ 7/4 → 15/8 | note:D2 s:folkharp ]",
"[ 13/8 → 7/4 | note:Eb2 s:folkharp ]",
"[ 3/2 → 13/8 | note:F2 s:folkharp ]",
"[ 11/8 → 3/2 | note:G2 s:folkharp ]",
"[ 5/4 → 11/8 | note:Ab2 s:folkharp ]",
"[ 9/8 → 5/4 | note:Bb2 s:folkharp ]",
"[ 1/1 → 9/8 | note:C3 s:folkharp ]",
"[ 23/8 → 3/1 | note:C2 s:folkharp ]",
"[ 11/4 → 23/8 | note:D2 s:folkharp ]",
"[ 21/8 → 11/4 | note:E2 s:folkharp ]",
"[ 5/2 → 21/8 | note:F2 s:folkharp ]",
"[ 19/8 → 5/2 | note:G2 s:folkharp ]",
"[ 9/4 → 19/8 | note:A2 s:folkharp ]",
"[ 17/8 → 9/4 | note:B2 s:folkharp ]",
"[ 2/1 → 17/8 | note:C3 s:folkharp ]",
"[ 31/8 → 4/1 | note:C2 s:folkharp ]",
"[ 15/4 → 31/8 | note:D2 s:folkharp ]",
"[ 29/8 → 15/4 | note:Eb2 s:folkharp ]",
"[ 7/2 → 29/8 | note:F2 s:folkharp ]",
"[ 27/8 → 7/2 | note:G2 s:folkharp ]",
"[ 13/4 → 27/8 | note:Ab2 s:folkharp ]",
"[ 25/8 → 13/4 | note:Bb2 s:folkharp ]",
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
]
`;
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -3690,39 +3857,6 @@ exports[`runs examples > example "sine" example index 0 1`] = `
]
`;
exports[`runs examples > example "slice" example index 0 1`] = `
[
"[ (15/16 → 1/1) ⇝ 9/8 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 3/4 → 15/16 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 21/32 → 3/4 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 9/16 → 21/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 3/8 → 9/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 3/16 → 3/8 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
"[ 0/1 → 3/16 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 21/16 → 3/2 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 9/8 → 21/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 15/16 ⇜ (1/1 → 9/8) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 3/2 → 27/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 27/16 → 15/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 15/8 → 63/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ (63/32 → 2/1) ⇝ 33/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 63/32 ⇜ (2/1 → 33/16) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 33/16 → 9/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 9/4 → 75/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 75/32 → 39/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 39/16 → 21/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 21/8 → 45/16 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
"[ 45/16 → 3/1 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 3/1 → 51/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 51/16 → 27/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 27/8 → 57/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 57/16 → 15/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 15/4 → 123/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 123/32 → 63/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
]
`;
exports[`runs examples > example "slow" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd ]",
@@ -3848,36 +3982,6 @@ exports[`runs examples > example "speed" example index 1 1`] = `
]
`;
exports[`runs examples > example "splice" example index 0 1`] = `
[
"[ 0/1 → 5/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 5/26 → 5/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 5/13 → 20/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 20/39 → 25/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 25/39 → 10/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 10/13 → 25/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ (25/26 → 1/1) ⇝ 35/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 25/26 ⇜ (1/1 → 35/26) | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 35/26 → 20/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 20/13 → 45/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 45/26 → 25/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ (25/13 → 2/1) ⇝ 80/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 25/13 ⇜ (2/1 → 80/39) | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 80/39 → 85/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 85/39 → 30/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 30/13 → 5/2 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 5/2 → 75/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (75/26 → 3/1) ⇝ 40/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 75/26 ⇜ (3/1 → 40/13) | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 40/13 → 85/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 85/26 → 45/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 45/13 → 140/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 140/39 → 145/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 145/39 → 50/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (50/13 → 4/1) ⇝ 105/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
]
`;
exports[`runs examples > example "square" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -4294,48 +4398,6 @@ exports[`runs examples > example "vowel" example index 0 1`] = `
]
`;
exports[`runs examples > example "weave" example index 0 1`] = `
[
"[ 0/1 → 1/8 | pan:0.015625 s:bd ]",
"[ 3/8 → 1/2 | pan:0.109375 s:bd ]",
"[ 3/4 → 7/8 | pan:0.203125 s:bd ]",
"[ 1/1 → 9/8 | pan:0.265625 s:bd ]",
"[ 11/8 → 3/2 | pan:0.359375 s:bd ]",
"[ 7/4 → 15/8 | pan:0.453125 s:bd ]",
"[ 2/1 → 17/8 | pan:0.515625 s:bd ]",
"[ 19/8 → 5/2 | pan:0.609375 s:bd ]",
"[ 11/4 → 23/8 | pan:0.703125 s:bd ]",
"[ 3/1 → 25/8 | pan:0.765625 s:bd ]",
"[ 27/8 → 7/2 | pan:0.859375 s:bd ]",
"[ 15/4 → 31/8 | pan:0.953125 s:bd ]",
"[ 1/2 → 1/1 | pan:0.6875 s:sd ]",
"[ 3/2 → 2/1 | pan:0.9375 s:sd ]",
"[ 5/2 → 3/1 | pan:0.1875 s:sd ]",
"[ 7/2 → 4/1 | pan:0.4375 s:sd ]",
]
`;
exports[`runs examples > example "weave" example index 1 1`] = `
[
"[ 0/1 → 1/8 | n:0 s:bd ]",
"[ 3/8 → 1/2 | n:0 s:bd ]",
"[ 3/4 → 7/8 | n:0 s:bd ]",
"[ 1/1 → 9/8 | n:1 s:bd ]",
"[ 11/8 → 3/2 | n:1 s:bd ]",
"[ 7/4 → 15/8 | n:1 s:bd ]",
"[ 2/1 → 17/8 | n:2 s:bd ]",
"[ 19/8 → 5/2 | n:2 s:bd ]",
"[ 11/4 → 23/8 | n:2 s:bd ]",
"[ 3/1 → 25/8 | n:3 s:bd ]",
"[ 27/8 → 7/2 | n:3 s:bd ]",
"[ 15/4 → 31/8 | n:3 s:bd ]",
"[ 1/2 → 1/1 | n:4 s:sd ]",
"[ 3/2 → 2/1 | n:5 s:sd ]",
"[ 5/2 → 3/1 | n:6 s:sd ]",
"[ 7/2 → 4/1 | n:7 s:sd ]",
]
`;
exports[`runs examples > example "webdirt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd n:0 ]",
+32 -31
View File
@@ -63,6 +63,7 @@ exports[`renders tunes > tune: arpoon 1`] = `
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 gain:0.5 ]",
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 gain:0.5 ]",
"[ 1/2 → 2/3 | s:hh bank:RolandTR909 gain:0.5 ]",
"[ 5/6 → 1/1 | s:hh bank:RolandTR909 gain:0.5 ]",
]
`;
@@ -298,11 +299,11 @@ exports[`renders tunes > tune: bassFuge 1`] = `
"[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:A4 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ 0/1 → 1/2 | s:bd:1 ]",
"[ 1/2 → 1/1 | s:bd:1 ]",
"[ 1/2 → 1/1 | s:sd:0 ]",
"[ 1/4 → 1/2 | s:hh:0 ]",
"[ 3/4 → 1/1 | s:hh:0 ]",
"[ 0/1 → 1/2 | s:bd n:1 ]",
"[ 1/2 → 1/1 | s:bd n:1 ]",
"[ 1/2 → 1/1 | s:sd n:0 ]",
"[ 1/4 → 1/2 | s:hh n:0 ]",
"[ 3/4 → 1/1 | s:hh n:0 ]",
]
`;
@@ -313,7 +314,7 @@ exports[`renders tunes > tune: belldub 1`] = `
"[ (5/8 → 1/1) ⇝ 5/4 | s:hh room:0 end:0.04483079938329212 ]",
"[ 0/1 → 5/16 | s:mt gain:0.5 room:0.5 ]",
"[ (15/16 → 1/1) ⇝ 5/4 | s:lt gain:0.5 room:0.5 ]",
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc n:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:F3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:A3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:Bb3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
@@ -6974,16 +6975,16 @@ exports[`renders tunes > tune: flatrave 1`] = `
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
"[ 0/1 → 1/4 | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/8 → 1/2 | s:hh:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/4 → 7/8 | s:hh:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 → 1/4 | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
@@ -8127,8 +8128,8 @@ exports[`renders tunes > tune: loungeSponge 1`] = `
exports[`renders tunes > tune: meltingsubmarine 1`] = `
[
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd:5 speed:0.7519542165100574 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:1 speed:0.7931522866332671 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd n:5 speed:0.7519542165100574 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:1 speed:0.7931522866332671 ]",
"[ 3/8 → 3/4 | s:hh27 speed:0.7285963821098448 ]",
"[ (3/4 → 1/1) ⇝ 9/8 | s:hh27 speed:0.77531205091027 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]",
@@ -8193,11 +8194,11 @@ exports[`renders tunes > tune: orbit 1`] = `
exports[`renders tunes > tune: outroMusic 1`] = `
[
"[ (0/1 → 1/1) ⇝ 3/1 | note:E3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:G3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:B3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:D4 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 9/2 | note:C5 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:E3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:G3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:B3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:D4 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 9/2 | note:C5 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ 0/1 → 3/4 | note:C2 s:sawtooth attack:0.05 decay:0.1 sustain:0.7 cutoff:864.536878321087 gain:0.3 ]",
"[ 0/1 → 3/4 | s:bd speed:0.9107561463868479 n:3 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd speed:0.9931522866332672 n:3 ]",
@@ -8364,15 +8365,15 @@ exports[`renders tunes > tune: randomBells 1`] = `
exports[`renders tunes > tune: sampleDemo 1`] = `
[
"[ 0/1 → 1/4 | s:woodblock:1 ]",
"[ 1/4 → 3/8 | s:woodblock:2 ]",
"[ 0/1 → 1/8 | s:brakedrum:1 ]",
"[ 3/4 → 7/8 | s:brakedrum:1 ]",
"[ 3/8 → 1/2 | s:woodblock:2 speed:2 ]",
"[ 1/2 → 1/1 | s:snare_rim:0 speed:2 ]",
"[ 0/1 → 1/4 | s:woodblock n:1 ]",
"[ 1/4 → 3/8 | s:woodblock n:2 ]",
"[ 0/1 → 1/8 | s:brakedrum n:1 ]",
"[ 3/4 → 7/8 | s:brakedrum n:1 ]",
"[ 3/8 → 1/2 | s:woodblock n:2 speed:2 ]",
"[ 1/2 → 1/1 | s:snare_rim n:0 speed:2 ]",
"[ (0/1 → 1/1) ⇝ 8/1 | s:gong speed:2 ]",
"[ 3/8 → 1/2 | s:brakedrum:1 speed:2 ]",
"[ 3/4 → 1/1 | s:cowbell:3 speed:2 ]",
"[ 3/8 → 1/2 | s:brakedrum n:1 speed:2 ]",
"[ 3/4 → 1/1 | s:cowbell n:3 speed:2 ]",
"[ -3/4 ⇜ (0/1 → 1/4) | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:0 ]",
"[ (3/4 → 1/1) ⇝ 7/4 | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
"[ -1/4 ⇜ (0/1 → 3/4) | note:F3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
+2 -2
View File
@@ -15,8 +15,8 @@
"isNoteWithOctave",
"isNote",
"tokenizeNote",
"toMidi",
"fromMidi",
"noteToMidi",
"midiToFreq",
"freqToMidi",
"valueToMidi",
"_mod",
Binary file not shown.
@@ -0,0 +1,8 @@
https://www.1001fonts.com/galactico-font.html
If the License is “Free” do what ever you want with it, even for commercial use, any “shout-out” or support thrown our way is much appreciated, even donating to buy a cup of coffee helps a lot. If the License is “For Personal Use Only” do what ever you want with it for yourself, but if you plan to make money out of it, meaning for commercial use, buy a license @ www.hellodonmarciano.com
Ill appreciate your support when and if possible. Enjoy!
Galactico is licensed under the 1001Fonts Free For Commercial Use License (FFC)
https://www.1001fonts.com/licenses/ffc.html
@@ -0,0 +1,7 @@
This font was created by Nate Piekos of Blambot Comic Fonts. It's freeware to use as you'd like, but if you redistribute it, you must include this text file with the zip.
~Nate Piekos
Blambot Comic Fonts
http://www.piekosarts.com/blambotfonts
blambot@piekosarts.com
+1 -1
View File
@@ -1986,7 +1986,7 @@
"Idiophones/Struck%20Idiophones/Tambourine%202/Tamb2_Shake_rr3_Mid.wav",
"Idiophones/Struck%20Idiophones/Tambourine%202/Tamb2_Shake_rr4_Mid.wav"
],
"triangle": [
"triangles": [
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitFM_v1_rr1_Mid.wav",
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitFM_v1_rr2_Mid.wav",
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitM_v1_rr2_Mid.wav",
+1
View File
@@ -73,6 +73,7 @@ export const SIDEBAR: Sidebar = {
],
Development: [
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Sounds', link: 'technical-manual/sounds' },
{ text: 'Packages', link: 'technical-manual/packages' },
{ text: 'Docs', link: 'technical-manual/docs' },
{ text: 'Testing', link: 'technical-manual/testing' },
+1
View File
@@ -18,6 +18,7 @@ if (typeof window !== 'undefined') {
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/csound'),
import('@strudel.cycles/soundfonts'),
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ to fit better for the larger context, while mini notation is more practical for
## Limits of Mini Notation
While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example:
While the Mini Notation is a powerful way to write rhythms concisely, it also has its limits. Take this example:
<MiniRepl
client:idle
+4
View File
@@ -49,6 +49,10 @@ As a chained function:
<JsDoc client:idle name="timeCat" h={0} />
## arrange
<JsDoc client:idle name="arrange" h={0} />
## polymeter
<JsDoc client:idle name="polymeter" h={0} />
+5 -5
View File
@@ -46,16 +46,16 @@ For drum sounds, strudel uses the comprehensive [tidal-drum-machines](https://gi
Furthermore, strudel also loads instrument samples from [VCSL](https://github.com/sgossner/VCSL) by default.
To see which sample names are available, open the `samples` tab in the [REPL](https://strudel.tidalcycles.org/).
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.tidalcycles.org/).
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples itself are not loaded until they are actually played.
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
This behaviour of loading things only when they are needed is also called `lazy loading`.
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
[This might be fixed in the future](https://github.com/tidalcycles/strudel/issues/187)
# Sound Banks
If we look at the `samples` tab, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
We _could_ use them like this:
@@ -75,9 +75,9 @@ Also note that some banks won't have samples for all sounds!
# Selecting Sounds
If we look again at the `samples` tab, there is also a number behind each name, indicating how many individual samples are available.
If we open the `sounds` tab again, followed by tab `drum machines`, there is also a number behind each name, indicating how many individual samples are available.
For example `RolandTR909_hh(4)` means there are 4 samples of a TR909 hihat available.
By default, `s` will play the first sample, but we can selecting the other ones using `n`, starting from 0:
By default, `s` will play the first sample, but we can select the other ones using `n`, starting from 0:
<MiniRepl client:idle tune={`s("hh*4").bank("RolandTR909").n("<0 1 2 3>")`} />
@@ -0,0 +1,75 @@
---
title: Sounds
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
# Sounds
Let's take a closer look about how sounds are implemented in the webaudio output.
## Registering a sound
All sounds are registered in the sound map, using the the `registerSound` function:
```ts
function registerSound(
name: string, // The name of the sound that should be given to `s`, e.g. `mysaw`
// The function called by the scheduler to trigger the sound:
(
time: number, // The audio context time the sound should start
value: object, // The value of the `Hap`
onended: () => void // A callback that should be fired when the sound has ended
) => {
node: AudioNode, // node to connect to rest of the effects chain
stop: (time:number) => void // a function that will stop the sound
},
data: object // meta data, only for ui logic in sounds tab
);
```
When `registerSound` is called, it registers `{ onTrigger, data }` under the given `name` in a [nanostore map](https://github.com/nanostores/nanostores#maps).
### Example
This might be a bit abstract, so here is a minimal example:
```js
registerSound(
'mysaw',
(time, value, onended) => {
let { freq } = value; // destructure control params
const ctx = getAudioContext();
// create oscillator
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
o.start(time);
// add gain node to level down osc
const g = new GainNode(ctx, { gain: 0.3 });
// connect osc to gain
const node = o.connect(g);
// this function can be called from outside to stop the sound
const stop = (time) => o.stop(time);
// ended will be fired when stop has been fired
o.addEventListener('ended', () => {
o.disconnect();
g.disconnect();
onended();
});
return { node, stop };
},
{ type: 'synth' },
);
// use the sound
freq(220, 440, 330).s('mysaw');
```
You can actually use this code in the [REPL](https://strudel.tidalcycles.org/) and it'll work.
After evaluating the code, you should see `mysaw` in listed in the sounds tab.
## Playing sounds
Now here is what happens when a sound is played:
When the webaudio output plays a `Hap`, it will lookup and call the `onTrigger` function for the given `s`.
The returned `node` can then be connected to the rest of the standard effects chain
Having the stop function separate allows playing sounds via midi too, where you don't know how long the noteon will last
+94 -26
View File
@@ -3,11 +3,12 @@ import { logger } from '@strudel.cycles/core';
import { useEvent, cx } from '@strudel.cycles/react';
// import { cx } from '@strudel.cycles/react';
import { nanoid } from 'nanoid';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { loadedSamples } from './Repl';
import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { Reference } from './Reference';
import { themes } from './themes.mjs';
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
import { useStore } from '@nanostores/react';
export function Footer({ context }) {
const footerContent = useRef();
@@ -72,7 +73,7 @@ export function Footer({ context }) {
<div className="flex justify-between px-2">
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
<FooterTab name="intro" label="welcome" />
<FooterTab name="samples" />
<FooterTab name="sounds" />
<FooterTab name="console" />
<FooterTab name="reference" />
<FooterTab name="settings" />
@@ -84,13 +85,10 @@ export function Footer({ context }) {
)}
</div>
{activeFooter !== '' && (
<div
className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
ref={footerContent}
>
<div className="text-white flex-none h-[360px] overflow-auto max-w-full relative" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'samples' && <SamplesTab />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
</div>
@@ -154,7 +152,7 @@ function WelcomeTab() {
<h3>about</h3>
<p>
strudel is a JavaScript version of{' '}
<a href="tidalcycles.org/" target="_blank">
<a href="https://tidalcycles.org/" target="_blank">
tidalcycles
</a>
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
@@ -173,7 +171,7 @@ function WelcomeTab() {
function ConsoleTab({ log }) {
return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900">
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
@@ -193,36 +191,104 @@ function ConsoleTab({ log }) {
);
}
function SamplesTab() {
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const soundEntries = useMemo(() => {
let filtered = Object.entries(sounds).filter(([key]) => !key.startsWith('_'));
if (!sounds) {
return [];
}
if (soundsFilter === 'user') {
return filtered.filter(([key, { data }]) => !data.prebake);
}
if (soundsFilter === 'drums') {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
}
if (soundsFilter === 'samples') {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
}
if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
return filtered;
}, [sounds, soundsFilter]);
// holds mutable ref to current triggered sound
const trigRef = useRef();
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
trigRef.current = undefined;
t?.then((ref) => {
ref?.stop(getAudioContext().currentTime + 0.01);
});
});
return (
<div id="samples-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
<span>{loadedSamples.length} banks loaded:</span>
{loadedSamples.map(([name, samples]) => (
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
{' '}
{name}(
{Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1}){' '}
</span>
))}
<div id="sounds-tab" className="flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="px-2 pb-2 flex-none">
<ButtonGroup
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
items={{
samples: 'samples',
drums: 'drum-machines',
synths: 'Synths',
user: 'User',
}}
></ButtonGroup>
</div>
<div className="p-2 min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
{soundEntries.map(([name, { data, onTrigger }]) => (
<span
key={name}
className="cursor-pointer hover:opacity-50"
onMouseDown={async () => {
const ctx = getAudioContext();
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
clip: 1,
release: 0.5,
};
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
trigRef.current.then((ref) => {
ref?.node.connect(ctx.destination);
});
}}
>
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
))}
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
</div>
</div>
);
}
function ButtonGroup({ value, onChange, items }) {
return (
<div className="flex grow border border-foreground rounded-md">
<div className="flex max-w-lg">
{Object.entries(items).map(([key, label], i, arr) => (
<button
key={key}
onClick={() => onChange(key)}
className={cx(
'p-2 grow',
i === 0 && 'rounded-l-md',
i === arr.length - 1 && 'rounded-r-md',
value === key ? 'bg-background' : 'bg-lineHighlight',
'px-2 border-b h-8',
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
value === key ? 'border-foreground' : 'border-transparent',
)}
>
{label}
{label.toLowerCase()}
</button>
))}
</div>
@@ -282,6 +348,8 @@ const fontFamilyOptions = {
BigBlueTerminal: 'BigBlueTerminal',
x3270: 'x3270',
PressStart: 'PressStart2P',
galactico: 'galactico',
'we-come-in-peace': 'we-come-in-peace',
};
function SettingsTab({ scheduler }) {
+1 -1
View File
@@ -6,7 +6,7 @@ const visibleFunctions = jsdocJson.docs
export function Reference() {
return (
<div className="flex h-full w-full pt-2 text-foreground">
<div className="w-64 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
{visibleFunctions.map((entry, i) => (
<a key={i} className="cursor-pointer block hover:bg-lineHighlight py-1 px-4" href={`#doc-${i}`}>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
+2 -14
View File
@@ -6,13 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import {
getAudioContext,
getLoadedSamples,
initAudioOnFirstClick,
resetLoadedSamples,
webaudioOutput,
} from '@strudel.cycles/webaudio';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
import React, { createContext, useCallback, useEffect, useState } from 'react';
@@ -53,7 +47,6 @@ evalScope(
...modules,
);
export let loadedSamples = [];
const presets = prebake();
let drawContext, clearCanvas;
@@ -62,11 +55,6 @@ if (typeof window !== 'undefined') {
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
Promise.all([...modules, presets]).then((data) => {
// console.log('modules and sample registry loade', data);
loadedSamples = Object.entries(getLoadedSamples() || {});
});
const getTime = () => getAudioContext().currentTime;
async function initCode() {
@@ -211,7 +199,7 @@ export function Repl({ embedded = false }) {
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
clearCanvas();
resetLoadedSamples();
resetLoadedSounds();
await prebake(); // declare default samples
await evaluate(code, false);
};
+14 -8
View File
@@ -1,22 +1,28 @@
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core';
//import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
return await Promise.all([
samples(`./piano.json`, `./piano/`),
registerSynthSounds();
//registerSoundfonts();
await Promise.all([
samples(`./piano.json`, `./piano/`, { prebake: true }),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/'),
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/'),
samples(`./EmuSP12.json`, `./EmuSP12/`),
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
prebake: true,
tag: 'drum-machines',
}),
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }),
// samples('github:tidalcycles/Dirt-Samples/master'),
]);
}
const maxPan = toMidi('C8');
const maxPan = noteToMidi('C8');
const panwidth = (pan, width) => pan * width + (1 - width) / 2;
Pattern.prototype.piano = function () {
+1 -1
View File
@@ -566,7 +566,7 @@ samples({
"C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
.note().slow(4)
.s('0040_FluidR3_GM_sf2_file')
.s("gm_epiano1:1")
.color('steelblue')
.stack(
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2".scale('C3 major')
+1
View File
@@ -9,6 +9,7 @@ export const defaultSettings = {
fontSize: 18,
latestCode: '',
isZen: false,
soundsFilter: 'all',
};
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
+9 -2
View File
@@ -10,6 +10,14 @@
font-family: 'x3270';
src: url('/fonts/3270/3270-Regular.ttf');
}
@font-face {
font-family: 'galactico';
src: url('/fonts/galactico/Galactico-Basic.otf');
}
@font-face {
font-family: 'we-come-in-peace';
src: url('/fonts/we-come-in-peace/we-come-in-peace-bb.regular.ttf');
}
.cm-gutters {
display: none !important;
@@ -22,7 +30,6 @@
--app-height: 100vh;
}
#console-tab,
#samples-tab {
#console-tab {
font-family: BigBlueTerminal, monospace;
}