mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-19 04:06:16 -04:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37990b459f | |||
| a34e58aa09 | |||
| 8cbe4b945c | |||
| 6f9a1470e7 | |||
| 8a3719752f | |||
| 96835be056 | |||
| 960f1de9d2 | |||
| 12884cba3a | |||
| 20c513d649 | |||
| e89414e400 | |||
| 765de9daf3 | |||
| 132dea8478 | |||
| b889d19493 | |||
| 71d36b79ed | |||
| d7bf5db8be | |||
| 48231ea12a | |||
| c8a442234e | |||
| efda32eedb | |||
| 95d04d1149 | |||
| 358b1e3f56 | |||
| fe7e8f7add | |||
| 45d329d3d1 | |||
| fab8d2193d | |||
| 150835aa4d | |||
| 4374bdf1bd | |||
| c4826b7dfb | |||
| bd61c1dc7e | |||
| 1225d04437 | |||
| f6f507dd29 | |||
| c7586e96ff | |||
| 46291bf85c | |||
| 36587ae747 | |||
| 6c01c16a0c | |||
| be36898803 | |||
| df047fd48d | |||
| 68840de188 | |||
| 020a6bc75f | |||
| 7bc6fcbf2a | |||
| 92a82ce4bd | |||
| b2ce4591d9 | |||
| 1be9f40574 |
+22
-17
@@ -4,20 +4,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi
|
||||
|
||||
## Move to codeberg
|
||||
|
||||
We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us.
|
||||
|
||||
To update your local clone, you can run this command:
|
||||
|
||||
```
|
||||
git remote set-url origin git@codeberg.org:uzu/strudel.git
|
||||
```
|
||||
|
||||
Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github.
|
||||
|
||||
## Communication Channels
|
||||
|
||||
To get in touch with the contributors, either
|
||||
To get in touch with the community, either
|
||||
|
||||
- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel
|
||||
- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel)
|
||||
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
|
||||
|
||||
## Ask a Question
|
||||
@@ -36,22 +29,35 @@ Use one of the Communication Channels listed above and drop us a line or two!
|
||||
## Share Music
|
||||
|
||||
If you made some music with strudel, you can give back some love and share what you've done!
|
||||
Your creation could also be part of the random selection in the REPL if you want.
|
||||
Use one of the Communication Channels listed above.
|
||||
(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse)
|
||||
|
||||
## Improve the Docs
|
||||
|
||||
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
|
||||
you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar)
|
||||
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place.
|
||||
|
||||
## Propose a Feature
|
||||
|
||||
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above.
|
||||
Maybe you even want to help with the implementation of that feature!
|
||||
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature!
|
||||
|
||||
## Contribute a feature
|
||||
|
||||
Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that.
|
||||
|
||||
At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion.
|
||||
|
||||
### AI/LLM policy
|
||||
|
||||
Strudel is a project handmade by humans, with thought and nuance.
|
||||
|
||||
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
|
||||
|
||||
There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels.
|
||||
|
||||
## Report a Bug
|
||||
|
||||
If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues).
|
||||
|
||||
Please check that it has not been reported before.
|
||||
|
||||
## Fix a Bug
|
||||
@@ -118,8 +124,7 @@ There are also eslint extensions / plugins for most editors.
|
||||
|
||||
## Running all CI Checks
|
||||
|
||||
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||
You can run the same check with `pnpm check`
|
||||
When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`.
|
||||
|
||||
## Package Workflow
|
||||
|
||||
|
||||
@@ -2753,8 +2753,13 @@ export const as = register('as', (mapping, pat) => {
|
||||
mapping = Array.isArray(mapping) ? mapping : [mapping];
|
||||
return pat.fmap((v) => {
|
||||
v = Array.isArray(v) ? v : [v];
|
||||
v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]]));
|
||||
return v;
|
||||
const entries = [];
|
||||
for (let i = 0; i < mapping.length; ++i) {
|
||||
if (v[i] !== undefined) {
|
||||
entries.push([getControlName(mapping[i]), v[i]]);
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(entries);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2786,3 +2791,17 @@ export const scrub = register(
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Transient shaper. Gives independent control over the emphasis on transients
|
||||
* and sustains
|
||||
*
|
||||
* @name transient
|
||||
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
|
||||
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
|
||||
* @example
|
||||
* s("bd").transient("<-1 -0.5 0 0.5 1>")
|
||||
* @example
|
||||
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
|
||||
*/
|
||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { register, reify, Pattern } from './pattern.mjs';
|
||||
|
||||
let timelines = {};
|
||||
|
||||
export const reset_state = function () {
|
||||
reset_timelines();
|
||||
};
|
||||
|
||||
export const reset_timelines = function () {
|
||||
timelines = {};
|
||||
};
|
||||
|
||||
/***
|
||||
* Allows you to switch a pattern between different 'timelines'. This is particularly useful when
|
||||
* live coding, for example when you want to cue a pattern up to play from its start.
|
||||
*
|
||||
* Timelines are specified by number, so that if you had a pattern like
|
||||
* `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1'
|
||||
* to '2', it would always align '0' to the nearest cycle. You will likely want to trigger
|
||||
* an evaluation a little bit before the cycle starts, to avoid missing events.
|
||||
*
|
||||
* After the first use, a timeline will continue with the same 'offset'. That is, if you change
|
||||
* a pattern without changing its timeline number, it will stay on that timeline without resetting.
|
||||
*
|
||||
* Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2`
|
||||
* and `2`. This is because when you negate a timeline it will always reset.
|
||||
*
|
||||
* You can also pattern the timeline if you want, to create strange resetting patterns.
|
||||
* @param {number | Pattern} timeline The timeline that the pattern should play on.
|
||||
* @example
|
||||
* n("<0 1 2 3>(3,8)")
|
||||
* .sound("num")
|
||||
* // resets the timeline every two cycles, by negating the timeline.
|
||||
* // in a lot of cases this will be edited by a human live coder
|
||||
* // rather than patterned!
|
||||
* .timeline("<2 -2>".slow(2))
|
||||
*/
|
||||
|
||||
export const timeline = register(
|
||||
'timeline',
|
||||
function (tpat, pat) {
|
||||
tpat = reify(tpat);
|
||||
const f = function (state) {
|
||||
// Is this called from the scheduler? (rather than from e.g. the visualiser)
|
||||
const scheduler = !!state.controls.cyclist;
|
||||
const timehaps = tpat.query(state);
|
||||
const result = [];
|
||||
for (const timehap of timehaps) {
|
||||
const tlid = timehap.value;
|
||||
let offset;
|
||||
if (tlid === 0) {
|
||||
offset = 0;
|
||||
} else if (tlid in timelines) {
|
||||
offset = timelines[tlid];
|
||||
} else {
|
||||
const timearc = timehap.wholeOrPart();
|
||||
if (!scheduler || state.span.begin.lt(timearc.midpoint())) {
|
||||
offset = timearc.begin;
|
||||
} else {
|
||||
// Sync to end of timearc if we first see it over halfway into its
|
||||
// timespan. Allows 'cuing up' next timeline when live coding.
|
||||
offset = timearc.end;
|
||||
}
|
||||
}
|
||||
if (scheduler) {
|
||||
// update state
|
||||
timelines[tlid] = offset;
|
||||
if (tlid !== 0) {
|
||||
delete timelines[-tlid];
|
||||
}
|
||||
}
|
||||
|
||||
const pathaps = pat
|
||||
.late(offset)
|
||||
.query(state.setSpan(timehap.part))
|
||||
.map((h) => h.setContext(h.combineContext(timehap)));
|
||||
result.push(...pathaps);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return new Pattern(f, pat._steps);
|
||||
},
|
||||
false,
|
||||
);
|
||||
+12
-11
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
index.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -11,20 +11,21 @@ import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls, createClock };
|
||||
export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './impure.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './ui.mjs';
|
||||
export * from './util.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
import { reset_state } from './impure.mjs';
|
||||
|
||||
export function repl({
|
||||
defaultOutput,
|
||||
@@ -52,6 +53,9 @@ export function repl({
|
||||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
onToggle?.(started);
|
||||
if (!started) {
|
||||
reset_state();
|
||||
}
|
||||
},
|
||||
setInterval,
|
||||
clearInterval,
|
||||
|
||||
+26
-9
@@ -479,14 +479,24 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
const refsByChan = {};
|
||||
|
||||
/**
|
||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||
*
|
||||
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {Function}
|
||||
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
|
||||
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
|
||||
* that came through that cc number (and channel, if provided)
|
||||
* @example
|
||||
* let cc = await midin('IAC Driver Bus 1')
|
||||
* const cc = await midin('IAC Driver Bus 1')
|
||||
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
|
||||
* @example
|
||||
* const allCC = await midin('IAC Driver Bus 1')
|
||||
* const cc = (ccNum) => allCC(ccNum, 2) // just channel 2
|
||||
* note("c a f e").s("saw")
|
||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||
*/
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
@@ -511,17 +521,24 @@ export async function midin(input) {
|
||||
}`,
|
||||
);
|
||||
}
|
||||
// ensure refs for this input are initialized
|
||||
if (!refs[input]) {
|
||||
refs[input] = {};
|
||||
}
|
||||
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
||||
refs[input] ??= {};
|
||||
refsByChan[input] ??= {};
|
||||
const cc = (cc, chan) => {
|
||||
if (chan !== undefined) {
|
||||
return ref(() => refsByChan[input][cc]?.[chan] || 0);
|
||||
}
|
||||
return ref(() => refs[input][cc] || 0);
|
||||
};
|
||||
|
||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
||||
listeners[input] = (e) => {
|
||||
const cc = e.dataBytes[0];
|
||||
const ccNum = e.dataBytes[0];
|
||||
const v = e.dataBytes[1];
|
||||
refs[input] && (refs[input][cc] = v / 127);
|
||||
const chan = e.message.channel;
|
||||
const scaled = v / 127;
|
||||
refsByChan[input][ccNum] ??= {};
|
||||
refsByChan[input][ccNum][chan] = scaled;
|
||||
refs[input][ccNum] = scaled;
|
||||
};
|
||||
device.addListener('midimessage', listeners[input]);
|
||||
return cc;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
onceEnded,
|
||||
releaseAudioNode,
|
||||
} from '@strudel/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
@@ -172,9 +173,8 @@ export function registerSoundfonts() {
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (releaseTime) => {};
|
||||
onceEnded(bufferSource, () => {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
|
||||
@@ -189,7 +189,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
|
||||
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
|
||||
}
|
||||
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
|
||||
return { lfo, disconnect: () => lfo?.disconnect() };
|
||||
return lfo;
|
||||
}
|
||||
export function createFilter(context, start, end, params, cps, cycle) {
|
||||
let {
|
||||
@@ -283,9 +283,12 @@ export function drywet(dry, wet, wetAmount = 0) {
|
||||
wet_gain.connect(mix);
|
||||
return {
|
||||
node: mix,
|
||||
onended: () => {
|
||||
dry_gain.disconnect(mix);
|
||||
wet_gain.disconnect(mix);
|
||||
teardown: () => {
|
||||
releaseAudioNode(dry_gain);
|
||||
releaseAudioNode(wet_gain);
|
||||
// it is not the responsability of drywet
|
||||
// to call `releaseAudioNode` on
|
||||
// the 2 external args dry and wet
|
||||
dry.disconnect(dry_gain);
|
||||
wet.disconnect(wet_gain);
|
||||
},
|
||||
@@ -324,10 +327,10 @@ export function getVibratoOscillator(param, value, t) {
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(param);
|
||||
vibratoOscillator.onended = () => {
|
||||
gain.disconnect(param);
|
||||
vibratoOscillator.disconnect(gain);
|
||||
};
|
||||
onceEnded(vibratoOscillator, () => {
|
||||
releaseAudioNode(gain);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
});
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
}
|
||||
@@ -613,6 +616,8 @@ export const releaseAudioNode = (node) => {
|
||||
// returns true and either its active source flag is true or
|
||||
// any AudioNode connected to one of its inputs is actively processing.
|
||||
if (node instanceof AudioWorkletNode) {
|
||||
// while `end` is not native to the web audio API, it is common practice in superdough
|
||||
// to use that param in the worklets to trigger returning false from the processor
|
||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { drywet } from './helpers.mjs';
|
||||
import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let noiseCache = {};
|
||||
@@ -65,9 +65,12 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
||||
export function getNoiseMix(inputNode, wet, t) {
|
||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||
noiseOscillator.node.onended = () => noiseMix.onended();
|
||||
onceEnded(noiseOscillator.node, () => {
|
||||
releaseAudioNode(noiseOscillator.node);
|
||||
});
|
||||
return {
|
||||
node: noiseMix.node,
|
||||
stop: (time) => noiseOscillator?.stop(time),
|
||||
teardown: noiseMix.teardown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -323,10 +323,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||
node.connect(out);
|
||||
onceEnded(bufferSource, function () {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
out.disconnect();
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
releaseAudioNode(node);
|
||||
releaseAudioNode(out);
|
||||
onended();
|
||||
});
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
|
||||
@@ -9,7 +9,16 @@ import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import {
|
||||
createFilter,
|
||||
gainNode,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getWorklet,
|
||||
effectSend,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
@@ -448,6 +457,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
compressorKnee,
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
transient,
|
||||
transsustain,
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
@@ -503,7 +514,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const onEnded = () => {
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
audioNodes.forEach((n) => releaseAudioNode(n));
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
@@ -529,6 +540,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
chain.push(sourceNode);
|
||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
|
||||
transient !== undefined &&
|
||||
chain.push(
|
||||
getWorklet(
|
||||
ac,
|
||||
'transient-processor',
|
||||
{},
|
||||
{
|
||||
processorOptions: {
|
||||
attack: transient,
|
||||
sustain: transsustain,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
noises,
|
||||
onceEnded,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
@@ -52,7 +53,7 @@ export function registerSynthSounds() {
|
||||
const g = gainNode(0.3);
|
||||
|
||||
let sound = getOscillator(s, t, value, () => {
|
||||
g.disconnect();
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
});
|
||||
|
||||
@@ -110,15 +111,15 @@ export function registerSynthSounds() {
|
||||
|
||||
const mix = gainNode(mixGain);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
sat.disconnect();
|
||||
noise.node.disconnect();
|
||||
noiseGain.disconnect();
|
||||
mix.disconnect();
|
||||
onceEnded(o, () => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(g);
|
||||
releaseAudioNode(sat);
|
||||
releaseAudioNode(noise.node);
|
||||
releaseAudioNode(noiseGain);
|
||||
releaseAudioNode(mix);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
const node = o.connect(sat).connect(g).connect(mix);
|
||||
noise.node.connect(noiseGain).connect(mix);
|
||||
@@ -384,11 +385,11 @@ export function registerSynthSounds() {
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onceEnded(o, () => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
@@ -489,11 +490,12 @@ export function getOscillator(s, t, value, onended) {
|
||||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
o.onended = () => {
|
||||
noiseMix || o.disconnect();
|
||||
noiseMix?.node.disconnect();
|
||||
onceEnded(o, () => {
|
||||
noiseMix?.teardown();
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(noiseMix?.node);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
o.start(t);
|
||||
|
||||
return {
|
||||
|
||||
@@ -320,10 +320,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
fm?.stop();
|
||||
wtPosModulators?.disconnect();
|
||||
wtWarpModulators?.disconnect();
|
||||
releaseAudioNode(wtPosModulators);
|
||||
releaseAudioNode(wtWarpModulators);
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
|
||||
@@ -11,6 +11,9 @@ const PI = Math.PI;
|
||||
const TWO_PI = 2 * PI;
|
||||
const INVSR = 1 / sampleRate;
|
||||
|
||||
const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
|
||||
const dbToLin = (db) => Math.pow(10, db / 20);
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
const lerp = (a, b, n) => n * (b - a) + a;
|
||||
@@ -1384,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
|
||||
|
||||
class TransientProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
this.gainCoeff = timeToCoeff(0.2);
|
||||
this.avgGain = 1;
|
||||
let {
|
||||
attackTime = 0.003,
|
||||
sustainTime = 0.08,
|
||||
attack = 0,
|
||||
sustain = 0,
|
||||
sensitivity = 0.1,
|
||||
mix = 1,
|
||||
begin = 0,
|
||||
end = 0,
|
||||
} = options.processorOptions;
|
||||
attackTime = clamp(attackTime, 0.0005, 0.05);
|
||||
sustainTime = clamp(sustainTime, 0.01, 0.5);
|
||||
this.attackCoeff = timeToCoeff(attackTime);
|
||||
this.sustainCoeff = timeToCoeff(sustainTime);
|
||||
this.attackAmt = clamp(attack, -1, 1);
|
||||
this.sustainAmt = clamp(sustain, -1, 1);
|
||||
this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
|
||||
this.mix = clamp(mix, 0, 1);
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.attackEnv = new Float32Array(2); // assume stereo
|
||||
this.sustainEnv = new Float32Array(2);
|
||||
}
|
||||
|
||||
process(inputs, outputs, _params) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
if (currentTime >= this.end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= this.begin) {
|
||||
return true;
|
||||
}
|
||||
const channels = input.length;
|
||||
if (channels > this.attackEnv.length) {
|
||||
this.attackEnv = new Float32Array(channels);
|
||||
this.sustainEnv = new Float32Array(channels);
|
||||
}
|
||||
let avgGain = this.avgGain;
|
||||
for (let ch = 0; ch < channels; ch++) {
|
||||
let attEnv = this.attackEnv[ch];
|
||||
let susEnv = this.sustainEnv[ch];
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
const sample = input[ch][n];
|
||||
const x = Math.abs(sample);
|
||||
attEnv = lerp(attEnv, x, this.attackCoeff);
|
||||
susEnv = lerp(susEnv, x, this.sustainCoeff);
|
||||
const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
|
||||
const attScale = peakiness > 0 ? peakiness : 0;
|
||||
const susScale = peakiness < 0 ? -peakiness : 0;
|
||||
const attackGain = dbToLin(this.attackAmt * attScale * 18);
|
||||
const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
|
||||
const gain = clamp(attackGain * sustainGain, 0, 8);
|
||||
avgGain = lerp(avgGain, gain, this.gainCoeff);
|
||||
const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
|
||||
const wet = sample * gain * makeup;
|
||||
let y = lerp(sample, wet, this.mix);
|
||||
y /= 1 + Math.abs(y); // soft clip
|
||||
output[ch][n] = y;
|
||||
}
|
||||
this.attackEnv[ch] = attEnv;
|
||||
this.sustainEnv[ch] = susEnv;
|
||||
}
|
||||
this.avgGain = avgGain;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('transient-processor', TransientProcessor);
|
||||
|
||||
@@ -7048,6 +7048,27 @@ exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c s:saw ]",
|
||||
"[ 1/4 → 1/2 | note:a s:saw ]",
|
||||
"[ 1/2 → 3/4 | note:f s:saw ]",
|
||||
"[ 3/4 → 1/1 | note:e s:saw ]",
|
||||
"[ 1/1 → 5/4 | note:c s:saw ]",
|
||||
"[ 5/4 → 3/2 | note:a s:saw ]",
|
||||
"[ 3/2 → 7/4 | note:f s:saw ]",
|
||||
"[ 7/4 → 2/1 | note:e s:saw ]",
|
||||
"[ 2/1 → 9/4 | note:c s:saw ]",
|
||||
"[ 9/4 → 5/2 | note:a s:saw ]",
|
||||
"[ 5/2 → 11/4 | note:f s:saw ]",
|
||||
"[ 11/4 → 3/1 | note:e s:saw ]",
|
||||
"[ 3/1 → 13/4 | note:c s:saw ]",
|
||||
"[ 13/4 → 7/2 | note:a s:saw ]",
|
||||
"[ 7/2 → 15/4 | note:f s:saw ]",
|
||||
"[ 15/4 → 4/1 | note:e s:saw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midiport" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c midiport:0 ]",
|
||||
@@ -12008,6 +12029,84 @@ exports[`runs examples > example "tour" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "transient" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:bd transient:-1 ]",
|
||||
"[ 1/1 → 2/1 | s:bd transient:-0.5 ]",
|
||||
"[ 2/1 → 3/1 | s:bd transient:0 ]",
|
||||
"[ 3/1 → 4/1 | s:bd transient:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "transient" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 1/16 → 1/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 1/8 → 3/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 3/16 → 1/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 1/4 → 5/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 5/16 → 3/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 3/8 → 7/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 7/16 → 1/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 1/2 → 9/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 9/16 → 5/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 5/8 → 11/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 11/16 → 3/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 3/4 → 13/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 13/16 → 7/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 7/8 → 15/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 15/16 → 1/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 1/1 → 17/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 17/16 → 9/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 9/8 → 19/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 19/16 → 5/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 5/4 → 21/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 21/16 → 11/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 11/8 → 23/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 23/16 → 3/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 3/2 → 25/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 25/16 → 13/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 13/8 → 27/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 27/16 → 7/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 7/4 → 29/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 29/16 → 15/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 15/8 → 31/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 31/16 → 2/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 2/1 → 33/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 33/16 → 17/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 17/8 → 35/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 35/16 → 9/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 9/4 → 37/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 37/16 → 19/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 19/8 → 39/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 39/16 → 5/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 5/2 → 41/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 41/16 → 21/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 21/8 → 43/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 43/16 → 11/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 11/4 → 45/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 45/16 → 23/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 23/8 → 47/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 47/16 → 3/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
|
||||
"[ 3/1 → 49/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 49/16 → 25/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 25/8 → 51/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 51/16 → 13/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 13/4 → 53/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 53/16 → 27/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 27/8 → 55/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 55/16 → 7/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 7/2 → 57/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 57/16 → 29/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 29/8 → 59/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 59/16 → 15/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 15/4 → 61/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 61/16 → 31/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 31/8 → 63/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
"[ 63/16 → 4/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "transpose" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C2 ]",
|
||||
|
||||
Reference in New Issue
Block a user