mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-05 00:48:04 -04:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98e99e8166 | |||
| 8f81463b9c | |||
| 8120fc72ab | |||
| d7fb9fe992 | |||
| 37d524e639 | |||
| d65b24db8f | |||
| f1dd209749 | |||
| d6f0b863f0 | |||
| 5dbbc7b9e1 | |||
| beb16f4040 | |||
| f2be5e4f5e | |||
| c7db8eaad6 | |||
| d5d6bc6f34 | |||
| d9336aba3a | |||
| 263c9c0253 | |||
| 6ecdc739b4 | |||
| f67e81a03f | |||
| 5cf7571b77 | |||
| b2aeaa56c1 | |||
| 9e3792f6a6 | |||
| f8ed9c3a7c | |||
| c5b42b5555 | |||
| 75f58bc875 | |||
| 3b037deb10 | |||
| 362b2df390 | |||
| 20d925c10c | |||
| 91f37accdd | |||
| 07f01c237f | |||
| 76fa8264cd | |||
| 08dc5e2c5c | |||
| bd54c8003a | |||
| 9e946f1d14 | |||
| 47cdf4561d | |||
| 6c8b707d2e | |||
| adcbf9240e | |||
| f4b3fdaacd | |||
| 9bf3249e82 | |||
| 0a0afcf73f | |||
| 6ebf506070 | |||
| eafa2f17ec | |||
| 429682735d | |||
| 04232370c2 | |||
| 4484ee4e80 | |||
| 57598f2aa1 | |||
| c9522a4e49 | |||
| 25d1179985 | |||
| e38811d325 | |||
| 422b03699a | |||
| cb046a3548 | |||
| fe0be6bc53 | |||
| 4747fab4ed | |||
| 896ec4c643 | |||
| 1723125099 |
@@ -2352,11 +2352,18 @@ export const { offset } = registerControl('offset');
|
|||||||
**/
|
**/
|
||||||
export const { octaves } = registerControl('octaves');
|
export const { octaves } = registerControl('octaves');
|
||||||
/**
|
/**
|
||||||
* Remove anchor note from the voicing. Useful for melody harmonization
|
* How the voicing is aligned to the anchor
|
||||||
|
* - `below`: top note <= anchor
|
||||||
|
* - `duck`: top note <= anchor, anchor excluded
|
||||||
|
* - `above`: bottom note >= anchor
|
||||||
|
* - `root`: bottom note is the lowest root of the chord >= anchor
|
||||||
|
*
|
||||||
|
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
|
||||||
|
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
|
||||||
*
|
*
|
||||||
* @name mode
|
* @name mode
|
||||||
* @tags tonal
|
* @tags tonal
|
||||||
* @param {string | Pattern} modeName one of {below | above | duck | root}
|
* @param {string | Pattern} modeName one of {below | above | duck | root | oldabove | oldroot}
|
||||||
* @example
|
* @example
|
||||||
* mode("<below above duck root>").chord("C").voicing()
|
* mode("<below above duck root>").chord("C").voicing()
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({
|
constructor({
|
||||||
interval,
|
interval,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
getTime,
|
getTime,
|
||||||
@@ -63,12 +63,7 @@ export class Cyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const targetTime =
|
const targetTime =
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||||
const duration = hap.duration / this.cps;
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
// the following line is dumb and only here for backwards compatibility
|
|
||||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
const deadline = targetTime - phase;
|
|
||||||
// this onTrigger has another signature
|
|
||||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
|
||||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||||
this.cps = hap.value.cps;
|
this.cps = hap.value.cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logger } from './logger.mjs';
|
|||||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onSchedule, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
@@ -43,8 +43,7 @@ export class NeoCyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||||
const duration = cycleToSeconds(hap.duration, this.cps);
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-36
@@ -27,6 +27,7 @@ import {
|
|||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
import { strudelScope } from './evaluate.mjs';
|
import { strudelScope } from './evaluate.mjs';
|
||||||
|
import { getTime } from './schedulerState.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -872,23 +873,41 @@ export class Pattern {
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Context methods - ones that deal with metadata
|
// Context methods - ones that deal with metadata
|
||||||
|
|
||||||
onTrigger(onTrigger, dominant = true) {
|
/**
|
||||||
|
* Attaches a function that is called for each hap onset.
|
||||||
|
* @name onSchedule
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd sd").onSchedule((hap, time, cps) => {
|
||||||
|
* console.log('hap scheduled', time, cps);
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
onSchedule(onSchedule, dominant = true) {
|
||||||
return this.withHap((hap) =>
|
return this.withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onSchedule: (scheduleHap, t, cps) => {
|
||||||
// run previously set trigger, if it exists
|
// run previously set trigger, if it exists
|
||||||
hap.context.onTrigger?.(...args);
|
hap.context.onSchedule?.(scheduleHap, t, cps);
|
||||||
onTrigger(...args);
|
onSchedule(scheduleHap, t, cps);
|
||||||
},
|
},
|
||||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
// if dominantScheduler is set to true, the default output (webaudio) will be disabled
|
||||||
// when using multiple triggers, you cannot flip this flag to false again!
|
// when using multiple triggers, you cannot flip this flag to false again!
|
||||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
dominantScheduler: hap.context.dominantScheduler || dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy:
|
||||||
|
onTrigger(onTrigger, dominant = true) {
|
||||||
|
console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead');
|
||||||
|
return this.onSchedule((hap, t, cps) => {
|
||||||
|
const currentTime = getTime();
|
||||||
|
return onTrigger(hap, currentTime, cps, t);
|
||||||
|
}, dominant);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the content of the current event to the console (visible in the side menu).
|
* Writes the content of the current event to the console (visible in the side menu).
|
||||||
* @tags visualization
|
* @tags visualization
|
||||||
@@ -898,7 +917,7 @@ export class Pattern {
|
|||||||
* s("bd sd").log()
|
* s("bd sd").log()
|
||||||
*/
|
*/
|
||||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||||
return this.onTrigger((...args) => {
|
return this.onSchedule((...args) => {
|
||||||
logger(func(...args), undefined, getData(...args));
|
logger(func(...args), undefined, getData(...args));
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
@@ -1002,7 +1021,7 @@ export const arpWith = register('arpWith', (func, pat) => {
|
|||||||
* */
|
* */
|
||||||
export const arp = register(
|
export const arp = register(
|
||||||
'arp',
|
'arp',
|
||||||
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
|
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[_mod(i, haps.length)])),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1529,7 +1548,9 @@ export function slowcat(...pats) {
|
|||||||
// Array test here is to avoid infinite recursions..
|
// Array test here is to avoid infinite recursions..
|
||||||
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
|
||||||
|
|
||||||
if (pats.length == 1) {
|
if (!pats.length) {
|
||||||
|
return silence;
|
||||||
|
} else if (pats.length == 1) {
|
||||||
return pats[0];
|
return pats[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1537,10 +1558,6 @@ export function slowcat(...pats) {
|
|||||||
const span = state.span;
|
const span = state.span;
|
||||||
const pat_n = _mod(span.begin.sam(), pats.length);
|
const pat_n = _mod(span.begin.sam(), pats.length);
|
||||||
const pat = pats[pat_n];
|
const pat = pats[pat_n];
|
||||||
if (!pat) {
|
|
||||||
// pat_n can be negative, if the span is in the past..
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// A bit of maths to make sure that cycles from constituent patterns aren't skipped.
|
// A bit of maths to make sure that cycles from constituent patterns aren't skipped.
|
||||||
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
|
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
|
||||||
// be the second (rather than fourth) cycle from the first pattern.
|
// be the second (rather than fourth) cycle from the first pattern.
|
||||||
@@ -1557,11 +1574,14 @@ export function slowcat(...pats) {
|
|||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
*/
|
*/
|
||||||
export function slowcatPrime(...pats) {
|
export function slowcatPrime(...pats) {
|
||||||
|
if (!pats.length) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
pats = pats.map(reify);
|
pats = pats.map(reify);
|
||||||
const query = function (state) {
|
const query = function (state) {
|
||||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
const pat_n = _mod(Math.floor(state.span.begin), pats.length);
|
||||||
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
|
const pat = pats[pat_n];
|
||||||
return pat?.query(state) || [];
|
return pat.query(state);
|
||||||
};
|
};
|
||||||
return new Pattern(query).splitQueries();
|
return new Pattern(query).splitQueries();
|
||||||
}
|
}
|
||||||
@@ -3634,26 +3654,6 @@ export const slice = register(
|
|||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* make something happen on event time
|
|
||||||
* uses browser timeout which is innacurate for audio tasks
|
|
||||||
* @name onTriggerTime
|
|
||||||
* @tags external_io
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
|
||||||
*/
|
|
||||||
Pattern.prototype.onTriggerTime = function (func) {
|
|
||||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
|
||||||
const diff = targetTime - currentTime;
|
|
||||||
window.setTimeout(() => {
|
|
||||||
func(hap);
|
|
||||||
}, diff * 1000);
|
|
||||||
}, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
* @name splice
|
* @name splice
|
||||||
|
|||||||
+11
-14
@@ -7,7 +7,7 @@ import {
|
|||||||
setIsStarted,
|
setIsStarted,
|
||||||
setPattern as exposeSchedulerPattern,
|
setPattern as exposeSchedulerPattern,
|
||||||
setTime,
|
setTime,
|
||||||
setTriggerFunc,
|
setSchedulerFunc,
|
||||||
} from './schedulerState.mjs';
|
} from './schedulerState.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
@@ -55,7 +55,7 @@ export function repl({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const schedulerOptions = {
|
const schedulerOptions = {
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onSchedule: getScheduler({ defaultOutput }),
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
@@ -73,7 +73,7 @@ export function repl({
|
|||||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
const scheduler =
|
const scheduler =
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
setTriggerFunc(schedulerOptions.onTrigger);
|
setSchedulerFunc(schedulerOptions.onSchedule);
|
||||||
setCpsFunc(() => scheduler.cps);
|
setCpsFunc(() => scheduler.cps);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let anonymousIndex = 0;
|
let anonymousIndex = 0;
|
||||||
@@ -560,20 +560,17 @@ export function repl({
|
|||||||
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
export const getScheduler =
|
||||||
({ getTime, defaultOutput }) =>
|
({ defaultOutput }) =>
|
||||||
async (hap, deadline, duration, cps, t) => {
|
async (hap, t, cps) => {
|
||||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
|
||||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onSchedule || !hap.context.dominantScheduler) {
|
||||||
await defaultOutput(hap, deadline, duration, cps, t);
|
await defaultOutput(hap, t, cps);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onSchedule) {
|
||||||
// call signature of output / onTrigger is different...
|
await hap.context.onSchedule(hap, t, cps);
|
||||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorLogger(err, 'getTrigger');
|
errorLogger(err, 'getScheduler');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
let time;
|
let time;
|
||||||
let cpsFunc;
|
let cpsFunc;
|
||||||
let pattern;
|
let pattern;
|
||||||
let triggerFunc;
|
let schedulerFunc;
|
||||||
let isStarted;
|
let isStarted;
|
||||||
export function getTime() {
|
export function getTime() {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
@@ -36,12 +36,12 @@ export function getPattern() {
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTriggerFunc(func) {
|
export function setSchedulerFunc(func) {
|
||||||
triggerFunc = func;
|
schedulerFunc = func;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTriggerFunc() {
|
export function getSchedulerFunc() {
|
||||||
return triggerFunc;
|
return schedulerFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setIsStarted(val) {
|
export function setIsStarted(val) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
||||||
|
import { _mod } from './util.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||||
@@ -33,7 +34,7 @@ export const signal = (func) => {
|
|||||||
* .scale('C major')
|
* .scale('C major')
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => _mod(t, 1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
||||||
@@ -56,7 +57,7 @@ export const saw2 = saw.toBipolar();
|
|||||||
* .scale('C major')
|
* .scale('C major')
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
export const isaw = signal((t) => 1 - _mod(t, 1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
||||||
@@ -113,7 +114,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor(_mod(t * 2, 2)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A square signal between -1 and 1 (like `square`, but bipolar).
|
* A square signal between -1 and 1 (like `square`, but bipolar).
|
||||||
@@ -123,6 +124,22 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
|
|||||||
*/
|
*/
|
||||||
export const square2 = square.toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between 1 and 0 (like `square` but flipped).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
|
*/
|
||||||
|
export const isquare = signal((t) => 1 - Math.floor(_mod(t * 2, 2)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between 1 and -1 (like `isquare`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
|
*/
|
||||||
|
export const isquare2 = isquare.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A triangle signal between 0 and 1.
|
* A triangle signal between 0 and 1.
|
||||||
*
|
*
|
||||||
@@ -396,7 +413,7 @@ export const randrun = (n) => {
|
|||||||
.map((n, i) => [n, i])
|
.map((n, i) => [n, i])
|
||||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||||
.map((x) => x[1]);
|
.map((x) => x[1]);
|
||||||
const i = t.cyclePos().mul(n).floor() % n;
|
const i = _mod(t.cyclePos().mul(n).floor(), n);
|
||||||
return nums[i];
|
return nums[i];
|
||||||
})._segment(n);
|
})._segment(n);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return pat.onTrigger((hap) => {
|
return pat.onSchedule((hap) => {
|
||||||
triggerSpeech(hap.value, lang, voice);
|
triggerSpeech(hap.value, lang, voice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
fastcat,
|
fastcat,
|
||||||
firstOf,
|
firstOf,
|
||||||
slowcat,
|
slowcat,
|
||||||
|
slowcatPrime,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
palindrome,
|
palindrome,
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
rev,
|
rev,
|
||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
|
randrun,
|
||||||
binaryN,
|
binaryN,
|
||||||
pick,
|
pick,
|
||||||
stackLeft,
|
stackLeft,
|
||||||
@@ -53,6 +55,7 @@ import {
|
|||||||
stepcat,
|
stepcat,
|
||||||
sometimes,
|
sometimes,
|
||||||
expand,
|
expand,
|
||||||
|
arp,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { log, logValues } from '../pattern.mjs';
|
import { log, logValues } from '../pattern.mjs';
|
||||||
@@ -544,6 +547,9 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('slowcat()', () => {
|
describe('slowcat()', () => {
|
||||||
|
it('Can be empty', () => {
|
||||||
|
expect(slowcat().firstCycle()).toStrictEqual([]);
|
||||||
|
});
|
||||||
it('Can concatenate things slowly', () => {
|
it('Can concatenate things slowly', () => {
|
||||||
expect(
|
expect(
|
||||||
slowcat('a', 'b')
|
slowcat('a', 'b')
|
||||||
@@ -576,6 +582,38 @@ describe('Pattern', () => {
|
|||||||
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('slowcatPrime()', () => {
|
||||||
|
it('Can be empty', () => {
|
||||||
|
expect(slowcatPrime().firstCycle()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
it('Can slowcat patterns swapping back and forth skipping the expected notes', () => {
|
||||||
|
expect(
|
||||||
|
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
|
||||||
|
.fast(4)
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(
|
||||||
|
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
|
||||||
|
.fast(4)
|
||||||
|
.late(8)
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('arp()', () => {
|
||||||
|
it('It wraps around with both positive and negative indices', () => {
|
||||||
|
expect(
|
||||||
|
stack('a', 'b', 'c')
|
||||||
|
.arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4))
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('rev()', () => {
|
describe('rev()', () => {
|
||||||
it('Can reverse things', () => {
|
it('Can reverse things', () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -945,6 +983,17 @@ describe('Pattern', () => {
|
|||||||
it('Can run', () => {
|
it('Can run', () => {
|
||||||
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||||
});
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(run(4).late(1).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('randrun', () => {
|
||||||
|
it('Can randrun', () => {
|
||||||
|
expect(randrun(4).firstCycle()).toStrictEqual(sequence(2, 1, 3, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(randrun(4).late(1).firstCycle()).toStrictEqual(sequence(1, 2, 0, 3).firstCycle());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('binaryN', () => {
|
describe('binaryN', () => {
|
||||||
it('Can make a binary pattern from a decimal', () => {
|
it('Can make a binary pattern from a decimal', () => {
|
||||||
@@ -1314,7 +1363,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
@@ -1332,7 +1381,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -8,7 +8,23 @@ import Fraction from 'fraction.js';
|
|||||||
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
import {
|
||||||
|
saw,
|
||||||
|
saw2,
|
||||||
|
isaw,
|
||||||
|
isaw2,
|
||||||
|
tri,
|
||||||
|
tri2,
|
||||||
|
itri,
|
||||||
|
itri2,
|
||||||
|
square,
|
||||||
|
square2,
|
||||||
|
isquare,
|
||||||
|
isquare2,
|
||||||
|
per,
|
||||||
|
perx,
|
||||||
|
cyclesPer,
|
||||||
|
} from '../signal.mjs';
|
||||||
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
|
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
|
||||||
|
|
||||||
const st = (begin, end) => new State(ts(begin, end));
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
@@ -24,17 +40,61 @@ const sameFirst = (a, b) => {
|
|||||||
|
|
||||||
describe('signal()', () => {
|
describe('signal()', () => {
|
||||||
it('Can make saw/saw2', () => {
|
it('Can make saw/saw2', () => {
|
||||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.25, 0.5, 0.75).firstCycle());
|
||||||
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
||||||
});
|
});
|
||||||
it('Can make isaw/isaw2', () => {
|
it('Can make isaw/isaw2', () => {
|
||||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
||||||
|
|
||||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
||||||
});
|
});
|
||||||
|
it('Can make tri/tri2', () => {
|
||||||
|
expect(tri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.5, 1, 0.5).firstCycle());
|
||||||
|
expect(tri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make itri/itri2', () => {
|
||||||
|
expect(itri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, 0.5).firstCycle());
|
||||||
|
expect(itri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make square/square2', () => {
|
||||||
|
expect(square.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
|
||||||
|
expect(square2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -1, 1, 1).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make isquare/isquare2', () => {
|
||||||
|
expect(isquare.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, 0, 0).firstCycle());
|
||||||
|
expect(isquare2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, -1, -1).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(saw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(0, 0.25, 0.5, 0.75).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(saw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(-1, -0.5, 0, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isaw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.75, 0.5, 0.25).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isaw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.5, 0, -0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(tri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(0, 0.5, 1, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(tri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
|
||||||
|
expect(itri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.5, 0, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(itri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
|
||||||
|
expect(square.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
|
||||||
|
expect(square2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(-1, -1, 1, 1).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isquare.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 1, 0, 0).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isquare2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 1, -1, -1).firstCycle(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cyclesPer', () => {
|
describe('cyclesPer', () => {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const getFrequency = (hap) => {
|
|||||||
if (value.freq) {
|
if (value.freq) {
|
||||||
return value.freq;
|
return value.freq;
|
||||||
}
|
}
|
||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note ?? value.n ?? value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = midiToFreq(hap.value);
|
value = midiToFreq(hap.value);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
const controls = Object.entries({ ...hap.value, freq })
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
.flat()
|
.flat()
|
||||||
.join('/');
|
.join('/');
|
||||||
|
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
const timeOffset = targetTime - currentTime; // latency ?
|
const timeOffset = targetTime - getAudioContext().currentTime; // latency ?
|
||||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${instrument}"`, // p1: instrument name
|
||||||
@@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
p1 = `"${instrument}"`;
|
p1 = `"${instrument}"`;
|
||||||
}
|
}
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error('csound only support objects as hap values');
|
throw new Error('csound only support objects as hap values');
|
||||||
}
|
}
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
// Time in seconds counting from now.
|
// Time in seconds counting from now.
|
||||||
const p2 = targetTime - currentTime;
|
const p2 = targetTime - currentTime;
|
||||||
const p3 = hap.duration.valueOf() + 0;
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
@@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80;
|
|||||||
const CC_MESSAGE = 0xb0;
|
const CC_MESSAGE = 0xb0;
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = 34;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Pattern, ClockCollator } from '@strudel/core';
|
import { Pattern, ClockCollator } from '@strudel/core';
|
||||||
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
import { parseControlsFromHap } from '@strudel/osc/osc.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
export async function oscSchedulerTauri(hap, targetTime, cps = 1) {
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const params = [];
|
const params = [];
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
@@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(oscTriggerTauri);
|
return this.onSchedule(oscSchedulerTauri);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@tauri-apps/api": "^2.2.0"
|
"@tauri-apps/api": "^2.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
dough.mjs
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { register, noteToMidi } from '@strudel/core';
|
||||||
|
import { Dough, doughsamples } from 'dough-synth';
|
||||||
|
import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio';
|
||||||
|
import wasm from 'dough-synth/dough.wasm?url';
|
||||||
|
import workletCode from 'dough-synth/dough.js?raw';
|
||||||
|
|
||||||
|
Object.assign(globalThis, { doughsamples });
|
||||||
|
|
||||||
|
let D;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* initializes dough ahead of time. you don't need to use this, but if you do, you can await it, so dough is ready before the pattern starts.
|
||||||
|
*
|
||||||
|
* @name initDough
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* await initDough()
|
||||||
|
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||||
|
* .phaser(3).fm(1.4).fmh(1.01)
|
||||||
|
* .dough()
|
||||||
|
*/
|
||||||
|
export function initDough() {
|
||||||
|
if (!D || D.audioContext !== getAudioContext()) {
|
||||||
|
D = new Dough({
|
||||||
|
wasm,
|
||||||
|
workletCode,
|
||||||
|
audioContext: getAudioContext(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return D.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doughScheduler(hap, targetTime, cps = 1) {
|
||||||
|
const offset = D.context_offset?.[0];
|
||||||
|
if (!offset) {
|
||||||
|
return; // not ready
|
||||||
|
}
|
||||||
|
hap.ensureObjectValue();
|
||||||
|
const event = {
|
||||||
|
dough: 'play',
|
||||||
|
...hap.value,
|
||||||
|
time: targetTime - offset,
|
||||||
|
duration: hap.duration / cps,
|
||||||
|
};
|
||||||
|
if (typeof event.note === 'string') {
|
||||||
|
event.note = noteToMidi(event.note);
|
||||||
|
}
|
||||||
|
D.evaluate(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Uses dough as the audio engine. more info at https://dough.strudel.cc
|
||||||
|
*
|
||||||
|
* @name dough
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||||
|
* .phaser(3).fm(1.4).fmh(1.01)
|
||||||
|
* .dough()
|
||||||
|
*/
|
||||||
|
export const dough = register('dough', (pat) => {
|
||||||
|
initDough();
|
||||||
|
ensureMinimalOutput();
|
||||||
|
return pat.onSchedule(doughScheduler);
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/dough",
|
||||||
|
"version": "1.3.2",
|
||||||
|
"description": "dough synth integration for strudel",
|
||||||
|
"main": "dough.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
|
"dough-synth": "0.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-27
@@ -13,7 +13,7 @@ import {
|
|||||||
getIsStarted,
|
getIsStarted,
|
||||||
getPattern,
|
getPattern,
|
||||||
getTime,
|
getTime,
|
||||||
getTriggerFunc,
|
getSchedulerFunc,
|
||||||
isPattern,
|
isPattern,
|
||||||
logger,
|
logger,
|
||||||
ref,
|
ref,
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from '@strudel/core';
|
} from '@strudel/core';
|
||||||
import { noteToMidi, getControlName } from '@strudel/core';
|
import { noteToMidi, getControlName } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
import { getAudioContext } from '@strudel/webaudio';
|
import { getAudioContext, getClockBridge } from '@strudel/webaudio';
|
||||||
import { scheduleAtTime, ensureMinimalOutput } from '../superdough/helpers.mjs';
|
import { scheduleAtTime, ensureMinimalOutput } from '../superdough/helpers.mjs';
|
||||||
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
||||||
import { MidiInput } from './input.mjs';
|
import { MidiInput } from './input.mjs';
|
||||||
@@ -177,7 +177,11 @@ const isFirefox = navigator?.userAgent?.includes('Firefox');
|
|||||||
// firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997
|
// firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997
|
||||||
function timedSend(timeMs, fn) {
|
function timedSend(timeMs, fn) {
|
||||||
if (isFirefox) {
|
if (isFirefox) {
|
||||||
const audioTime = getAudioContext().currentTime + (timeMs - performance.now()) / 1000;
|
const audioTime = getClockBridge().getAudioContextTime(timeMs);
|
||||||
|
if (!audioTime) {
|
||||||
|
logger('[midi]: skip event, not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
scheduleAtTime(() => fn(undefined), audioTime);
|
scheduleAtTime(() => fn(undefined), audioTime);
|
||||||
} else {
|
} else {
|
||||||
fn(timeMs);
|
fn(timeMs);
|
||||||
@@ -281,11 +285,6 @@ function sendNote(note, velocity, duration, device, midichan, timeMs) {
|
|||||||
timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs }));
|
timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// thanks freya https://youtu.be/LSNQuFEDOyQ?si=ukZI2IGgWV_NDZzP&t=2979
|
|
||||||
function expDecay(a, b, decay, dt) {
|
|
||||||
return b + (a - b) * Math.exp(-decay * dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIDI output: Opens a MIDI output port.
|
* MIDI output: Opens a MIDI output port.
|
||||||
* @tags external_io
|
* @tags external_io
|
||||||
@@ -343,26 +342,16 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
|
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
|
|
||||||
let p; // filtered clock offset
|
return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
|
||||||
let lastTime;
|
|
||||||
|
|
||||||
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => {
|
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { contextTime, performanceTime } = getAudioContext().getOutputTimestamp();
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
if (!contextTime || !performanceTime) {
|
if (!timeMs) {
|
||||||
logger('[midi] skip midi event: not ready yet?');
|
logger('[midi] clockbridge not ready');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// time conversion from audio context time (targetTime) to performance time (what midi needs)
|
|
||||||
const offset = performanceTime - contextTime * 1000; // clock offset in ms
|
|
||||||
const dt = performanceTime - (lastTime ?? performanceTime); // delta time since last midi hap
|
|
||||||
const decay = 1 / 10000; // how fast offset changes have an effect
|
|
||||||
p = expDecay(p ?? offset, offset, decay, dt); // smooth clock offset
|
|
||||||
lastTime = performanceTime;
|
|
||||||
const timeMs = targetTime * 1000 + p; // this is now correct in performance time
|
|
||||||
|
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
@@ -581,8 +570,8 @@ const kListeners = {};
|
|||||||
|
|
||||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||||
const pattern = getPattern();
|
const pattern = getPattern();
|
||||||
const trigger = getTriggerFunc();
|
const schedule = getSchedulerFunc();
|
||||||
if (!pattern || !trigger) {
|
if (!pattern || !schedule) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const t = now + latencyCycles;
|
const t = now + latencyCycles;
|
||||||
@@ -599,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||||
const duration = hap.duration / cps;
|
schedule(hap, t, cps);
|
||||||
trigger(hap, t - ctxNow, duration, cps, t);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -635,7 +623,7 @@ export async function midikeys(input) {
|
|||||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||||
const key = `${input}_${note}`;
|
const key = `${input}_${note}`;
|
||||||
const cps = getCps() ?? 0.5;
|
const cps = getCps() ?? 0.5;
|
||||||
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
const triggerAvailable = !!(getPattern() && getSchedulerFunc());
|
||||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||||
const now = getTime();
|
const now = getTime();
|
||||||
const t = now + latencySeconds * cps;
|
const t = now + latencySeconds * cps;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, isPattern, createParams } from '@strudel/core';
|
import { Pattern, getTime, createParams } from '@strudel/core';
|
||||||
import Paho from 'paho-mqtt';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -80,8 +80,7 @@ Pattern.prototype.mqtt = function (
|
|||||||
}
|
}
|
||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.withHap((hap) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
|
||||||
let msg_topic = topic;
|
let msg_topic = topic;
|
||||||
if (!cx || !cx.isConnected()) {
|
if (!cx || !cx.isConnected()) {
|
||||||
return;
|
return;
|
||||||
@@ -109,13 +108,11 @@ Pattern.prototype.mqtt = function (
|
|||||||
message = new Paho.Message(message);
|
message = new Paho.Message(message);
|
||||||
message.destinationName = msg_topic;
|
message.destinationName = msg_topic;
|
||||||
|
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
cx.send(message);
|
cx.send(message);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-7
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
import { logger, parseNumeral, register, isNote, noteToMidi } from '@strudel/core';
|
||||||
|
import { ensureMinimalOutput, getClockBridge } from '@strudel/webaudio';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -45,7 +46,6 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
controls.midinote = parseNumeral(controls.note);
|
controls.midinote = parseNumeral(controls.note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controls.bank && (controls.s = controls.bank + controls.s);
|
|
||||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||||
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
||||||
// so we need to undo the adjustment before sending the message to superdirt.
|
// so we need to undo the adjustment before sending the message to superdirt.
|
||||||
@@ -55,13 +55,18 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
export async function oscScheduler(hap, targetTime, cps = 1) {
|
||||||
|
|
||||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|
||||||
const ws = await connect();
|
const ws = await connect();
|
||||||
|
|
||||||
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
|
if (!timeMs) {
|
||||||
|
logger('[osc] clockbridge not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ts = performance.timeOrigin + timeMs;
|
||||||
|
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
|
||||||
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
||||||
|
|
||||||
if ('oschost' in hap.value) {
|
if ('oschost' in hap.value) {
|
||||||
@@ -83,4 +88,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
export const osc = register('osc', (pat) => {
|
||||||
|
ensureMinimalOutput();
|
||||||
|
return pat.onSchedule(oscScheduler);
|
||||||
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
"osc": "^2.4.5",
|
"osc": "^2.4.5",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||||
import { oscTrigger } from './osc.mjs';
|
import { oscScheduler } from './osc.mjs';
|
||||||
|
|
||||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
const scheduler = /* isTauri() ? oscTriggerTauri : */ oscScheduler;
|
||||||
|
|
||||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps);
|
||||||
const currentTime = performance.now() / 1000;
|
|
||||||
return trigger(hap, currentTime, cps, targetTime);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ if (args.includes('--json')) {
|
|||||||
}
|
}
|
||||||
writeFileSync(outFile, json, 'utf8');
|
writeFileSync(outFile, json, 'utf8');
|
||||||
console.log(`Wrote json to ${outFile}`);
|
console.log(`Wrote json to ${outFile}`);
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, isPattern } from '@strudel/core';
|
import { Pattern, getTime } from '@strudel/core';
|
||||||
|
|
||||||
var writeMessagers = {};
|
var writeMessagers = {};
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
@@ -64,11 +64,10 @@ function crc16(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
||||||
return this.withHap((hap) => {
|
|
||||||
if (!(name in writeMessagers)) {
|
if (!(name in writeMessagers)) {
|
||||||
getWriter(name, br);
|
getWriter(name, br);
|
||||||
}
|
}
|
||||||
const onTrigger = (hap, currentTime, _cps, targetTime) => {
|
return this.onSchedule((hap, targetTime) => {
|
||||||
var message = '';
|
var message = '';
|
||||||
var chk = 0;
|
var chk = 0;
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
@@ -105,12 +104,10 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
|||||||
} else {
|
} else {
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
writeMessagers[name](message, chk);
|
writeMessagers[name](message, chk);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
|||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
return this.onTrigger((h, ct, cps, targetTime) => {
|
return this.onSchedule((h, targetTime) => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const note = getPlayableNoteValue(h);
|
const note = getPlayableNoteValue(h);
|
||||||
const preset = sf.presets[n % sf.presets.length];
|
const preset = sf.presets[n % sf.presets.length];
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let audioContext;
|
import { clearNodePool } from './nodePools.mjs';
|
||||||
|
import { ClockBridge } from './clockbridge.mjs';
|
||||||
|
|
||||||
|
let audioContext, clockBridge;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
audioContext = new AudioContext();
|
return setAudioContext(new AudioContext());
|
||||||
return audioContext;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAudioContext = (context) => {
|
export const setAudioContext = (context) => {
|
||||||
|
// Existing nodes in the node pool contain references to the previous AudioContext,
|
||||||
|
// so all the nodes in the pool must be cleared when we set a new AudioContext.
|
||||||
|
clearNodePool();
|
||||||
|
if (audioContext && audioContext.state !== 'closed') {
|
||||||
|
audioContext.close();
|
||||||
|
}
|
||||||
audioContext = context;
|
audioContext = context;
|
||||||
|
clockBridge = new ClockBridge(audioContext);
|
||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +36,13 @@ export const getAudioContext = () => {
|
|||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getClockBridge() {
|
||||||
|
if (!clockBridge) {
|
||||||
|
getAudioContext(); // creates clockBridge
|
||||||
|
}
|
||||||
|
return clockBridge;
|
||||||
|
}
|
||||||
|
|
||||||
export function getAudioContextCurrentTime() {
|
export function getAudioContextCurrentTime() {
|
||||||
return getAudioContext().currentTime;
|
return getAudioContext().currentTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
clockbridge.mjs
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// thanks freya https://youtu.be/LSNQuFEDOyQ?si=ukZI2IGgWV_NDZzP&t=2979
|
||||||
|
export function expDecay(a, b, decay, dt) {
|
||||||
|
return b + (a - b) * Math.exp(-decay * dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// translates between audio and performance clock
|
||||||
|
export class ClockBridge {
|
||||||
|
p; // smoothed clock offset
|
||||||
|
lastTime;
|
||||||
|
audioContext;
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
}
|
||||||
|
// delta between audio and performance time in ms
|
||||||
|
getOffset() {
|
||||||
|
const { contextTime, performanceTime } = this.audioContext.getOutputTimestamp();
|
||||||
|
if (!contextTime || !performanceTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const offset = performanceTime - contextTime * 1000; // clock offset in ms
|
||||||
|
const dt = performanceTime - (this.lastTime ?? performanceTime); // delta time since last hap
|
||||||
|
const decay = 1 / 10000; // how fast offset changes have an effect
|
||||||
|
this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset
|
||||||
|
this.lastTime = performanceTime;
|
||||||
|
return this.p;
|
||||||
|
}
|
||||||
|
getPerformanceTime(audioContextTime) {
|
||||||
|
const offset = this.getOffset();
|
||||||
|
return audioContextTime * 1000 + offset; // this is now correct in performance time (ms)
|
||||||
|
}
|
||||||
|
getAudioContextTime(performanceTime) {
|
||||||
|
const offset = this.getOffset();
|
||||||
|
return (performanceTime - offset) / 1000; // this is now correct in audio context time (seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,13 +67,14 @@ if (typeof window !== 'undefined') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dough = async (code) => {
|
export const rawdsp = async (code) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
stop();
|
stop();
|
||||||
worklet = await dspWorklet(ac, code);
|
worklet = await dspWorklet(ac, code);
|
||||||
worklet.node.connect(ac.destination);
|
worklet.node.connect(ac.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doughTrigger(hap, currentTime, cps, targetTime) {
|
export function rawdspScheduler(hap, time, cps) {
|
||||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
const currentTime = getAudioContext().currentTime;
|
||||||
|
window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
|||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
export function gainNode(value) {
|
export function gainNode(value, audioContext = getAudioContext()) {
|
||||||
const node = getAudioContext().createGain();
|
const node = audioContext.createGain();
|
||||||
node.gain.value = value;
|
node.gain.value = value;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
|
|
||||||
// Certain browsers requires audio nodes to be connected in order for their onended events
|
// Certain browsers requires audio nodes to be connected in order for their onended events
|
||||||
// to fire, so we _mute it_ and then connect it to the destination
|
// to fire, so we _mute it_ and then connect it to the destination
|
||||||
const zeroGain = gainNode(0);
|
const zeroGain = gainNode(0, audioContext);
|
||||||
zeroGain.connect(audioContext.destination);
|
zeroGain.connect(audioContext.destination);
|
||||||
constantNode.connect(zeroGain);
|
constantNode.connect(zeroGain);
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from './modulators.mjs';
|
|||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
export * from './audioContext.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
export * from './clockbridge.mjs';
|
||||||
|
|||||||
@@ -5,11 +5,24 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 { releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
const nodePools = new Map();
|
const nodePools = new Map();
|
||||||
const POOL_KEY = Symbol('nodePoolKey');
|
const POOL_KEY = Symbol('nodePoolKey');
|
||||||
|
|
||||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||||
|
|
||||||
|
export const clearNodePool = () => {
|
||||||
|
for (const pool of nodePools) {
|
||||||
|
for (const node of pool) {
|
||||||
|
if (node instanceof AudioNode) {
|
||||||
|
releaseAudioNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodePools.clear();
|
||||||
|
};
|
||||||
|
|
||||||
const getNodeTime = (node) => {
|
const getNodeTime = (node) => {
|
||||||
return node.context?.currentTime ?? 0;
|
return node.context?.currentTime ?? 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1048,7 +1048,3 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
|
||||||
superdough(hap, t - ct, hap.duration / cps, cps);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -137,7 +137,14 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones)
|
? Interval.fromSemitones(intervalOrSemitones)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
const targetNote = Note.transpose(note, interval);
|
|
||||||
|
let n = Note.get(note);
|
||||||
|
if (n.oct == undefined) {
|
||||||
|
n.oct = 3;
|
||||||
|
let n1 = Note.get(Note.transpose(n, interval));
|
||||||
|
n.oct = n1.oct == 3 ? undefined : 3;
|
||||||
|
}
|
||||||
|
const targetNote = Note.transpose(n, interval);
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ let modeTarget = {
|
|||||||
duck: (v) => v.slice(-1)[0],
|
duck: (v) => v.slice(-1)[0],
|
||||||
above: (v) => v[0],
|
above: (v) => v[0],
|
||||||
root: (v) => v[0],
|
root: (v) => v[0],
|
||||||
|
oldabove: (v) => v[0],
|
||||||
|
oldroot: (v) => v[0],
|
||||||
|
};
|
||||||
|
let modeMult = {
|
||||||
|
below: 1,
|
||||||
|
duck: 1,
|
||||||
|
above: -1,
|
||||||
|
root: -1,
|
||||||
|
oldabove: 1,
|
||||||
|
oldroot: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
|
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
|
||||||
@@ -144,19 +154,20 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
|
|||||||
const voicings = dictionary[symbol].map((voicing) =>
|
const voicings = dictionary[symbol].map((voicing) =>
|
||||||
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
||||||
);
|
);
|
||||||
|
const mult = modeMult[mode];
|
||||||
|
|
||||||
let minDistance, bestIndex;
|
let minDistance, bestIndex;
|
||||||
// calculate distances up from voicing top notes
|
// calculate distances up from voicing top notes
|
||||||
let chromaDiffs = voicings.map((v, i) => {
|
let chromaDiffs = voicings.map((v, i) => {
|
||||||
const targetStep = modeTarget[mode](v);
|
const targetStep = modeTarget[mode](v);
|
||||||
const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
|
const diff = _mod((anchorChroma - targetStep - rootChroma) * mult, 12);
|
||||||
if (minDistance === undefined || diff < minDistance) {
|
if (minDistance === undefined || diff < minDistance) {
|
||||||
minDistance = diff;
|
minDistance = diff;
|
||||||
bestIndex = i;
|
bestIndex = i;
|
||||||
}
|
}
|
||||||
return diff;
|
return diff * mult;
|
||||||
});
|
});
|
||||||
if (mode === 'root') {
|
if (mode === 'root' || mode === 'oldroot') {
|
||||||
bestIndex = 0;
|
bestIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
|
|||||||
* - `below`: top note <= anchor
|
* - `below`: top note <= anchor
|
||||||
* - `duck`: top note <= anchor, anchor excluded
|
* - `duck`: top note <= anchor, anchor excluded
|
||||||
* - `above`: bottom note >= anchor
|
* - `above`: bottom note >= anchor
|
||||||
|
* - `root`: bottom note is the lowest root of the chord >= anchor
|
||||||
|
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
|
||||||
|
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
|
||||||
* - `offset`: whole number that shifts the voicing up or down to the next voicing
|
* - `offset`: whole number that shifts the voicing up or down to the next voicing
|
||||||
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
|
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const soundMap = new Map();
|
|||||||
const loadedSounds = new Map();
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
Pattern.prototype.supradough = function () {
|
Pattern.prototype.supradough = function () {
|
||||||
return this.onTrigger((hap, __, cps, begin) => {
|
return this.onSchedule((hap, begin, cps) => {
|
||||||
hap.value._begin = begin;
|
hap.value._begin = begin;
|
||||||
hap.value._duration = hap.duration / cps;
|
hap.value._duration = hap.duration / cps;
|
||||||
!doughWorklet && initDoughWorklet();
|
!doughWorklet && initDoughWorklet();
|
||||||
@@ -114,11 +114,11 @@ async function fetchSample(url) {
|
|||||||
return { channels, sampleRate: buffer.sampleRate };
|
return { channels, sampleRate: buffer.sampleRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doughsamples(sampleMap, baseUrl) {
|
export async function supradoughsamples(sampleMap, baseUrl) {
|
||||||
if (typeof sampleMap === 'string') {
|
if (typeof sampleMap === 'string') {
|
||||||
const [json, base] = await fetchSampleMap(sampleMap);
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
// console.log('json', json, 'base', base);
|
// console.log('json', json, 'base', base);
|
||||||
return doughsamples(json, base);
|
return supradoughsamples(json, base);
|
||||||
}
|
}
|
||||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||||
if (key !== '_base') {
|
if (key !== '_base') {
|
||||||
|
|||||||
+110
-29
@@ -9,7 +9,7 @@ import {
|
|||||||
superdough,
|
superdough,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
setLogger,
|
setLogger,
|
||||||
doughTrigger,
|
rawdspScheduler,
|
||||||
registerWorklet,
|
registerWorklet,
|
||||||
setAudioContext,
|
setAudioContext,
|
||||||
initAudio,
|
initAudio,
|
||||||
@@ -33,7 +33,8 @@ const hap2value = (hap) => {
|
|||||||
|
|
||||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
// TODO: refactor output callbacks to eliminate deadline
|
// TODO: refactor output callbacks to eliminate deadline
|
||||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
export const webaudioOutput = (hap, t, cps) => {
|
||||||
|
const hapDuration = hap.duration / cps;
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,32 +57,14 @@ export async function renderPatternAudio(
|
|||||||
maxPolyphony,
|
maxPolyphony,
|
||||||
multiChannelOrbits,
|
multiChannelOrbits,
|
||||||
});
|
});
|
||||||
logger('[webaudio] preloading');
|
|
||||||
|
|
||||||
// Calling superdough(...) in ascending onset time order is important
|
// Firefox currently doesn't support suspending an OfflineAudioContext,
|
||||||
// for controls that depend on the audio graph state like `cut`
|
// so no chunked rendering. Bad performance, but at least it works.
|
||||||
let haps = pattern
|
return (
|
||||||
.queryArc(begin, end, { _cps: cps })
|
audioContext.suspend === undefined
|
||||||
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
? renderPatternAudioWhole(audioContext, pattern, cps, begin, end)
|
||||||
for (const hap of haps) {
|
: renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1)
|
||||||
if (hap.hasOnset()) {
|
)
|
||||||
try {
|
|
||||||
await superdough(
|
|
||||||
hap2value(hap),
|
|
||||||
(hap.whole.begin.valueOf() - begin) / cps,
|
|
||||||
hap.duration / cps,
|
|
||||||
cps,
|
|
||||||
(hap.whole?.begin.valueOf() - begin) / cps,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
errorLogger(err, 'webaudio');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger('[webaudio] start rendering');
|
|
||||||
|
|
||||||
return audioContext
|
|
||||||
.startRendering()
|
|
||||||
.then((renderedBuffer) => {
|
.then((renderedBuffer) => {
|
||||||
const wavBuffer = audioBufferToWav(renderedBuffer);
|
const wavBuffer = audioBufferToWav(renderedBuffer);
|
||||||
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
||||||
@@ -102,6 +85,84 @@ export async function renderPatternAudio(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderPatternAudioWhole(audioContext, pattern, cps, begin, end) {
|
||||||
|
logger(`[webaudio] preloading`);
|
||||||
|
|
||||||
|
await scheduleHapsChunk(pattern, cps, begin, begin, end);
|
||||||
|
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
return audioContext.startRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, chunkSizeInCycles) {
|
||||||
|
let currentCycle = begin;
|
||||||
|
let renderPromise = null;
|
||||||
|
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
while (currentCycle <= end) {
|
||||||
|
const chunkStart = currentCycle;
|
||||||
|
const chunkEnd = Math.min(currentCycle + chunkSizeInCycles, end);
|
||||||
|
|
||||||
|
logger(`[webaudio] preloading cycles ${chunkStart} - ${chunkEnd}`);
|
||||||
|
|
||||||
|
await scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd);
|
||||||
|
|
||||||
|
logger(`[webaudio] rendering cycles ${chunkStart} - ${chunkEnd}`);
|
||||||
|
|
||||||
|
currentCycle += chunkSizeInCycles;
|
||||||
|
|
||||||
|
// According to the MDN docs, suspends should be scheduled while
|
||||||
|
// the audioContext is not currently running for better precision.
|
||||||
|
// So we schedule the suspend first, and await after resuming.
|
||||||
|
var suspendPromise;
|
||||||
|
if (currentCycle < end) {
|
||||||
|
// Make sure to suspend one cycle before the next currentCycle
|
||||||
|
// so the next haps can be scheduled on time.
|
||||||
|
suspendPromise = audioContext.suspend((currentCycle - begin - 1) / cps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderPromise === null) {
|
||||||
|
renderPromise = audioContext.startRendering();
|
||||||
|
} else {
|
||||||
|
await audioContext.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCycle < end) {
|
||||||
|
await suspendPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('[webaudio] finish rendering');
|
||||||
|
|
||||||
|
return renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd) {
|
||||||
|
// Calling superdough(...) in ascending onset time order is important
|
||||||
|
// for controls that depend on the audio graph state like `cut`
|
||||||
|
let haps = pattern
|
||||||
|
.queryArc(chunkStart, chunkEnd, { _cps: cps })
|
||||||
|
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
||||||
|
|
||||||
|
for (const hap of haps) {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
try {
|
||||||
|
await superdough(
|
||||||
|
hap2value(hap),
|
||||||
|
(hap.whole.begin.valueOf() - begin) / cps,
|
||||||
|
hap.duration / cps,
|
||||||
|
cps,
|
||||||
|
(hap.whole?.begin.valueOf() - begin) / cps,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errorLogger(err, 'webaudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function webaudioRepl(options = {}) {
|
export function webaudioRepl(options = {}) {
|
||||||
const audioContext = options.audioContext ?? getAudioContext();
|
const audioContext = options.audioContext ?? getAudioContext();
|
||||||
setAudioContext(audioContext);
|
setAudioContext(audioContext);
|
||||||
@@ -113,8 +174,8 @@ export function webaudioRepl(options = {}) {
|
|||||||
return repl(options);
|
return repl(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.dough = function () {
|
Pattern.prototype.rawdsp = function () {
|
||||||
return this.onTrigger(doughTrigger, 1);
|
return this.onSchedule(rawdspScheduler, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function audioBufferToWav(buffer, opt) {
|
function audioBufferToWav(buffer, opt) {
|
||||||
@@ -211,3 +272,23 @@ function writeString(view, offset, string) {
|
|||||||
view.setUint8(offset + i, string.charCodeAt(i));
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* make something happen on event time
|
||||||
|
* uses browser timeout which is innacurate for audio tasks
|
||||||
|
* @name onTriggerTime
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||||
|
*/
|
||||||
|
Pattern.prototype.onTriggerTime = function (func) {
|
||||||
|
return this.onSchedule((hap, targetTime) => {
|
||||||
|
const diff = targetTime - getAudioContext().currentTime;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
func(hap);
|
||||||
|
}, diff * 1000);
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|||||||
Generated
+40
-1
@@ -303,6 +303,22 @@ importers:
|
|||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
|
|
||||||
|
packages/dough:
|
||||||
|
dependencies:
|
||||||
|
'@strudel/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@strudel/webaudio':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../webaudio
|
||||||
|
dough-synth:
|
||||||
|
specifier: 0.2.4
|
||||||
|
version: 0.2.4
|
||||||
|
devDependencies:
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.11
|
||||||
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
|
||||||
packages/draw:
|
packages/draw:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
@@ -470,6 +486,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@strudel/webaudio':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../webaudio
|
||||||
osc:
|
osc:
|
||||||
specifier: ^2.4.5
|
specifier: ^2.4.5
|
||||||
version: 2.4.5
|
version: 2.4.5
|
||||||
@@ -808,6 +827,9 @@ importers:
|
|||||||
'@strudel/desktopbridge':
|
'@strudel/desktopbridge':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/desktopbridge
|
version: link:../packages/desktopbridge
|
||||||
|
'@strudel/dough':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/dough
|
||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/draw
|
version: link:../packages/draw
|
||||||
@@ -2066,6 +2088,7 @@ packages:
|
|||||||
'@lerna/create@8.1.9':
|
'@lerna/create@8.1.9':
|
||||||
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: This package is an implementation detail of Lerna and is no longer published separately.
|
||||||
|
|
||||||
'@lezer/common@1.2.3':
|
'@lezer/common@1.2.3':
|
||||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||||
@@ -3065,6 +3088,7 @@ packages:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||||
|
|
||||||
'@vite-pwa/astro@0.5.0':
|
'@vite-pwa/astro@0.5.0':
|
||||||
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
|
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
|
||||||
@@ -3716,6 +3740,7 @@ packages:
|
|||||||
conventional-changelog-core@5.0.1:
|
conventional-changelog-core@5.0.1:
|
||||||
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
|
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
|
||||||
|
|
||||||
conventional-changelog-preset-loader@3.0.0:
|
conventional-changelog-preset-loader@3.0.0:
|
||||||
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
|
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
|
||||||
@@ -4047,6 +4072,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dough-synth@0.2.4:
|
||||||
|
resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
dset@3.1.4:
|
dset@3.1.4:
|
||||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4579,6 +4608,7 @@ packages:
|
|||||||
git-raw-commits@3.0.0:
|
git-raw-commits@3.0.0:
|
||||||
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
|
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-remote-origin-url@2.0.0:
|
git-remote-origin-url@2.0.0:
|
||||||
@@ -4588,6 +4618,7 @@ packages:
|
|||||||
git-semver-tags@5.0.1:
|
git-semver-tags@5.0.1:
|
||||||
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
|
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-up@7.0.0:
|
git-up@7.0.0:
|
||||||
@@ -4615,15 +4646,17 @@ packages:
|
|||||||
|
|
||||||
glob@10.4.5:
|
glob@10.4.5:
|
||||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
glob@9.3.5:
|
glob@9.3.5:
|
||||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
globalize@0.1.1:
|
globalize@0.1.1:
|
||||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||||
@@ -6406,6 +6439,7 @@ packages:
|
|||||||
prebuild-install@7.1.1:
|
prebuild-install@7.1.1:
|
||||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
precinct@12.1.2:
|
precinct@12.1.2:
|
||||||
@@ -7230,6 +7264,7 @@ packages:
|
|||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
temp-dir@1.0.0:
|
temp-dir@1.0.0:
|
||||||
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
|
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
|
||||||
@@ -7357,6 +7392,7 @@ packages:
|
|||||||
tsconfck@3.1.4:
|
tsconfck@3.1.4:
|
||||||
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
|
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
|
deprecated: unmaintained
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5.0.0
|
typescript: ^5.0.0
|
||||||
@@ -7636,6 +7672,7 @@ packages:
|
|||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0:
|
||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
|
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
@@ -11757,6 +11794,8 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.4.7: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
|
dough-synth@0.2.4: {}
|
||||||
|
|
||||||
dset@3.1.4: {}
|
dset@3.1.4: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
|
|||||||
@@ -639,30 +639,30 @@ exports[`runs examples > example "K" example index 0 1`] = `
|
|||||||
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 2/1 → 25/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 2/1 → 25/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 9/4 → 7/3 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ 9/4 → 7/3 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 5/2 → 31/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 5/2 → 31/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 11/4 → 17/6 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 11/4 → 17/6 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 3/1 → 37/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 3/1 → 37/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 13/4 → 10/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ 13/4 → 10/3 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 7/2 → 43/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 7/2 → 43/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 15/4 → 23/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 15/4 → 23/6 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
]
|
]
|
||||||
@@ -3422,6 +3422,30 @@ exports[`runs examples > example "djf" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "dough" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
|
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "drive" example index 0 1`] = `
|
exports[`runs examples > example "drive" example index 0 1`] = `
|
||||||
@@ -5930,6 +5954,30 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "initDough" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "inside" example index 0 1`] = `
|
exports[`runs examples > example "inside" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 ]",
|
"[ 0/1 → 1/8 | note:D3 ]",
|
||||||
@@ -8474,6 +8522,19 @@ exports[`runs examples > example "often" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "onSchedule" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/2 | s:bd ]",
|
||||||
|
"[ 1/2 → 1/1 | s:sd ]",
|
||||||
|
"[ 1/1 → 3/2 | s:bd ]",
|
||||||
|
"[ 3/2 → 2/1 | s:sd ]",
|
||||||
|
"[ 2/1 → 5/2 | s:bd ]",
|
||||||
|
"[ 5/2 → 3/1 | s:sd ]",
|
||||||
|
"[ 3/1 → 7/2 | s:bd ]",
|
||||||
|
"[ 7/2 → 4/1 | s:sd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
"[ 0/1 → 1/8 | s:bd ]",
|
||||||
|
|||||||
@@ -7203,8 +7203,12 @@ exports[`renders tunes > tune: holyflute 1`] = `
|
|||||||
|
|
||||||
exports[`renders tunes > tune: juxUndTollerei 1`] = `
|
exports[`renders tunes > tune: juxUndTollerei 1`] = `
|
||||||
[
|
[
|
||||||
|
"[ -99/200 ⇜ (0/1 → 1/200) | note:63 s:triangle pan:0 cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ -99/200 ⇜ (0/1 → 1/200) | note:67 s:triangle pan:1 color:green cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ 1/200 → 101/200 | note:55 s:triangle pan:0 cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ 1/200 → 101/200 | note:65 s:triangle pan:1 color:green cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const toneHelpersMocked = {
|
|||||||
'_spectrum',
|
'_spectrum',
|
||||||
'markcss',
|
'markcss',
|
||||||
'p',
|
'p',
|
||||||
|
'dough',
|
||||||
].forEach((mock) => {
|
].forEach((mock) => {
|
||||||
strudel.Pattern.prototype[mock] = function () {
|
strudel.Pattern.prototype[mock] = function () {
|
||||||
return this;
|
return this;
|
||||||
@@ -174,6 +175,7 @@ evalScope(
|
|||||||
getDuration,
|
getDuration,
|
||||||
setcps: id,
|
setcps: id,
|
||||||
setcpm: id,
|
setcpm: id,
|
||||||
|
initDough: id,
|
||||||
Clock: {}, // whatever
|
Clock: {}, // whatever
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -597,8 +597,8 @@
|
|||||||
],
|
],
|
||||||
"/packages/superdough/dspworklet.mjs": [
|
"/packages/superdough/dspworklet.mjs": [
|
||||||
"dspWorklet",
|
"dspWorklet",
|
||||||
"dough",
|
"rawdsp",
|
||||||
"doughTrigger"
|
"rawdspTrigger"
|
||||||
],
|
],
|
||||||
"/packages/superdough/index.mjs": [],
|
"/packages/superdough/index.mjs": [],
|
||||||
"/packages/tonal/tonleiter.mjs": [
|
"/packages/tonal/tonleiter.mjs": [
|
||||||
|
|||||||
@@ -145,4 +145,13 @@ export default defineConfig({
|
|||||||
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
|
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
// these are needed for dough, which uses shared memory
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||||
|
// this is only for the dev server, so the server strudel runs on, also needs those
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'credentialless',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@strudel/motion": "workspace:*",
|
"@strudel/motion": "workspace:*",
|
||||||
"@strudel/mqtt": "workspace:*",
|
"@strudel/mqtt": "workspace:*",
|
||||||
"@strudel/osc": "workspace:*",
|
"@strudel/osc": "workspace:*",
|
||||||
|
"@strudel/dough": "workspace:*",
|
||||||
"@strudel/serial": "workspace:*",
|
"@strudel/serial": "workspace:*",
|
||||||
"@strudel/soundfonts": "workspace:*",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tidal": "workspace:*",
|
"@strudel/tidal": "workspace:*",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
|||||||
|
|
||||||
<base href={BASE_URL} />
|
<base href={BASE_URL} />
|
||||||
|
|
||||||
<!-- Scrollable a11y code helper -->
|
<!-- Scrollable accessibility code helper -->
|
||||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||||
|
|
||||||
<script src="/src/pwa.ts"></script>
|
<script src="/src/pwa.ts"></script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
|||||||
|
|
||||||
<base href={BASE_URL} />
|
<base href={BASE_URL} />
|
||||||
|
|
||||||
<!-- Scrollable a11y code helper -->
|
<!-- Scrollable accessibility code helper -->
|
||||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||||
|
|
||||||
<script src="/src/pwa.ts"></script>
|
<script src="/src/pwa.ts"></script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
|||||||
<!-- Page Metadata -->
|
<!-- Page Metadata -->
|
||||||
<link rel="canonical" href={canonicalUrl} />
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
|
||||||
<!-- OpenGraph Tags -->
|
<!-- og Tags -->
|
||||||
<meta property="og:title" content={formattedContentTitle} />
|
<meta property="og:title" content={formattedContentTitle} />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:url" content={canonicalUrl} />
|
<meta property="og:url" content={canonicalUrl} />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function MiniRepl({
|
|||||||
tunes,
|
tunes,
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
canvasHeight = 100,
|
canvasHeight = 100,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels = true,
|
punchcardLabels = true,
|
||||||
claviature,
|
claviature,
|
||||||
@@ -58,8 +58,8 @@ export function MiniRepl({
|
|||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
editPattern: (pat, id) => {
|
editPattern: (pat, id) => {
|
||||||
if (onTrigger) {
|
if (onSchedule) {
|
||||||
pat = pat.onTrigger(onTrigger, false);
|
pat = pat.onSchedule(onSchedule, false);
|
||||||
}
|
}
|
||||||
if (claviature) {
|
if (claviature) {
|
||||||
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
||||||
|
|||||||
@@ -150,5 +150,3 @@ In the future, the integration could be improved by passing all patterned contro
|
|||||||
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
|
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
|
||||||
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
|
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
|
||||||
Also, it might make sense to have a standard library of csound instruments for strudel's effects.
|
Also, it might make sense to have a standard library of csound instruments for strudel's effects.
|
||||||
|
|
||||||
Now, let's dive into the [Functional JavaScript API](/functions/intro)
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Here, the LFO will modulate the low pass filter `.lpf`.
|
|||||||
|
|
||||||
## Moving away from the default
|
## Moving away from the default
|
||||||
|
|
||||||
The following sections explain how pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
|
The following sections explain how to pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
|
||||||
All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
|
All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
|
||||||
|
|
||||||
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
|
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|
||||||
## onTrigger
|
## onSchedule
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#onTrigger" h={0} />
|
<JsDoc client:idle name="Pattern#onSchedule" h={0} />
|
||||||
|
|
||||||
## log
|
## log
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP
|
|||||||
|
|
||||||
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
||||||
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
||||||
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
|
3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output.
|
||||||
|
|
||||||
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
||||||
|
|
||||||
@@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const haps = pattern.queryArc(time, time + interval);
|
const haps = pattern.queryArc(time, time + interval);
|
||||||
time += interval; // increment time
|
time += interval; // increment time
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
|
||||||
const deadline = hap.whole.begin - time + minLatency;
|
|
||||||
onTrigger(hap, deadline, duration);
|
|
||||||
});
|
|
||||||
}, interval * 1000); // query each "interval" seconds
|
}, interval * 1000); // query each "interval" seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us
|
|||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function onTrigger(hap, deadline, duration) {
|
function onSchedule(hap, time, cps) {
|
||||||
const { note } = hap.value;
|
const { note } = hap.value;
|
||||||
const time = getAudioContext().currentTime + deadline;
|
|
||||||
const o = getAudioContext().createOscillator();
|
const o = getAudioContext().createOscillator();
|
||||||
o.frequency.value = getFreq(note);
|
o.frequency.value = getFreq(note);
|
||||||
o.start(time);
|
o.start(time);
|
||||||
o.stop(time + event.duration);
|
o.stop(time + hap.duration / cps);
|
||||||
o.connect(getAudioContext().destination);
|
o.connect(getAudioContext().destination);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||||
|
|
||||||
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
||||||
|
|||||||
@@ -287,7 +287,10 @@ The modes are:
|
|||||||
- `below`: the top note of the voicing is lower than or equal to the anchor (default)
|
- `below`: the top note of the voicing is lower than or equal to the anchor (default)
|
||||||
- `above`: the bottom note of the voicing is higher than or equal to the anchor
|
- `above`: the bottom note of the voicing is higher than or equal to the anchor
|
||||||
- `duck`: the top note of the voicing is lower than the anchor
|
- `duck`: the top note of the voicing is lower than the anchor
|
||||||
- `root`: the bottom note of the voicing is always the root note closest to the anchor
|
- `root`: the bottom note of the voicing is always the lowest root note higher than or equal to the anchor
|
||||||
|
|
||||||
|
- `oldabove` : old (buggy) behavior of above, kept for legacy reasons
|
||||||
|
- `oldroot` : old (buggy) behavior of root, kept for legacy reasons
|
||||||
|
|
||||||
The `anchor` can also be set from within the `mode` function:
|
The `anchor` can also be set from within the `mode` function:
|
||||||
|
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => {
|
|||||||
|
|
||||||
/*global all*/
|
/*global all*/
|
||||||
all((pat) =>
|
all((pat) =>
|
||||||
pat.onTrigger(async (hap, duration, cps, t) => {
|
pat.onSchedule(async (hap, t, cps) => {
|
||||||
hap_count++;
|
hap_count++;
|
||||||
const key = Object.entries(hap.value)
|
const key = Object.entries(hap.value)
|
||||||
.map((param) => param.join('/'))
|
.map((param) => param.join('/'))
|
||||||
|
|||||||
@@ -214,7 +214,12 @@ export const Reference = memo(function Reference() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
|
<p
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
// If we have an external url make sure it opens in another tab to not interrupt the player
|
||||||
|
__html: entry.description.replaceAll(/<a href="https:\/\//g, `<a target="_blank" href="https://`),
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
<ul>
|
<ul>
|
||||||
{entry.params?.map(({ name, type, description }, i) => (
|
{entry.params?.map(({ name, type, description }, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export function loadModules() {
|
|||||||
import('@strudel/motion'),
|
import('@strudel/motion'),
|
||||||
import('@strudel/mqtt'),
|
import('@strudel/mqtt'),
|
||||||
import('@strudel/mondo'),
|
import('@strudel/mondo'),
|
||||||
|
import('@strudel/dough'),
|
||||||
];
|
];
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
modules = modules.concat([
|
modules = modules.concat([
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
|||||||
|
|
||||||
const patternSetting = (key) =>
|
const patternSetting = (key) =>
|
||||||
register(key, (value, pat) =>
|
register(key, (value, pat) =>
|
||||||
pat.onTrigger(() => {
|
pat.onSchedule(() => {
|
||||||
value = Array.isArray(value) ? value.join(' ') : value;
|
value = Array.isArray(value) ? value.join(' ') : value;
|
||||||
if (value !== settingsMap.get()[key]) {
|
if (value !== settingsMap.get()[key]) {
|
||||||
settingsMap.setKey(key, value);
|
settingsMap.setKey(key, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user