Compare commits

..

102 Commits

Author SHA1 Message Date
alex 8c8f6302b7 reinstate weave / weaveWith 2025-10-13 21:57:59 +01:00
Alex McLean 7eebf3a466 Merge pull request 'Add control-metadata' (#1634) from control-metadata into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1634
2025-10-10 11:25:00 +02:00
Alex McLean 64f16c4f33 placate format gods 2025-10-08 15:35:34 +01:00
Alex McLean ffbbc43c89 add cyclist version to the query metadata 2025-10-08 15:34:51 +01:00
Alex McLean 086596c47c fix setControls to do union with existing controls 2025-10-08 14:52:00 +01:00
Alex McLean fdde05e751 add pattern id to query state controls 2025-10-08 14:51:21 +01:00
Alex McLean 694162a7b1 fix purity of 'withState' so it returns a new pattern 2025-10-08 14:13:33 +01:00
Alex McLean cbcb25b063 Merge pull request 'Fix MQTT: change the trigger handler to match new hap' (#1629) from jurrchen/strudel:bugfix/mqtt-hap-sig into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1629
2025-10-07 22:59:38 +02:00
Jieren Chen 0d08225983 change the trigger handler to match new hap 2025-10-05 21:45:23 +02:00
froos 00872bd1de Merge pull request 'Fix case sensitivity for synonym search' (#1618) from vvolhejn/strudel:vv/fix-case-sensitivity into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1618
2025-10-05 21:42:18 +02:00
Václav Volhejn b7d98dd370 Fix case sensitivity for synonym search 2025-10-01 21:53:28 +02:00
Alex McLean 0ecacae71e format 2025-10-01 12:02:09 +01:00
yaxu 8cd497abb2 Update website/src/pages/technical-manual/project-start.mdx 2025-10-01 12:54:58 +02:00
Switch Angel AKA Jade Rose 070055d2ef Merge pull request 'Optimize wavetable synth' (#1613) from glossing/strudel:glossing/wavetable-optimizations into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1613
2025-10-01 08:56:33 +02:00
Switch Angel AKA Jade Rose e500ebe268 Merge branch 'main' into glossing/wavetable-optimizations 2025-10-01 08:54:53 +02:00
Aria 3a7ec18d46 Move invsr to constructor 2025-09-30 23:19:55 -05:00
Aria a2b8407fbb Add back mipmaps; add caching 2025-09-30 23:08:28 -05:00
Switch Angel AKA Jade Rose ac1fd88d8d Merge pull request 'fix: pattern switching' (#1616) from fix_pattern_Switching into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1616
2025-10-01 02:02:30 +02:00
Jade (Rose) Rowland 9f8143e062 format 2025-09-30 19:46:53 -04:00
Jade (Rose) Rowland cdc5e5f3f3 working 2025-09-30 19:45:30 -04:00
Aria bad498a1e5 Remove unused var 2025-09-30 16:21:28 -07:00
Aria 4de0c11f8c Remove mipmaps, fix clamping of parameters 2025-09-29 14:15:50 -07:00
Switch Angel AKA Jade Rose f360d394a7 Merge pull request 'fix wavetable JSON' (#1611) from fix_JSON into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1611
2025-09-29 02:10:53 +02:00
Jade (Rose) Rowland 48718daea8 add_vgame 2025-09-28 19:57:19 -04:00
Jade (Rose) Rowland 8d45016ccf working 2025-09-28 19:11:13 -04:00
Switch Angel AKA Jade Rose ec69f6fd77 Merge pull request 'rename wavetable controls to match existing naming conventions' (#1610) from wavetable_controls into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1610
2025-09-28 21:28:15 +02:00
Jade (Rose) Rowland d94f145649 fix vib 2025-09-28 15:23:58 -04:00
Jade (Rose) Rowland bf3fe605d9 fix unison gain 2025-09-28 15:21:06 -04:00
Jade (Rose) Rowland 54ebc97ddc fix tests 2025-09-28 15:07:35 -04:00
Jade (Rose) Rowland c689874bb4 simplify modulators 2025-09-28 14:57:12 -04:00
Jade (Rose) Rowland 23d4bfa9d9 rename controls 2025-09-28 09:19:39 -04:00
Switch Angel AKA Jade Rose 91eb50f1c1 Merge pull request '<Warn: Don't merge until tables uploaded> Feature: Wavetable synth improvements' (#1607) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1607
2025-09-27 23:28:12 +02:00
Aria 5838ade740 Merge branch 'main' into glossing/wavetable-synth 2025-09-27 13:17:05 -07:00
Aria 917d01c47d Add LFOs and envelopes 2025-09-27 12:48:37 -07:00
Switch Angel AKA Jade Rose 0a51718df3 Merge pull request 'wavetable improvements' (#1608) from wt_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1608
2025-09-27 21:43:09 +02:00
Jade (Rose) Rowland 774372a339 format 2025-09-27 15:29:55 -04:00
Jade (Rose) Rowland 626a99ba5b supports old documented way of loading wavetables: 2025-09-27 15:22:54 -04:00
Aria 8c3ee6db6b Missed deletion 2025-09-27 11:07:04 -07:00
Aria c5c05ebadc Clean up to align with other PR 2025-09-27 11:02:57 -07:00
Aria a8918d55fb Actually update normalizer 2025-09-27 10:45:30 -07:00
Jade (Rose) Rowland a9957b45e5 suppoert existing wt_ api 2025-09-27 13:22:21 -04:00
Jade (Rose) Rowland 4bd103e612 gain adj 2025-09-27 11:31:43 -04:00
Aria 466234bad6 Merge branch 'main' into glossing/wavetable-synth 2025-09-26 21:22:45 -07:00
Aria 97beaec25a Added examples, fixed samplerate issue on import, added to settings tab, fixed spread, added default wavetables, change default phaserand to 0 2025-09-26 20:46:10 -07:00
Jade (Rose) Rowland 2b3b638943 sample source for WT 2025-09-26 21:47:45 -05:00
Switch Angel AKA Jade Rose e1629e8383 Merge pull request 'create orbit based DJ filter' (#1603) from djf into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1603
2025-09-26 08:48:45 +02:00
Jade (Rose) Rowland 266cb614a8 add better example 2025-09-25 23:44:32 -07:00
Jade (Rose) Rowland dfc4a0818c res adjust 2025-09-25 23:39:08 -07:00
Jade (Rose) Rowland 236ff577a6 Merge branch 'main' into djf 2025-09-25 23:18:22 -07:00
Switch Angel AKA Jade Rose 66a32ac45d Merge pull request 'Feature: Wavetable synth' (#1525) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1525
2025-09-26 08:15:48 +02:00
Aria 8b2c35b7a3 Cleanup 2025-09-25 01:10:41 -07:00
Aria f8f42565ae Typo - add back slight delay in cleanup 2025-09-25 00:40:25 -07:00
Aria 96981c3c1d A bit more cleanup 2025-09-25 00:36:28 -07:00
Aria bd8d207a3d Typo 2025-09-25 00:17:13 -07:00
Aria e3741ae8fa Add control over phase randomization and fix a bug with supersaw 2025-09-25 00:05:50 -07:00
Switch Angel AKA Jade Rose ef87f65bef Merge branch 'main' into glossing/wavetable-synth 2025-09-25 07:21:17 +02:00
Aria d3d9f23c3c Remove unnecessary defaulting 2025-09-24 22:06:53 -07:00
Jade (Rose) Rowland c474b8c92e rm comment 2025-09-24 20:33:51 -07:00
Jade (Rose) Rowland da81ba00fb adjust control range 2025-09-24 20:32:45 -07:00
Jade (Rose) Rowland 28c94efaa9 format 2025-09-24 00:24:25 -07:00
Jade (Rose) Rowland 0633954e24 working 2025-09-24 00:23:05 -07:00
Switch Angel AKA Jade Rose cedf09168f Merge pull request 'fix: time signal' (#1583) from fix_time into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1583
2025-09-23 13:28:19 +02:00
Switch Angel AKA Jade Rose 3373225f53 Merge branch 'main' into fix_time 2025-09-23 13:13:51 +02:00
Jade (Rose) Rowland 1eb9dc73fa fix duck 2025-09-22 00:34:57 -04:00
Jade (Rose) Rowland fdcdc3aaa0 rm deadcode 2025-09-22 00:13:59 -04:00
Jade (Rose) Rowland da926805b3 rm dead code 2025-09-22 00:12:48 -04:00
Jade (Rose) Rowland 82893ffc22 fix import 2025-09-22 00:11:02 -04:00
Jade (Rose) Rowland beafd9c700 format 2025-09-22 00:07:06 -04:00
Jade (Rose) Rowland c5bd2a7487 working 2025-09-21 23:54:04 -04:00
froos 118086aff5 Merge pull request 'mondo: fix all + setcpm + setcps' (#1600) from mondo-fixes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1600
2025-09-21 16:16:05 +02:00
Felix Roos 452827630b move unpure up 2025-09-21 16:13:09 +02:00
Felix Roos 86248328cc fix: mondo setcps / setcpm 2025-09-21 16:09:10 +02:00
Felix Roos 8833d623f7 fix: all function in mondo 2025-09-21 16:06:28 +02:00
froos 3fff4609a0 Merge pull request 'fix: osc error message' (#1597) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1597
2025-09-19 21:59:42 +02:00
froos e2b5b92a0c Merge pull request 'osc: --debug flag to see incoming messages' (#1595) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1595
2025-09-18 21:09:55 +02:00
Felix Roos 1c3e07afd3 fix: rephrase error message + bump to 1.2.10 2025-09-18 20:40:25 +02:00
Felix Roos c54fa7d266 chore: bump osc to 1.2.8 2025-09-18 20:39:05 +02:00
Felix Roos a25f763796 docs: add --debug flag to readme 2025-09-18 20:37:19 +02:00
Felix Roos 91ad3d729f feat: osc add --debug flag to log messages + log errors 2025-09-18 20:36:40 +02:00
froos 4cb0488839 Merge pull request 'simplify osc usage' (#1588) from osc-server-via-npx into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1588
2025-09-17 14:12:09 +02:00
Felix Roos 663229ecde bump osc to 1.2.8 2025-09-17 13:50:02 +02:00
Felix Roos cdb623cb66 overhaul osc readme 2025-09-17 13:39:35 +02:00
Felix Roos 28d7e0e489 export osc function to be able to do all(osc) 2025-09-17 13:39:28 +02:00
Felix Roos aba594b72a fix: bin field 2025-09-17 12:52:42 +02:00
Felix Roos 5d7c6c3e4b fix: add node shebang + bump osc to 1.2.6 2025-09-17 12:47:41 +02:00
Felix Roos 820744dc27 bump osc to 1.2.5 2025-09-17 12:42:11 +02:00
Felix Roos 5445f0812f configurable port for osc bridge + add bin field 2025-09-17 12:41:46 +02:00
Jade (Rose) Rowland a68c784753 rm unessecary change 2025-09-16 11:55:31 -04:00
Jade (Rose) Rowland f2b2f9f959 format 2025-09-16 11:53:19 -04:00
Jade (Rose) Rowland 53dd53553a Merge branch 'main' into fix_time 2025-09-16 11:52:54 -04:00
Jade (Rose) Rowland 89b7eac789 fix test 2025-09-16 11:52:36 -04:00
Jade (Rose) Rowland 9cdba1a50e Working 2025-09-16 11:36:21 -04:00
Switch Angel AKA Jade Rose 98b7003504 Merge pull request 'dev: logger errors to console in dev mode' (#1585) from devlog into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1585
2025-09-16 16:59:57 +02:00
Jade (Rose) Rowland 4cc453c640 fix test 2025-09-15 22:09:05 -04:00
Jade (Rose) Rowland 74e27ca94f coerce in correct place 2025-09-15 21:57:56 -04:00
Jade (Rose) Rowland 7dfd7dbcdc rmconsole 2025-09-15 21:47:40 -04:00
Jade (Rose) Rowland 2cba1dcb31 working 2025-09-15 21:46:37 -04:00
Aria 46a45b4596 Some missing cleanup 2025-09-04 20:51:06 -05:00
Aria 1205573bc6 Merge branch 'main' into glossing/wavetable-synth 2025-09-04 20:31:01 -05:00
Aria fe46e1da53 Update docstring to include new warp modes; save sample server for a separate PR 2025-08-28 16:28:33 -05:00
Aria d1869c18ba Remove internal LFO 2025-08-28 16:25:10 -05:00
Aria 02cd79a6d9 Add wavetable oscillator with scanning, warps, and detune 2025-08-28 16:25:10 -05:00
30 changed files with 1866 additions and 396 deletions
+234 -1
View File
@@ -87,6 +87,239 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* Position in the wavetable of the wavetable oscillator
*
* @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/**
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
*
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
*
* @name warpmode
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
*
*/
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
/**
* Amount of randomness of the initial phase of the wavetable oscillator.
*
* @name wtphaserand
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
*
*/
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/**
* Define a custom webaudio node to use as a sound source.
*
@@ -1128,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
*
*/
export const { djf } = registerControl('djf');
+1 -1
View File
@@ -57,7 +57,7 @@ export class Cyclist {
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
+2
View File
@@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;
+1 -2
View File
@@ -38,8 +38,7 @@ export class NeoCyclist {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
+20 -22
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import TimeSpan from './timespan.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@@ -98,10 +98,7 @@ export class Pattern {
// runs func on query state
withState(func) {
return this.withHaps((haps, state) => {
func(state);
return haps;
});
return new Pattern((state) => this.query(func(state)));
}
/**
@@ -966,26 +963,13 @@ export const arp = register(
false,
);
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
*/
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
export const weaveWith = register('weaveWith', function (t, funcs, pat) {
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
@@ -993,13 +977,27 @@ addToPrototype('weaveWith', function (t, ...funcs) {
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, [s("bd(3,8)"), s("~ sd")])
* @example n("0 1 2 3 4 5 6 7").weave(8, [s("bd(3,8)"), s("~ sd")])
*/
export const weave = register('weave', function (t, pats, pat) {
return pat.weaveWith(
t,
pats.map((x) => (y) => y.set.out(x)),
);
});
//////////////////////////////////////////////////////////////////////
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object';
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
+21 -3
View File
@@ -74,6 +74,14 @@ export function repl({
return silence;
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
return pat.__pure;
}
return pat;
}
const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
@@ -85,7 +93,10 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCps = (cps) => {
scheduler.setCps(unpure(cps));
return silence;
};
/**
* Changes the global tempo to the given cycles per minute
@@ -97,7 +108,10 @@ export function repl({
* setcpm(140/4) // =140 bpm in 4/4
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const setCpm = (cpm) => {
scheduler.setCps(unpure(cpm) / 60);
return silence;
};
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@@ -200,7 +214,10 @@ export function repl({
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
let patterns = [];
for (const [key, value] of Object.entries(pPatterns)) {
patterns.push(value.withState((state) => state.setControls({ id: key })));
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
@@ -214,6 +231,7 @@ export function repl({
pattern = allTransforms[i](pattern);
}
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
+2 -2
View File
@@ -19,9 +19,9 @@ export class State {
return this.setSpan(func(this.span));
}
// Returns new State with different controls
// Returns new State with added controls.
setControls(controls) {
return new State(this.span, controls);
return new State(this.span, { ...this.controls, ...controls });
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ function evaluator(node, scope) {
let pat;
if (type === 'plain' && typeof variable !== 'undefined') {
// some function names are not patternable, so we skip reification here
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
return variable;
}
pat = reify(variable);
+1 -1
View File
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
const onTrigger = (hap, currentTime, cps, targetTime) => {
let msg_topic = topic;
if (!cx || !cx.isConnected()) {
return;
+27 -17
View File
@@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
## Usage
OSC will only work if you run the REPL locally + the OSC server besides it:
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
From the project root:
```js
npm run repl
```sh
npx @strudel/osc
```
and in a seperate shell:
```js
npm run osc
```
This should give you
You should see something like:
```log
osc client running on port 57120
@@ -26,14 +18,32 @@ osc server running on port 57121
websocket server running on port 8080
```
Now open Supercollider (with the super dirt startup file)
### --port
Now open the REPL and type:
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
```js
s("<bd sd> hh").osc()
```sh
npx @strudel/osc --port 7771 # classic dirt
```
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
### --debug
To log all incoming osc messages, add the `--debug` flag:
```sh
npx @strudel/osc --debug
```
## Usage in Strudel
To test it in strudel, you have can use `all(osc)` to send all events through osc:
```js
$: s("bd*4")
all(osc)
```
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+2 -4
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
@@ -81,6 +81,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this.onTrigger(oscTrigger);
};
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
+2 -1
View File
@@ -1,8 +1,9 @@
{
"name": "@strudel/osc",
"version": "1.2.4",
"version": "1.2.10",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
+43 -2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env node
/*
server.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
@@ -6,6 +8,19 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
let udpClientPort = Number(getArgValue('--port')) || 57120;
let debug = Number(getArgValue('--debug')) || 0;
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
@@ -17,7 +32,7 @@ const config = {
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
port: udpClientPort, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
@@ -27,8 +42,34 @@ const config = {
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
osc.open(); // start a WebSocket server on port 8080
if (debug) {
osc.on('*', (message) => {
const { address, args } = message;
let str = '';
for (let i = 0; i < args.length; i += 2) {
str += `${args[i]}: ${args[i + 1]} `;
}
console.log(`${address} ${str}`);
});
}
osc.on('error', (message) => {
if (message.toString().includes('EADDRINUSE')) {
console.log(`------ ERROR -------
a server is already running on port 57121! to stop it:
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
2. re-run the osc server
`);
} else {
console.log(message);
}
});
osc.open();
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
if (debug) {
console.log('debug logs enabled. incoming messages will appear below');
}
+97 -2
View File
@@ -1,5 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -10,6 +10,13 @@ export function gainNode(value) {
return node;
}
export function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
@@ -21,7 +28,9 @@ const getSlope = (y1, y2, x1, x2) => {
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
if (value !== undefined) {
node.parameters.get(key).value = value;
}
});
return node;
}
@@ -88,6 +97,35 @@ export const getParamADSR = (
param[ramp](min, end + release);
};
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,
@@ -115,6 +153,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
if (amount == null) {
const hasADSRParams = values.some((p) => p != null);
amount = hasADSRParams ? defaultAmount : 0;
}
const min = offset ?? 0;
const max = amount + min;
const diff = Math.abs(max - min);
if (diff) {
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
@@ -307,3 +380,25 @@ export function applyFM(param, value, begin) {
}
return { stop };
}
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
export const destroyAudioWorkletNode = (node) => {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
};
+1
View File
@@ -11,3 +11,4 @@ export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './wavetable.mjs';
+29 -39
View File
@@ -1,5 +1,5 @@
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -22,39 +22,16 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
sampleUrl = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, sampleUrl, index, midi, label, playbackRate };
return { transpose, url, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
@@ -79,14 +56,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.buffer = buffer;
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
const loop = hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
@@ -267,16 +244,12 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
return samples(json, baseUrl || base, options);
}
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
baseUrl,
prebake,
tag,
}),
(key, bank) => {
registerSampleSource(key, bank, { baseUrl, prebake, tag });
},
baseUrl,
);
};
@@ -366,3 +339,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
return handle;
}
function registerSample(key, bank, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
registerWaveTable(key, bank, params);
} else {
registerSample(key, bank, params);
}
}
+38 -205
View File
@@ -7,12 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
@@ -28,13 +29,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -301,87 +295,16 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
let controller;
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
return controller;
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(ctx);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(ctx, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
export const panic = () => {
if (destinationGain == null) {
return;
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
let delayNode = orbits[orbit].delayNode;
if (delayNode === undefined) {
const ac = getAudioContext();
delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(delayNode, orbit);
orbits[orbit].delayNode = delayNode;
}
delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t);
delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t);
return delayNode;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -415,97 +338,6 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
// type orbit {
// output: GainNode,
// reverbNode: ConvolverNode
// delayNode: FeedbackDelayNode
// }
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].output);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
// Setup output node through which all audio filters prior to hitting
// the destination (and thus allows for global volume automation)
output: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].output, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [duckdepth].flat();
targetArr.forEach((target, idx) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
const gainParam = orbits[target].output.gain;
webAudioTimeout(
audioContext,
() => {
const now = audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
let reverbNode = orbits[orbit].reverbNode;
if (reverbNode === undefined) {
const ac = getAudioContext();
reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverbNode, orbit);
orbits[orbit].reverbNode = reverbNode;
}
if (
hasChanged(duration, reverbNode.duration) ||
hasChanged(fade, reverbNode.fade) ||
hasChanged(lp, reverbNode.lp) ||
hasChanged(dim, reverbNode.dim) ||
hasChanged(irspeed, reverbNode.irspeed) ||
hasChanged(irbegin, reverbNode.irbegin) ||
reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return reverbNode;
}
export let analysers = {},
analysersData = {};
@@ -538,15 +370,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
return analysersData[id];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export function resetGlobalEffects() {
orbits = {};
controller?.reset();
analysers = {};
analysersData = {};
}
@@ -561,6 +386,7 @@ function mapChannelNumbers(channels) {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
let { stretch } = value;
if (stretch != null) {
@@ -603,6 +429,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
duckonset,
duckattack,
duckdepth,
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
@@ -679,10 +506,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
const orbitBus = audioController.getOrbit(orbit, channels);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth);
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
@@ -729,7 +555,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) {
sourceNode = soundHandle.node;
@@ -827,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
@@ -872,14 +706,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(post);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
orbitBus.getDelay(delaytime, delayfeedback, t);
orbitBus.sendDelay(post, delay);
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
@@ -892,25 +723,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
orbitBus.sendReverb(post, room);
}
if (djf != null) {
orbitBus.getDjf(djf, t);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
const analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
orbitBus.connectToOutput(dryGain);
} else {
connectToOrbit(post, orbit);
orbitBus.connectToOutput(post);
}
// connect chain elements together
+209
View File
@@ -0,0 +1,209 @@
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
let hasChanged = (now, before) => now !== undefined && now !== before;
export class Orbit {
reverbNode;
delayNode;
output;
summingNode;
djfNode;
audioContext;
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output);
}
disconnect() {
this.output.disconnect();
this.summingNode.disconnect();
this.delayNode?.disconnect();
this.reverbNode?.disconnect();
}
getDjf(value, t = 0) {
if (this.djfNode == null) {
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
this.summingNode.disconnect();
this.summingNode.connect(this.djfNode);
this.djfNode.connect(this.output);
}
const val = this.djfNode.parameters.get('value');
val.setValueAtTime(value, t);
}
getDelay(delaytime = 0, feedback = 0.5, t) {
const maxfeedback = 0.98;
if (feedback > maxfeedback) {
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
}
feedback = clamp(feedback, 0, 0.98);
if (this.delayNode == null) {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (this.reverbNode == null) {
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
this.reverbNode.connect(this.summingNode);
}
if (
hasChanged(duration, this.reverbNode.duration) ||
hasChanged(fade, this.reverbNode.fade) ||
hasChanged(lp, this.reverbNode.lp) ||
hasChanged(dim, this.reverbNode.dim) ||
hasChanged(irspeed, this.reverbNode.irspeed) ||
hasChanged(irbegin, this.reverbNode.irbegin) ||
this.reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return this.reverbNode;
}
sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) {
effectSend(node, this.delayNode, amount);
}
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const onset = onsettime;
const attack = Math.max(attacktime, 0.002);
const gainParam = this.output.gain;
webAudioTimeout(
this.audioContext,
() => {
const now = this.audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
}
connectToOutput(node) {
node.connect(this.summingNode);
}
}
export class SuperdoughOutput {
channelMerger;
destinationGain;
constructor(audioContext) {
this.audioContext = audioContext;
this.initializeAudio();
}
initializeAudio() {
const audioContext = this.audioContext;
const maxChannelCount = audioContext.destination.maxChannelCount;
this.audioContext.destination.channelCount = maxChannelCount;
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
this.destinationGain = new GainNode(audioContext);
this.channelMerger.connect(this.destinationGain);
this.destinationGain.connect(audioContext.destination);
}
reset() {
this.disconnect();
this.initializeAudio();
}
disconnect() {
this.channelMerger.disconnect();
this.destinationGain.disconnect();
this.destinationGain = null;
this.channelMerger = null;
}
connectToDestination = (input, channels = [0, 1]) => {
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(this.audioContext);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(this.audioContext, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
});
};
}
export class SuperdoughAudioController {
audioContext;
output;
nodes = {};
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new SuperdoughOutput(audioContext);
}
reset() {
Array.from(this.nodes).forEach((node) => {
node.disconnect();
});
this.nodes = {};
this.output.reset();
}
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
const targetArr = [targetOrbits].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [depth].flat();
targetArr.forEach((target, idx) => {
const orbit = this.nodes[target];
if (orbit == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
orbit.duck(t, onset, attack, depth);
});
}
getOrbit(orbitNum, channels) {
if (this.nodes[orbitNum] == null) {
this.nodes[orbitNum] = new Orbit(this.audioContext);
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
}
return this.nodes[orbitNum];
}
}
+6 -24
View File
@@ -1,39 +1,21 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import { clamp } from './util.mjs';
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
+29
View File
@@ -76,3 +76,32 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) {
return t * cps;
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
+333
View File
@@ -0,0 +1,333 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
const seenKeys = new Set();
async function getPayload(url, label, frameLen = 2048) {
const key = `${url},${frameLen}`;
if (!seenKeys.has(key)) {
const buf = await loadBuffer(url, label);
const ch0 = buf.getChannelData(0);
const total = ch0.length;
const numFrames = Math.max(1, Math.floor(total / frameLen));
const frames = new Array(numFrames);
for (let i = 0; i < numFrames; i++) {
const start = i * frameLen;
frames[i] = ch0.subarray(start, start + frameLen);
}
seenKeys.add(key);
return { frames, frameLen, numFrames, key };
}
return { frameLen, key }; // worklet will use the cached version
}
function humanFileSize(bytes, si) {
var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while (bytes >= thresh);
return bytes.toFixed(1) + ' ' + units[u];
}
// Extract the sample rate of a .wav file
function parseWavSampleRate(arrBuf) {
const dv = new DataView(arrBuf);
// Header is "RIFF<chunk size (4 bytes)>WAVE", so 12 bytes
let p = 12;
// Look through chunks for the format header
// (they will always have an 8 byte header (id and size) followed by a payload)
while (p + 8 <= dv.byteLength) {
// Parse id
const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3));
// Parse chunk size
const size = dv.getUint32(p + 4, true);
if (id === 'fmt ') {
// The format chunk contains the sample rate after
// 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels
// (for a total of 12)
return dv.getUint32(p + 12, true);
}
// Advance to next chunk
p += 8 + size + (size & 1);
}
return null;
}
async function decodeAtNativeRate(arr) {
const sr = parseWavSampleRate(arr) || 44100;
const tempAC = new OfflineAudioContext(1, 1, sr);
return await tempAC.decodeAudioData(arr);
}
const loadCache = {};
const loadBuffer = (url, label) => {
url = url.replace('#', '%23');
if (!loadCache[url]) {
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
const timestamp = Date.now();
loadCache[url] = fetch(url)
.then((res) => res.arrayBuffer())
.then(async (res) => {
const took = Date.now() - timestamp;
const size = humanFileSize(res.byteLength);
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
const decoded = await decodeAtNativeRate(res);
return decoded;
});
}
return loadCache[url];
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
const _processTables = (json, baseUrl, frameLen, options = {}) => {
baseUrl = json._base || baseUrl;
return Object.entries(json).forEach(([key, tables]) => {
if (key === '_base') return false;
if (typeof tables === 'string') {
tables = [tables];
}
if (typeof tables !== 'object') {
throw new Error('wrong json format for ' + key);
}
let resolvedUrl = baseUrl;
if (resolvedUrl.startsWith('github:')) {
resolvedUrl = githubPath(resolvedUrl, '');
}
tables = tables
.map((t) => resolvedUrl + t)
.filter((t) => {
if (!t.toLowerCase().endsWith('.wav')) {
logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`);
return false;
}
return true;
});
if (tables.length) {
registerWaveTable(key, tables, { baseUrl, frameLen });
}
});
};
export function registerWaveTable(key, tables, params) {
registerSound(
key,
(t, hapValue, onended, cps) => {
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
},
{
type: 'wavetable',
tables,
...params,
},
);
}
/**
* Loads a collection of wavetables to use with `s`
*
* @name tables
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(url)
.then((res) => res.json())
.then((json) => _processTables(json, url, frameLen, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
};
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const { s, n = 0, duration, clip } = value;
const ac = getAudioContext();
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let { warpmode } = value;
if (typeof warpmode === 'string') {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration;
if (clip !== undefined) {
holdEnd = Math.min(t + clip * duration, holdEnd);
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'table', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
}
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
let wtrate = value.wtrate;
if (value.wtsync != null) {
wtrate = cps * value.wtsync;
}
const wtPosModulators = applyParameterModulators(
ac,
positionParam,
t,
endWithRelease,
{
offset: value.wt,
amount: value.wtenv,
defaultAmount: 0.5,
shape: 'linear',
values: posADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: wtrate,
depth: value.wtdepth,
defaultDepth: 0.5,
shape: value.wtshape,
skew: value.wtskew,
dcoffset: value.wtdc ?? 0,
},
);
let warprate = value.warprate;
if (value.warpsync != null) {
warprate = warprate = cps * value.warpsync;
}
const wtWarpModulators = applyParameterModulators(
ac,
warpParam,
t,
endWithRelease,
{
offset: value.warp,
amount: value.warpenv,
defaultAmount: 0.5,
shape: 'linear',
values: warpADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: warprate,
depth: value.warpdepth,
defaultDepth: 0.5,
shape: value.warpshape,
skew: value.warpskew,
dcoffset: value.warpdc ?? 0,
},
);
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const envGain = ac.createGain();
const node = source.connect(envGain);
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
const handle = { node, source };
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
envEnd,
);
handle.stop = (time) => {
timeoutNode.stop(time);
};
return handle;
}
+445 -38
View File
@@ -6,7 +6,21 @@ import OLAProcessor from './ola-processor';
import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a;
const pv = (arr, n) => arr[n] ?? arr[0];
const frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; // fast floor for non-negative
const getUnisonDetune = (unison, detune, voiceIndex) => {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
};
const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12);
};
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
@@ -123,7 +137,7 @@ class LFOProcessor extends AudioWorkletProcessor {
}
}
process(inputs, outputs, parameters) {
process(_inputs, outputs, parameters) {
const begin = parameters['begin'][0];
if (currentTime >= parameters.end[0]) {
return false;
@@ -150,7 +164,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0;
if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1);
this.phase = mod(time * frequency + phaseoffset, 1);
}
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
@@ -271,6 +285,73 @@ class ShapeProcessor extends AudioWorkletProcessor {
}
registerProcessor('shape-processor', ShapeProcessor);
class TwoPoleFilter {
s0 = 0;
s1 = 0;
update(s, cutoff, resonance = 0) {
// Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf
return this.s1; // return lpf by default
}
}
class DJFProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{ name: 'value', defaultValue: 0.5 }];
}
constructor() {
super();
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
this.started = hasInput;
const value = clamp(parameters.value[0], 0, 1);
let filterType = 'none';
let cutoff;
let v = 1;
if (value > 0.51) {
filterType = 'hipass';
v = (value - 0.5) * 2;
} else if (value < 0.49) {
filterType = 'lopass';
v = value * 2;
}
cutoff = Math.pow(v * 11, 4);
for (let i = 0; i < input.length; i++) {
for (let n = 0; n < blockSize; n++) {
if (filterType == 'none') {
output[i][n] = input[i][n];
} else {
this.filters[i].update(input[i][n], cutoff, 0.1);
if (filterType === 'lopass') {
output[i][n] = this.filters[i].s1;
} else if (filterType === 'hipass') {
output[i][n] = input[i][n] - this.filters[i].s1;
} else {
output[i][n] = input[i][n];
}
}
}
}
return true;
}
}
registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
@@ -378,21 +459,6 @@ class DistortProcessor extends AudioWorkletProcessor {
registerProcessor('distort-processor', DistortProcessor);
// SUPERSAW
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
function applySemitoneDetuneToFrequency(frequency, detune) {
return frequency * Math.pow(2, detune / 12);
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
@@ -444,7 +510,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
},
];
}
process(input, outputs, params) {
process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
@@ -454,29 +520,31 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
}
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
const voices = pv(params.voices, i);
const freqspread = pv(params.freqspread, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
let freq = pv(params.frequency, i);
// Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// Individual voice detuning
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
const dt = mod(freqVoice / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
@@ -907,3 +975,342 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
}
registerProcessor('byte-beat-processor', ByteBeatProcessor);
export const WarpMode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
function hash32(u) {
u = u + 0x7ed55d16 + (u << 12);
u = u ^ 0xc761c23c ^ (u >>> 19);
u = u + 0x165667b1 + (u << 5);
u = (u + 0xd3a2646c) ^ (u << 9);
u = u + 0xfd7046c5 + (u << 3);
u = u ^ 0xb55a4f09 ^ (u >>> 16);
return u >>> 0;
}
const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
function bitReverse(i, n) {
let r = 0;
for (let b = 0; b < n; b++) {
r = (r << 1) | (i & 1);
i >>>= 1;
}
return r;
}
function noise(x) {
const i = Math.floor(x),
f = x - i;
const a = hash01(i),
b = hash01(i + 1);
return a + (b - a) * f;
}
function brownian(x, oct = 4) {
let amp = 0.5,
sum = 0,
norm = 0,
freq = 1;
for (let o = 0; o < oct; o++) {
sum += amp * noise(x * freq);
norm += amp;
amp *= 0.5;
freq *= 2;
}
return (sum / norm) * 2 - 1;
}
const tablesCache = {};
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
{ name: 'detune', defaultValue: 0.18 },
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
];
}
constructor(options) {
super(options);
this.frameLen = 0;
this.numFrames = 0;
this.phase = [];
this.invSR = 1 / sampleRate;
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'table') {
const key = payload.key;
this.frameLen = payload.frameLen;
if (!tablesCache[key]) {
const tables = [payload.frames];
let table = tables[0];
for (let level = 1; level < 1; level++) {
const nextLen = table.length >> 1;
const nextTable = table.map((frame) => {
const avg = new Float32Array(nextLen);
for (let i = 0; i < nextLen; i++) {
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
}
return avg;
});
tables.push(nextTable);
table = nextTable;
if (nextLen <= 32) break;
}
tablesCache[key] = tables;
}
this.tables = tablesCache[key];
this.numFrames = this.tables[0].length;
}
};
}
_mirror(x) {
return 1 - Math.abs(2 * x - 1);
}
_toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt;
return { b, n: Math.round(Math.pow(2, b)) };
}
_warpPhase(phase, amt, mode) {
switch (mode) {
case WarpMode.NONE: {
return phase;
}
case WarpMode.ASYM: {
const a = 0.01 + 0.99 * amt;
return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
}
case WarpMode.MIRROR: {
// Asym, then mirror
return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
}
case WarpMode.BENDP: {
return Math.pow(phase, 1 + 3 * amt);
}
case WarpMode.BENDM: {
return Math.pow(phase, 1 / (1 + 3 * amt));
}
case WarpMode.BENDMP: {
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
}
case WarpMode.SYNC: {
const syncRatio = Math.pow(16, amt * amt);
return (phase * syncRatio) % 1;
}
case WarpMode.QUANT: {
const { n } = this._toBits(amt);
return ffloor(phase * n) / n;
}
case WarpMode.FOLD: {
const K = 7;
const k = 1 + Math.max(1, Math.round(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2;
}
case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
if (phase < w) return (phase / w) * 0.5;
return 0.5 + ((phase - w) / (1 - w)) * 0.5;
}
case WarpMode.ORBIT: {
const depth = 0.5 * amt;
const n = 3;
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.SPIN: {
const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt;
const logistic = r * phase * (1 - phase);
return clamp((1 - amt) * phase + amt * logistic, 0, 1);
}
case WarpMode.PRIMES: {
const isPrime = (n) => {
if (n < 2) return false;
if (n % 2 === 0) return n === 2;
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
return true;
};
let { n } = this._toBits(amt, 3);
while (!isPrime(n)) n++;
return ffloor(phase * n) / n;
}
case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3);
b = Math.round(b);
const n = 1 << b;
const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b);
return ridx / n;
}
case WarpMode.MODULAR: {
const { n } = this._toBits(amt);
const depth = 0.5 * amt;
const jump = frac(phase * n) / n;
return frac(phase + depth * jump);
}
case WarpMode.BROWNIAN: {
const disp = 0.25 * amt * brownian(64 * phase, 4);
return frac(phase + disp);
}
case WarpMode.RECIPROCAL: {
const g = 2 + 4 * amt;
const num = phase * g;
const den = phase + (1 - phase) * g;
const y = den > 1e-12 ? num / den : 0;
return clamp(y, 0, 1);
}
case WarpMode.WORMHOLE: {
const gap = clamp(0.8 * amt, 0, 1);
const a = 0.5 * (1 - gap);
const b = 0.5 * (1 + gap);
if (phase < a) return (phase / a) * 0.5;
if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
return 0.5;
}
case WarpMode.LOGISTIC: {
let x = phase;
const r = 3.6 + 0.4 * amt;
const iters = 1 + Math.round(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1);
}
case WarpMode.SIGMOID: {
const k = 1 + 10 * amt;
const x = phase - 0.5;
const y = 1 / (1 + Math.exp(-k * x));
const y0 = 1 / (1 + Math.exp(0.5 * k));
const y1 = 1 / (1 + Math.exp(-0.5 * k));
return (y - y0) / (y1 - y0);
}
case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
return frac(phase + d);
}
case WarpMode.FLIP: {
return phase;
}
default:
return phase;
}
}
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
const i = pos | 0;
const frac = pos - i;
const a = frame[i];
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
const b = frame[i1];
return a + (b - a) * frac;
}
_chooseMip(dphi) {
const approxHarm = clamp(dphi, 1e-6, 64);
let level = 0;
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
level++;
}
return level;
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true;
}
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
if (!this.tables) {
outL.fill(0);
if (outR !== outL) outR.set(outL);
return true;
}
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0;
const frac = idx - fIdx;
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = s0 + (s1 - s0) * frac;
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s;
}
outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
}
}
return true;
}
}
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
+260 -20
View File
@@ -2831,26 +2831,38 @@ exports[`runs examples > example "distort" example index 1 1`] = `
exports[`runs examples > example "djf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 1/15/4 | n:0 s:superzow octave:3 djf:0.25 ]",
"[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]",
"[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]",
"[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 9/45/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]",
"[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]",
"[ 7/215/4 | n:7 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]",
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
"[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]",
"[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]",
"[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]",
"[ 5/83/4 | note:F3 s:supersaw djf:0.5 ]",
"[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]",
"[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]",
"[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]",
"[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]",
"[ 11/83/2 | note:F3 s:supersaw djf:0.3 ]",
"[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]",
"[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]",
"[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]",
"[ 2/117/8 | note:Eb5 s:supersaw djf:0.2 ]",
"[ 17/89/4 | note:D5 s:supersaw djf:0.2 ]",
"[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]",
"[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]",
"[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]",
"[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]",
"[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]",
"[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]",
"[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]",
"[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]",
"[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]",
"[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]",
"[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]",
"[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]",
]
`;
@@ -11777,6 +11789,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = `
]
`;
exports[`runs examples > example "warp" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
]
`;
exports[`runs examples > example "warpmode" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
"[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
]
`;
exports[`runs examples > example "wchoose" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:c2 s:sine ]",
@@ -11981,6 +12099,128 @@ exports[`runs examples > example "withValue" example index 0 1`] = `
]
`;
exports[`runs examples > example "wt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
]
`;
exports[`runs examples > example "wtphaserand" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]",
]
`;
exports[`runs examples > example "xfade" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0 ]",
+28
View File
@@ -0,0 +1,28 @@
{
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/",
"wt_digital": [
"wt_digital/wt_bad_day.wav",
"wt_digital/wt_basique.wav",
"wt_digital/wt_crickets.wav",
"wt_digital/wt_curses.wav",
"wt_digital/wt_echoes.wav"
],
"wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"],
"wt_digital_basique": ["wt_digital/wt_basique.wav"],
"wt_digital_crickets": ["wt_digital/wt_crickets.wav"],
"wt_digital_curses": ["wt_digital/wt_curses.wav"],
"wt_digital_echoes": ["wt_digital/wt_echoes.wav"],
"wt_vgame": [
"wt_vgame/wt_vgame10.wav",
"wt_vgame/wt_vgame11.wav",
"wt_vgame/wt_vgame12.wav",
"wt_vgame/wt_vgame13.wav",
"wt_vgame/wt_vgame14.wav",
"wt_vgame/wt_vgame15.wav",
"wt_vgame/wt_vgame16.wav",
"wt_vgame/wt_vgame17.wav",
"wt_vgame/wt_vgame18.wav",
"wt_vgame/wt_vgame19.wav",
"wt_vgame/wt_vgame20.wav"
]
}
@@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro
This Guide shows you the different ways to get started with using Strudel in your own project.
## Respect the license
First, please take a moment to understand Strudel's free/open source license,
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
Here is a lay summary, but check the license for legal definitions and responsibilities.
- You can distribute modified versions if you keep track of the changes and the date you made them.
- You must license derivative work under the same license.
- Source code must be distributed along with web publication.
Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license.
This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
## Embedding the Strudel REPL
There are 3 quick ways to embed strudel in your website:
@@ -44,10 +44,10 @@ export function Reference() {
return true;
}
const lowCaseSearch = search.toLowerCase();
const lowerCaseSearch = search.toLowerCase();
return (
entry.name.toLowerCase().includes(lowCaseSearch) ||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search]);
@@ -44,6 +44,9 @@ export function SoundsTab() {
if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
if (soundsFilter === soundFilterType.WAVETABLES) {
return filtered.filter(([_, { data }]) => data.type === 'wavetable');
}
//TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') {
return [];
@@ -54,6 +57,9 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound
const trigRef = useRef();
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
@@ -74,6 +80,7 @@ export function SoundsTab() {
samples: 'samples',
drums: 'drum-machines',
synths: 'Synths',
wavetables: 'Wavetables',
user: 'User',
importSounds: 'import-sounds',
}}
@@ -110,11 +117,13 @@ export function SoundsTab() {
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
n: soundPreviewIdx,
clip: 1,
release: 0.5,
sustain: 1,
duration: 0.5,
};
soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
@@ -126,6 +135,7 @@ export function SoundsTab() {
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
);
+2 -8
View File
@@ -1,4 +1,4 @@
import { registerSound, onTriggerSample } from '@strudel/webaudio';
import { registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core';
@@ -76,13 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
})
.map((title) => titlePathMap.get(title));
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
registerSampleSource(key, value, { prebake: false });
});
logger('imported sounds registered!', 'success');
+3
View File
@@ -32,6 +32,9 @@ export async function prebake() {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
prebake: true,
}),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
+1
View File
@@ -13,6 +13,7 @@ export const soundFilterType = {
DRUMS: 'drums',
SAMPLES: 'samples',
SYNTHS: 'synths',
WAVETABLES: 'wavetables',
ALL: 'all',
};