mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-07 17:32:02 -04:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 947dd76823 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| ded91fab7d | |||
| f40d079543 | |||
| d5833dd475 | |||
| e3658ec30e | |||
| 72eaf96b4f | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 7eebf3a466 | |||
| 6f703e48e7 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| cbcb25b063 | |||
| 1ae773acc4 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| 120f89d57f | |||
| d496919fe5 | |||
| f369de13d7 | |||
| 99e14dae5c | |||
| ac582b4d40 | |||
| b7d98dd370 | |||
| 0ecacae71e | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 72298c5f03 | |||
| c885d84785 | |||
| a14bf4d97b | |||
| c5f1085bd9 | |||
| ccbf28f2db | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| f360d394a7 | |||
| 07e3ddbe40 | |||
| b3537a9acb | |||
| f0669ce3ba | |||
| 69b034349a | |||
| 5f7e10733a | |||
| 9848740e18 | |||
| 51b150b829 | |||
| 9b1c23d49f | |||
| 42ab65abdb | |||
| bb982e6b9b | |||
| 28eedae730 | |||
| e075095e8b | |||
| 84e3f12171 | |||
| 260c6e0783 | |||
| bc3481822c |
@@ -1887,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
|||||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||||
/**
|
/**
|
||||||
* Wave shaping distortion. CAUTION: it can get loud.
|
* Wave shaping distortion. CAUTION: it can get loud.
|
||||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
||||||
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
||||||
*
|
*
|
||||||
* @name distort
|
* @name distort
|
||||||
* @synonyms dist
|
* @synonyms dist
|
||||||
* @param {number | Pattern} distortion
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
* @param {number | string | Pattern} type type of distortion to apply
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||||
* @example
|
* @example
|
||||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
||||||
|
* @example
|
||||||
|
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
|
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postgain for waveshaping distortion.
|
||||||
|
*
|
||||||
|
* @name distortvol
|
||||||
|
* @synonyms distvol
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
* @example
|
||||||
|
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
|
||||||
|
*/
|
||||||
|
export const { distortvol } = registerControl('distortvol', 'distvol');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of waveshaping distortion to apply.
|
||||||
|
*
|
||||||
|
* @name distorttype
|
||||||
|
* @synonyms disttype
|
||||||
|
* @param {number | string | Pattern} type type of distortion to apply
|
||||||
|
* @example
|
||||||
|
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("sine").note("F1*2").release(1)
|
||||||
|
* .penv(24).pdecay(0.05)
|
||||||
|
* .distort(rand.range(1, 8))
|
||||||
|
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
|
||||||
|
*/
|
||||||
|
export const { distorttype } = registerControl('distorttype', 'disttype');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
||||||
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||||
@@ -2287,6 +2320,23 @@ export const { miditouch } = registerControl('miditouch');
|
|||||||
// TODO: what is this?
|
// TODO: what is this?
|
||||||
export const { polyTouch } = registerControl('polyTouch');
|
export const { polyTouch } = registerControl('polyTouch');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a control name exists in the controlAlias map.
|
||||||
|
* @name hasControlName
|
||||||
|
* @param {string} alias The control name to check
|
||||||
|
* @returns {boolean} True if the control name exists, false otherwise
|
||||||
|
*/
|
||||||
|
export const hasControlName = (alias) => {
|
||||||
|
// Check if the name exists as a key (alias) or value (main control name)
|
||||||
|
return controlAlias.has(alias) || Array.from(controlAlias.values()).includes(alias);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the control name from the controlAlias map.
|
||||||
|
* @name getControlName
|
||||||
|
* @param {string} alias The control name to get
|
||||||
|
* @returns {string} The control name
|
||||||
|
*/
|
||||||
export const getControlName = (alias) => {
|
export const getControlName = (alias) => {
|
||||||
if (controlAlias.has(alias)) {
|
if (controlAlias.has(alias)) {
|
||||||
return controlAlias.get(alias);
|
return controlAlias.get(alias);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// query the pattern for events
|
// query the pattern for events
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ export class NeoCyclist {
|
|||||||
if (this.started === false) {
|
if (this.started === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
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);
|
||||||
|
|||||||
@@ -98,10 +98,7 @@ export class Pattern {
|
|||||||
|
|
||||||
// runs func on query state
|
// runs func on query state
|
||||||
withState(func) {
|
withState(func) {
|
||||||
return this.withHaps((haps, state) => {
|
return new Pattern((state) => this.query(func(state)));
|
||||||
func(state);
|
|
||||||
return haps;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3536,3 +3533,76 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
bypat = reify(bypat);
|
bypat = reify(bypat);
|
||||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-clipping distortion
|
||||||
|
*
|
||||||
|
* @name soft
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Hard-clipping distortion
|
||||||
|
*
|
||||||
|
* @name hard
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Cubic polynomial distortion
|
||||||
|
*
|
||||||
|
* @name cubic
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Diode-emulating distortion
|
||||||
|
*
|
||||||
|
* @name diode
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Asymmetrical diode distortion
|
||||||
|
*
|
||||||
|
* @name asym
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Wavefolding distortion
|
||||||
|
*
|
||||||
|
* @name fold
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Wavefolding distortion composed with sinusoid
|
||||||
|
*
|
||||||
|
* @name sinefold
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Distortion via Chebyshev polynomials
|
||||||
|
*
|
||||||
|
* @name chebyshev
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||||
|
for (const name of distAlgoNames) {
|
||||||
|
// Add aliases for distortion algorithms
|
||||||
|
Pattern.prototype[name] = function (args) {
|
||||||
|
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||||
|
return this.distort(argsPat);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,7 +214,10 @@ export function repl({
|
|||||||
}
|
}
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
if (Object.keys(pPatterns).length) {
|
if (Object.keys(pPatterns).length) {
|
||||||
let patterns = Object.values(pPatterns);
|
let patterns = [];
|
||||||
|
for (const [key, value] of Object.entries(pPatterns)) {
|
||||||
|
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||||
|
}
|
||||||
if (eachTransform) {
|
if (eachTransform) {
|
||||||
// Explicit lambda so only element (not index and array) are passed
|
// Explicit lambda so only element (not index and array) are passed
|
||||||
patterns = patterns.map((x) => eachTransform(x));
|
patterns = patterns.map((x) => eachTransform(x));
|
||||||
@@ -228,6 +231,7 @@ export function repl({
|
|||||||
pattern = allTransforms[i](pattern);
|
pattern = allTransforms[i](pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPattern(pattern)) {
|
if (!isPattern(pattern)) {
|
||||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export class State {
|
|||||||
return this.setSpan(func(this.span));
|
return this.setSpan(func(this.span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns new State with different controls
|
// Returns new State with added controls.
|
||||||
setControls(controls) {
|
setControls(controls) {
|
||||||
return new State(this.span, controls);
|
return new State(this.span, { ...this.controls, ...controls });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ This package adds midi functionality to strudel Patterns.
|
|||||||
npm i @strudel/midi --save
|
npm i @strudel/midi --save
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Enabling MIDI for Local Development in Chrome
|
||||||
|
|
||||||
|
1. Open Chrome and navigate to `chrome://flags`
|
||||||
|
2. Search for "Insecure origins treated as secure"
|
||||||
|
3. In the text field that appears, add your development origin (e.g., http://localhost:3000)
|
||||||
|
4. Enable the flag
|
||||||
|
5. Restart Chrome
|
||||||
|
|
||||||
## Available Controls
|
## Available Controls
|
||||||
|
|
||||||
The following MIDI controls are available:
|
The following MIDI controls are available:
|
||||||
|
|||||||
+40
-7
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||||
import { noteToMidi, getControlName } from '@strudel/core';
|
import { noteToMidi, hasControlName, getControlName, registerControl } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
@@ -100,10 +100,34 @@ export const midicontrolMap = new Map();
|
|||||||
function unifyMapping(mapping) {
|
function unifyMapping(mapping) {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(mapping).map(([key, mapping]) => {
|
Object.entries(mapping).map(([key, mapping]) => {
|
||||||
|
// Convert number to object with ccn property
|
||||||
if (typeof mapping === 'number') {
|
if (typeof mapping === 'number') {
|
||||||
mapping = { ccn: mapping };
|
mapping = { ccn: mapping };
|
||||||
}
|
}
|
||||||
return [getControlName(key), mapping];
|
|
||||||
|
// Get the non-aliased control name from the key
|
||||||
|
const controlName = getControlName(key);
|
||||||
|
|
||||||
|
// Check if the key or controlName already exists in the controlAlias map
|
||||||
|
if (hasControlName(key) || hasControlName(controlName)) {
|
||||||
|
// Show warning and carry on.
|
||||||
|
logger(`[midimap] '[${key}, ${controlName}]' overwrites a Strudel API.`);
|
||||||
|
|
||||||
|
// Throw error to stop the music
|
||||||
|
//throw new Error(`[midimap] '${key}' overwrites a Strudel API.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the control in the midicontrolMap if it doesn't exist
|
||||||
|
if (!midicontrolMap.has(controlName)) {
|
||||||
|
try {
|
||||||
|
registerControl(controlName);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`[midimap] Failed to register midimap control '${controlName}': ${err.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger(`[midimap] '${controlName}' already registered as a midimap control. Skipping registration.`);
|
||||||
|
}
|
||||||
|
return [controlName, mapping];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,7 +186,14 @@ export async function midimaps(map) {
|
|||||||
map = await loadCache[map];
|
map = await loadCache[map];
|
||||||
}
|
}
|
||||||
if (typeof map === 'object') {
|
if (typeof map === 'object') {
|
||||||
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
|
Object.entries(map).forEach(([name, mapping]) => {
|
||||||
|
try {
|
||||||
|
midicontrolMap.set(name, unifyMapping(mapping));
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,18 +355,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
const device = getDevice(midiConfig.midiport, outputs);
|
const device = getDevice(midiConfig.midiport, outputs);
|
||||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`Midi enabled! Using "${device.name}". ${
|
`[midi] Midi enabled! Using "${device.name}". ${
|
||||||
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onDisconnected: ({ outputs }) =>
|
onDisconnected: ({ outputs }) =>
|
||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('[midi] Midi not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
@@ -383,7 +414,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
||||||
} else if (midimap !== 'default') {
|
} else if (midimap !== 'default') {
|
||||||
// Add warning when a non-existent midimap is specified
|
// Add warning when a non-existent midimap is specified
|
||||||
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
throw new Error(
|
||||||
|
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle note
|
// Handle note
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
|
|||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.withHap((hap) => {
|
return this.withHap((hap) => {
|
||||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
||||||
let msg_topic = topic;
|
let msg_topic = topic;
|
||||||
if (!cx || !cx.isConnected()) {
|
if (!cx || !cx.isConnected()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ export async function prebake() {
|
|||||||
// import('@strudel/osc'),
|
// import('@strudel/osc'),
|
||||||
);
|
);
|
||||||
// load samples
|
// load samples
|
||||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
|
||||||
|
|
||||||
// TODO: move this onto the strudel repo
|
// TODO: move this onto the strudel repo
|
||||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
|
const ts = 'https://raw.githubusercontent.com/todepond/samples/main';
|
||||||
|
|
||||||
|
const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
modulesLoading,
|
modulesLoading,
|
||||||
registerSynthSounds(),
|
registerSynthSounds(),
|
||||||
@@ -36,9 +39,9 @@ export async function prebake() {
|
|||||||
samples(`${ds}/tidal-drum-machines.json`),
|
samples(`${ds}/tidal-drum-machines.json`),
|
||||||
samples(`${ds}/piano.json`),
|
samples(`${ds}/piano.json`),
|
||||||
samples(`${ds}/Dirt-Samples.json`),
|
samples(`${ds}/Dirt-Samples.json`),
|
||||||
samples(`${ds}/uzu-drumkit.json`),
|
|
||||||
samples(`${ds}/vcsl.json`),
|
samples(`${ds}/vcsl.json`),
|
||||||
samples(`${ds}/mridangam.json`),
|
samples(`${ds}/mridangam.json`),
|
||||||
|
samples(`${tc}/strudel.json`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
|
|||||||
|
|
||||||
The format is `github:<user>/<repo>/<branch>`.
|
The format is `github:<user>/<repo>/<branch>`.
|
||||||
|
|
||||||
|
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
|
||||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||||
|
|
||||||
The format is also expected to be the same as explained above.
|
The format is also expected to be the same as explained above.
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
let audioContext;
|
||||||
|
|
||||||
|
export const setDefaultAudioContext = () => {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
return audioContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAudioContext = () => {
|
||||||
|
if (!audioContext) {
|
||||||
|
return setDefaultAudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAudioContextCurrentTime() {
|
||||||
|
return getAudioContext().currentTime;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let worklet;
|
let worklet;
|
||||||
export async function dspWorklet(ac, code) {
|
export async function dspWorklet(ac, code) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
@@ -381,6 +382,102 @@ export function applyFM(param, value, begin) {
|
|||||||
return { stop };
|
return { stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Saturation curves
|
||||||
|
|
||||||
|
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
||||||
|
const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
|
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||||
|
const _soft = (x, k) => Math.tanh(x * (1 + k));
|
||||||
|
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
||||||
|
|
||||||
|
const _fold = (x, k) => {
|
||||||
|
// Closed form folding for audio rate
|
||||||
|
let y = (1 + 0.5 * k) * x;
|
||||||
|
const window = _mod(y + 1, 4);
|
||||||
|
return 1 - Math.abs(window - 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
|
||||||
|
|
||||||
|
const _cubic = (x, k) => {
|
||||||
|
const t = __squash(Math.log1p(k));
|
||||||
|
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1)
|
||||||
|
return _soft(cubic, k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _diode = (x, k, asym = false) => {
|
||||||
|
const g = 1 + 2 * k; // gain
|
||||||
|
const t = __squash(Math.log1p(k));
|
||||||
|
const bias = 0.07 * t;
|
||||||
|
const pos = _soft(x + bias, 2 * k);
|
||||||
|
const neg = _soft(asym ? bias : -x + bias, 2 * k);
|
||||||
|
const y = pos - neg;
|
||||||
|
// We divide by the derivative at 0 so that the distortion is roughly
|
||||||
|
// the identity map near 0 => small values are preserved and undistorted
|
||||||
|
const sech = 1 / Math.cosh(g * bias);
|
||||||
|
const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2
|
||||||
|
const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x
|
||||||
|
return _soft(y / denom, k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _asym = (x, k) => _diode(x, k, true);
|
||||||
|
|
||||||
|
const _chebyshev = (x, k) => {
|
||||||
|
const kl = 10 * Math.log1p(k);
|
||||||
|
let tnm1 = 1;
|
||||||
|
let tnm2 = x;
|
||||||
|
let tn;
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 1; i < 64; i++) {
|
||||||
|
if (i < 2) {
|
||||||
|
// Already set inital conditions
|
||||||
|
y += i == 0 ? tnm1 : tnm2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
||||||
|
tnm2 = tnm1;
|
||||||
|
tnm1 = tn;
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
y += Math.min((1.3 * kl) / i, 2) * tn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Soft clip
|
||||||
|
return _soft(y, kl / 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distortionAlgorithms = {
|
||||||
|
scurve: _scurve,
|
||||||
|
soft: _soft,
|
||||||
|
hard: _hard,
|
||||||
|
cubic: _cubic,
|
||||||
|
diode: _diode,
|
||||||
|
asym: _asym,
|
||||||
|
fold: _fold,
|
||||||
|
sinefold: _sineFold,
|
||||||
|
chebyshev: _chebyshev,
|
||||||
|
};
|
||||||
|
const _algoNames = Object.freeze(Object.keys(distortionAlgorithms));
|
||||||
|
|
||||||
|
export const getDistortionAlgorithm = (algo) => {
|
||||||
|
let index = algo;
|
||||||
|
if (typeof algo === 'string') {
|
||||||
|
index = _algoNames.indexOf(algo);
|
||||||
|
if (index === -1) {
|
||||||
|
logger(`[superdough] Could not find waveshaping algorithm ${algo}.
|
||||||
|
Available options are ${_algoNames.join(', ')}.
|
||||||
|
Defaulting to ${_algoNames[0]}.`);
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number
|
||||||
|
return distortionAlgorithms[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDistortion = (distort, postgain, algorithm) => {
|
||||||
|
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||||
|
};
|
||||||
|
|
||||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
let { note, freq } = value;
|
let { note, freq } = value;
|
||||||
note = note || defaultNote;
|
note = note || defaultNote;
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ export * from './synth.mjs';
|
|||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
|
export * from './audioContext.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { drywet } from './helpers.mjs';
|
import { drywet } from './helpers.mjs';
|
||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -120,13 +121,20 @@ function githubPath(base, subpath = '') {
|
|||||||
if (!base.startsWith('github:')) {
|
if (!base.startsWith('github:')) {
|
||||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
}
|
}
|
||||||
let [_, path] = base.split('github:');
|
let path = base.slice('github:'.length);
|
||||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
if (path.split('/').length === 2) {
|
|
||||||
// assume main as default branch if none set
|
let components = path.split('/');
|
||||||
path += '/main';
|
let user = components[0];
|
||||||
|
let repo = components.length >= 2 ? components[1] : 'samples';
|
||||||
|
let branch = components.length >= 3 ? components[2] : 'main';
|
||||||
|
let other = components.slice(3);
|
||||||
|
if (subpath) {
|
||||||
|
other.push(subpath);
|
||||||
}
|
}
|
||||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
other = other.join('/');
|
||||||
|
|
||||||
|
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import './reverb.mjs';
|
|||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
@@ -154,6 +155,7 @@ let defaultDefaultValues = {
|
|||||||
phaserdepth: 0.75,
|
phaserdepth: 0.75,
|
||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
|
distorttype: 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
byteBeatExpression: '0',
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
@@ -200,25 +202,6 @@ export function setVersionDefaults(version) {
|
|||||||
|
|
||||||
export const resetLoadedSounds = () => soundMap.set({});
|
export const resetLoadedSounds = () => soundMap.set({});
|
||||||
|
|
||||||
let audioContext;
|
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
|
||||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
|
||||||
return audioContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAudioContext = () => {
|
|
||||||
if (!audioContext) {
|
|
||||||
return setDefaultAudioContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getAudioContextCurrentTime() {
|
|
||||||
return getAudioContext().currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
let externalWorklets = [];
|
let externalWorklets = [];
|
||||||
export function registerWorklet(url) {
|
export function registerWorklet(url) {
|
||||||
externalWorklets.push(url);
|
externalWorklets.push(url);
|
||||||
@@ -473,6 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
shapevol = getDefaultValue('shapevol'),
|
shapevol = getDefaultValue('shapevol'),
|
||||||
distort,
|
distort,
|
||||||
distortvol = getDefaultValue('distortvol'),
|
distortvol = getDefaultValue('distortvol'),
|
||||||
|
distorttype = getDefaultValue('distorttype'),
|
||||||
pan,
|
pan,
|
||||||
vowel,
|
vowel,
|
||||||
delay = getDefaultValue('delay'),
|
delay = getDefaultValue('delay'),
|
||||||
@@ -646,8 +630,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// effects
|
// effects
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||||
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
|
||||||
|
|
||||||
if (tremolosync != null) {
|
if (tremolosync != null) {
|
||||||
tremolo = cps * tremolosync;
|
tremolo = cps * tremolosync;
|
||||||
|
|||||||
@@ -135,12 +135,14 @@ export class SuperdoughOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
this.disconnect();
|
||||||
|
this.initializeAudio();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
this.channelMerger.disconnect();
|
this.channelMerger.disconnect();
|
||||||
this.destinationGain.disconnect();
|
this.destinationGain.disconnect();
|
||||||
this.destinationGain = null;
|
this.destinationGain = null;
|
||||||
this.channelMerger = null;
|
this.channelMerger = null;
|
||||||
this.nodes = {};
|
|
||||||
this.initializeAudio();
|
|
||||||
}
|
}
|
||||||
connectToDestination = (input, channels = [0, 1]) => {
|
connectToDestination = (input, channels = [0, 1]) => {
|
||||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||||
@@ -172,6 +174,7 @@ export class SuperdoughAudioController {
|
|||||||
Array.from(this.nodes).forEach((node) => {
|
Array.from(this.nodes).forEach((node) => {
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
});
|
});
|
||||||
|
this.nodes = {};
|
||||||
this.output.reset();
|
this.output.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
|
import { registerSound, soundMap } from './superdough.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
destroyAudioWorkletNode,
|
destroyAudioWorkletNode,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
destroyAudioWorkletNode,
|
destroyAudioWorkletNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
getLfo,
|
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
const WT_MAX_MIP_LEVELS = 6;
|
|
||||||
export const Warpmode = Object.freeze({
|
export const Warpmode = Object.freeze({
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
ASYM: 1,
|
ASYM: 1,
|
||||||
@@ -40,39 +39,25 @@ export const Warpmode = Object.freeze({
|
|||||||
FLIP: 21,
|
FLIP: 21,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadWavetableFrames(url, label, frameLen = 2048) {
|
const seenKeys = new Set();
|
||||||
const buf = await loadBuffer(url, label);
|
async function getPayload(url, label, frameLen = 2048) {
|
||||||
const ch0 = buf.getChannelData(0);
|
const key = `${url},${frameLen}`;
|
||||||
const total = ch0.length;
|
if (!seenKeys.has(key)) {
|
||||||
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
const buf = await loadBuffer(url, label);
|
||||||
const frames = new Array(numFrames);
|
const ch0 = buf.getChannelData(0);
|
||||||
for (let i = 0; i < numFrames; i++) {
|
const total = ch0.length;
|
||||||
const start = i * frameLen;
|
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||||
frames[i] = ch0.subarray(start, start + frameLen);
|
const frames = new Array(numFrames);
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
const start = i * frameLen;
|
||||||
|
frames[i] = ch0.subarray(start, start + frameLen);
|
||||||
|
}
|
||||||
|
seenKeys.add(key);
|
||||||
|
return { frames, frameLen, numFrames, key };
|
||||||
}
|
}
|
||||||
|
return { frameLen, key }; // worklet will use the cached version
|
||||||
// build mipmaps
|
|
||||||
const mipmaps = [frames];
|
|
||||||
let levelFrames = frames;
|
|
||||||
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
|
|
||||||
const prevLen = levelFrames[0].length;
|
|
||||||
if (prevLen <= 32) break;
|
|
||||||
const nextLen = prevLen >> 1;
|
|
||||||
const next = levelFrames.map((src) => {
|
|
||||||
const out = new Float32Array(nextLen);
|
|
||||||
for (let j = 0; j < nextLen; j++) {
|
|
||||||
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
mipmaps.push(next);
|
|
||||||
levelFrames = next;
|
|
||||||
}
|
|
||||||
return { frames, mipmaps, frameLen, numFrames };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCache = {};
|
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
var thresh = si ? 1000 : 1024;
|
var thresh = si ? 1000 : 1024;
|
||||||
if (bytes < thresh) return bytes + ' B';
|
if (bytes < thresh) return bytes + ' B';
|
||||||
@@ -117,6 +102,7 @@ async function decodeAtNativeRate(arr) {
|
|||||||
return await tempAC.decodeAudioData(arr);
|
return await tempAC.decodeAudioData(arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadCache = {};
|
||||||
const loadBuffer = (url, label) => {
|
const loadBuffer = (url, label) => {
|
||||||
url = url.replace('#', '%23');
|
url = url.replace('#', '%23');
|
||||||
if (!loadCache[url]) {
|
if (!loadCache[url]) {
|
||||||
@@ -222,7 +208,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||||
const { s, n = 0, duration } = value;
|
const { s, n = 0, duration, clip } = value;
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
let { warpmode } = value;
|
let { warpmode } = value;
|
||||||
@@ -231,8 +217,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
}
|
}
|
||||||
const frequency = getFrequencyFromValue(value);
|
const frequency = getFrequencyFromValue(value);
|
||||||
const { url, label } = getCommonSampleInfo(value, tables);
|
const { url, label } = getCommonSampleInfo(value, tables);
|
||||||
const payload = await loadWavetableFrames(url, label, frameLen);
|
const payload = await getPayload(url, label, frameLen);
|
||||||
const holdEnd = t + duration;
|
let holdEnd = t + duration;
|
||||||
|
if (clip !== undefined) {
|
||||||
|
holdEnd = Math.min(t + clip * duration, holdEnd);
|
||||||
|
}
|
||||||
const endWithRelease = holdEnd + release;
|
const endWithRelease = holdEnd + release;
|
||||||
const envEnd = endWithRelease + 0.01;
|
const envEnd = endWithRelease + 0.01;
|
||||||
const source = getWorklet(
|
const source = getWorklet(
|
||||||
@@ -242,17 +231,17 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
begin: t,
|
begin: t,
|
||||||
end: envEnd,
|
end: envEnd,
|
||||||
frequency,
|
frequency,
|
||||||
detune: value.detune,
|
freqspread: value.detune,
|
||||||
position: value.wt,
|
position: value.wt,
|
||||||
warp: value.warp,
|
warp: value.warp,
|
||||||
warpMode: warpmode,
|
warpMode: warpmode,
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
spread: value.spread,
|
panspread: value.spread,
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
},
|
},
|
||||||
{ outputChannelCount: [2] },
|
{ outputChannelCount: [2] },
|
||||||
);
|
);
|
||||||
source.port.postMessage({ type: 'tables', payload });
|
source.port.postMessage({ type: 'table', payload });
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
return;
|
return;
|
||||||
@@ -320,17 +309,18 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
|
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||||
const envGain = ac.createGain();
|
const envGain = ac.createGain();
|
||||||
const node = source.connect(envGain);
|
const node = source.connect(envGain);
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||||
const handle = { node, source };
|
const handle = { node, source };
|
||||||
const timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
source.disconnect();
|
|
||||||
destroyAudioWorkletNode(source);
|
destroyAudioWorkletNode(source);
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
|
fm?.stop();
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
wtPosModulators?.disconnect();
|
wtPosModulators?.disconnect();
|
||||||
wtWarpModulators?.disconnect();
|
wtWarpModulators?.disconnect();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
@@ -137,7 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
const begin = parameters['begin'][0];
|
||||||
if (currentTime >= parameters.end[0]) {
|
if (currentTime >= parameters.end[0]) {
|
||||||
return false;
|
return false;
|
||||||
@@ -430,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor({ processorOptions }) {
|
||||||
super();
|
super();
|
||||||
this.started = false;
|
this.started = false;
|
||||||
|
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(inputs, outputs, parameters) {
|
||||||
@@ -444,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.started = hasInput;
|
this.started = hasInput;
|
||||||
|
|
||||||
const shape = Math.expm1(parameters.distort[0]);
|
|
||||||
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
|
|
||||||
|
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||||
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
|
const shape = Math.expm1(pv(parameters.distort, n));
|
||||||
|
for (let ch = 0; ch < input.length; ch++) {
|
||||||
|
const x = input[ch][n];
|
||||||
|
output[ch][n] = postgain * this.algorithm(x, shape);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -510,7 +511,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(input, outputs, params) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime <= params.begin[0]) {
|
if (currentTime <= params.begin[0]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1043,49 +1044,60 @@ function brownian(x, oct = 4) {
|
|||||||
return (sum / norm) * 2 - 1;
|
return (sum / norm) * 2 - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tablesCache = {};
|
||||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||||
{ name: 'detune', defaultValue: 0.18 },
|
{ name: 'detune', defaultValue: 0 },
|
||||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
|
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
{ name: 'voices', defaultValue: 1, min: 1 },
|
||||||
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
|
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.tables = null;
|
|
||||||
this.frameLen = 0;
|
this.frameLen = 0;
|
||||||
this.numFrames = 0;
|
this.numFrames = 0;
|
||||||
this.phase = [];
|
this.phase = [];
|
||||||
this.syncRatio = 1;
|
this.invSR = 1 / sampleRate;
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'tables') {
|
if (type === 'table') {
|
||||||
this.tables = payload.mipmaps;
|
const key = payload.key;
|
||||||
this.frameLen = payload.frameLen;
|
this.frameLen = payload.frameLen;
|
||||||
|
if (!tablesCache[key]) {
|
||||||
|
const tables = [payload.frames];
|
||||||
|
let table = tables[0];
|
||||||
|
for (let level = 1; level < 1; level++) {
|
||||||
|
const nextLen = table.length >> 1;
|
||||||
|
const nextTable = table.map((frame) => {
|
||||||
|
const avg = new Float32Array(nextLen);
|
||||||
|
for (let i = 0; i < nextLen; i++) {
|
||||||
|
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
||||||
|
}
|
||||||
|
return avg;
|
||||||
|
});
|
||||||
|
tables.push(nextTable);
|
||||||
|
table = nextTable;
|
||||||
|
if (nextLen <= 32) break;
|
||||||
|
}
|
||||||
|
tablesCache[key] = tables;
|
||||||
|
}
|
||||||
|
this.tables = tablesCache[key];
|
||||||
this.numFrames = this.tables[0].length;
|
this.numFrames = this.tables[0].length;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
|
||||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
|
||||||
let level = 0;
|
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
|
||||||
level++;
|
|
||||||
}
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
_mirror(x) {
|
_mirror(x) {
|
||||||
return 1 - Math.abs(2 * x - 1);
|
return 1 - Math.abs(2 * x - 1);
|
||||||
}
|
}
|
||||||
@@ -1222,14 +1234,27 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sampleFrame(frame, phase) {
|
_sampleFrame(frame, phase) {
|
||||||
const pos = phase * frame.length;
|
const len = frame.length;
|
||||||
const i = pos | 0;
|
const pos = phase * len;
|
||||||
|
let i = pos | 0;
|
||||||
|
if (i >= len) i = 0; // fast wrap
|
||||||
const frac = pos - i;
|
const frac = pos - i;
|
||||||
const a = frame[i % frame.length];
|
const a = frame[i];
|
||||||
const b = frame[(i + 1) % frame.length];
|
let i1 = i + 1;
|
||||||
|
if (i1 >= len) i1 = 0;
|
||||||
|
const b = frame[i1];
|
||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_chooseMip(dphi) {
|
||||||
|
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||||
|
let level = 0;
|
||||||
|
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
if (currentTime >= parameters.end[0]) {
|
if (currentTime >= parameters.end[0]) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1239,29 +1264,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
const outL = outputs[0][0];
|
const outL = outputs[0][0];
|
||||||
const outR = outputs[0][1] || outputs[0][0];
|
const outR = outputs[0][1] || outputs[0][0];
|
||||||
|
|
||||||
if (!this.tables) {
|
if (!this.tables) {
|
||||||
outL.fill(0);
|
outL.fill(0);
|
||||||
if (outR !== outL) outR.set(outL);
|
if (outR !== outL) outR.set(outL);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const detune = pv(parameters.detune, i);
|
||||||
const tablePos = pv(parameters.position, i);
|
const freqspread = pv(parameters.freqspread, i);
|
||||||
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
const frac = idx - fIdx;
|
const frac = idx - fIdx;
|
||||||
const warpAmount = pv(parameters.warp, i);
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
const voices = pv(parameters.voices, i);
|
const voices = pv(parameters.voices, i);
|
||||||
const phaseRand = pv(parameters.phaserand, i);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
const spread = voices > 1 ? pv(parameters.spread, i) : 0;
|
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
|
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
let f = pv(parameters.frequency, i);
|
let f = pv(parameters.frequency, i);
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
const normalizer = 0.3 / Math.sqrt(voices);
|
const normalizer = 1 / Math.sqrt(voices);
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let n = 0; n < voices; n++) {
|
||||||
const isOdd = (n & 1) == 1;
|
const isOdd = (n & 1) == 1;
|
||||||
let gainL = gain1;
|
let gainL = gain1;
|
||||||
@@ -1271,8 +1295,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
gainL = gain2;
|
gainL = gain2;
|
||||||
gainR = gain1;
|
gainR = gain1;
|
||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
||||||
const dPhase = fVoice / sampleRate;
|
const dPhase = fVoice * this.invSR;
|
||||||
const level = this._chooseMip(dPhase);
|
const level = this._chooseMip(dPhase);
|
||||||
const table = this.tables[level];
|
const table = this.tables[level];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//import { ZZFX } from 'zzfx';
|
//import { ZZFX } from 'zzfx';
|
||||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
import { registerSound } from './superdough.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||||
|
|||||||
@@ -2829,6 +2829,82 @@ exports[`runs examples > example "distort" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "distort" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "distorttype" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "distorttype" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||||
|
"[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||||
|
"[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||||
|
"[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||||
|
"[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||||
|
"[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||||
|
"[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||||
|
"[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "distortvol" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "djf" example index 0 1`] = `
|
exports[`runs examples > example "djf" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
||||||
|
|||||||
@@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
|||||||
.room(0.5)`}
|
.room(0.5)`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
## Write your own chained function
|
||||||
|
|
||||||
|
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const effectChain = register('effectChain', (pat) => pat
|
||||||
|
.s("sawtooth")
|
||||||
|
.cutoff(500)
|
||||||
|
//.delay(0.5)
|
||||||
|
.room(0.5)
|
||||||
|
)
|
||||||
|
note("a3 c#4 e4 a4").effectChain()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Try adding `.rev()` after `effectChain()` to hear further effects added.
|
||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
|
|
||||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
|||||||
|
|
||||||
### scrub
|
### scrub
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
<JsDoc client:idle name="Pattern.scrub" h={0} />
|
||||||
|
|
||||||
### speed
|
### speed
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro
|
|||||||
|
|
||||||
This Guide shows you the different ways to get started with using Strudel in your own project.
|
This Guide shows you the different ways to get started with using Strudel in your own project.
|
||||||
|
|
||||||
|
## Respect the license
|
||||||
|
|
||||||
|
First, please take a moment to understand Strudel's free/open source license,
|
||||||
|
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||||
|
|
||||||
|
Here is a lay summary, but check the license for legal definitions and responsibilities.
|
||||||
|
|
||||||
|
- You can distribute modified versions if you keep track of the changes and the date you made them.
|
||||||
|
- You must license derivative work under the same license.
|
||||||
|
- Source code must be distributed along with web publication.
|
||||||
|
|
||||||
|
Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license.
|
||||||
|
|
||||||
|
This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||||
|
|
||||||
## Embedding the Strudel REPL
|
## Embedding the Strudel REPL
|
||||||
|
|
||||||
There are 3 quick ways to embed strudel in your website:
|
There are 3 quick ways to embed strudel in your website:
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ export function Reference() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowCaseSearch = search.toLowerCase();
|
const lowerCaseSearch = search.toLowerCase();
|
||||||
return (
|
return (
|
||||||
entry.name.toLowerCase().includes(lowCaseSearch) ||
|
entry.name.toLowerCase().includes(lowerCaseSearch) ||
|
||||||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
|
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|||||||
Reference in New Issue
Block a user