mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0ce82e3cd | |||
| e82629de2e | |||
| 8a91549a7b | |||
| cc27d482cd | |||
| 37e640470b | |||
| 5e5e7730c1 | |||
| 89e7d52308 | |||
| 99bb227cf4 | |||
| 70e776e799 | |||
| 66aad0004a | |||
| a968545458 | |||
| 5414bbe85d | |||
| e7e80bfd83 | |||
| c57f5bb429 | |||
| f2608e712b | |||
| 34b41fc26f | |||
| f6e171fed8 | |||
| 2511dcc09e | |||
| 977420e74d | |||
| 2fbdc9fdd7 | |||
| 0a96fa5896 | |||
| 3e0e903d38 | |||
| 591c3fe08f | |||
| 06ebcbff8c | |||
| 142160d79a | |||
| ff2c59a2a6 | |||
| 9b9176325b | |||
| e84c39837f | |||
| 6527c68cda | |||
| 6a09f54b25 | |||
| dc6b766ab7 | |||
| 9eb3c9410d | |||
| 189e650a73 | |||
| a5886bb9d4 | |||
| 877bc95a58 | |||
| 18b1739b89 | |||
| b54139376e | |||
| d64a0ef0eb | |||
| 659071a4ee | |||
| 64f7bc4442 | |||
| 8c97f2e2d0 | |||
| b8c98736c5 | |||
| a0fc52b1ec | |||
| 0693a32736 | |||
| 42a903ecc0 | |||
| da283eb55a | |||
| b12707316a | |||
| 66aa3ac1da | |||
| d81b1ab65c | |||
| e490774294 | |||
| 34e8a57472 | |||
| 59c8d70714 | |||
| ce7cff2c3b | |||
| 32fe73aba2 | |||
| cf55d4c8d1 | |||
| e7e636886d | |||
| e9aef9e4d4 |
@@ -3,8 +3,6 @@
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
Development is moving to https://codeberg.org/uzu/strudel
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||
@@ -47,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
|
||||
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
||||
|
||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||
|
||||
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
|
||||
|
||||
@@ -56,11 +56,13 @@ const buildExamples = (examples) =>
|
||||
|
||||
export const Autocomplete = ({ doc, label }) =>
|
||||
h`
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
||||
${doc.description ? `<p class="autocomplete-info-function-description">${doc.description}</p>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
</div>
|
||||
</div>
|
||||
`[0];
|
||||
|
||||
@@ -98,4 +100,4 @@ export const strudelAutocomplete = (context) => {
|
||||
};
|
||||
|
||||
export const isAutoCompletionEnabled = (enabled) =>
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete] })] : [];
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus, sequence, stack } from '../index.mjs';
|
||||
import { calculateSteps, sequence, stack } from '../index.mjs';
|
||||
|
||||
const pat64 = sequence(...Array(64).keys());
|
||||
|
||||
describe('steps', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -14,7 +14,7 @@ describe('steps', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -25,7 +25,7 @@ describe('steps', () => {
|
||||
});
|
||||
|
||||
describe('stack', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -34,7 +34,7 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -43,4 +43,4 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
});
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
|
||||
@@ -1516,6 +1516,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
|
||||
*
|
||||
*/
|
||||
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
|
||||
|
||||
/**
|
||||
* Sets speed of the sample for the impulse response.
|
||||
* @name irspeed
|
||||
* @param {string | Pattern} speed
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
|
||||
*
|
||||
*/
|
||||
export const { irspeed } = registerControl('irspeed');
|
||||
|
||||
/**
|
||||
* Sets the beginning of the IR response sample
|
||||
* @name irbegin
|
||||
* @param {string | Pattern} begin between 0 and 1
|
||||
* @synonyms ir
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
|
||||
*
|
||||
*/
|
||||
export const { irbegin } = registerControl('irbegin');
|
||||
/**
|
||||
* Sets the room size of the reverb, see `room`.
|
||||
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -3277,7 +3277,7 @@ export const slice = register(
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||
*/
|
||||
Pattern.prototype.onTriggerTime = function (func) {
|
||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus } from '../../core/index.mjs';
|
||||
import { calculateSteps } from '../../core/index.mjs';
|
||||
import { mini } from '../index.mjs';
|
||||
|
||||
describe('mini', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -13,7 +13,7 @@ describe('mini', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -21,5 +21,5 @@ describe('mini', () => {
|
||||
},
|
||||
{ time: 1000 },
|
||||
);
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||
import { oscTrigger } from './osc.mjs';
|
||||
|
||||
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
||||
|
||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||
const currentTime = performance.now() / 1000;
|
||||
return trigger(null, hap, currentTime, cps, targetTime);
|
||||
return trigger(hap, currentTime, cps, targetTime);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -20,3 +20,13 @@ samples('http://localhost:5432')
|
||||
LOG=1 npx @strudel/sampler # adds logging
|
||||
PORT=5555 npx @strudel/sampler # changes port
|
||||
```
|
||||
|
||||
## static json
|
||||
|
||||
when running with `--json`, you will simply get the json logged back:
|
||||
|
||||
```sh
|
||||
npx --yes @strudel/sampler --json > strudel.json
|
||||
```
|
||||
|
||||
this is useful if you want to create a sample pack from the current folder.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cowsay from 'cowsay';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { createReadStream, existsSync, writeFileSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import http from 'http';
|
||||
import { join, sep } from 'path';
|
||||
import { join, resolve, sep } from 'path';
|
||||
import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
// eslint-disable-next-line
|
||||
const LOG = !!process.env.LOG || false;
|
||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||
|
||||
console.log(
|
||||
cowsay.say({
|
||||
text: 'welcome to @strudel/sampler',
|
||||
e: 'oO',
|
||||
T: 'U ',
|
||||
}),
|
||||
);
|
||||
const isAudioFile = (f) => {
|
||||
const ext = f.split('.').slice(-1)[0].toLowerCase();
|
||||
return VALID_AUDIO_EXTENSIONS.includes(ext);
|
||||
};
|
||||
|
||||
async function getFilesInDirectory(directory) {
|
||||
let files = [];
|
||||
@@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
||||
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
|
||||
);
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
|
||||
files = files.concat(subFiles);
|
||||
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
||||
} catch (err) {
|
||||
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
||||
}
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
isAudioFile(fullPath) && files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function getBanks(directory) {
|
||||
async function getBanks(directory, flat = false) {
|
||||
let files = await getFilesInDirectory(directory);
|
||||
let banks = {};
|
||||
directory = directory.split(sep).join('/');
|
||||
files = files.map((path) => {
|
||||
path = path.split(sep).join('/');
|
||||
const [bank] = path.split('/').slice(-2);
|
||||
const subDir = path.replace(directory, '');
|
||||
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
|
||||
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
|
||||
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
|
||||
banks[bank] = banks[bank] || [];
|
||||
const relativeUrl = path.replace(directory, '');
|
||||
banks[bank].push(relativeUrl);
|
||||
return relativeUrl;
|
||||
banks[bank].push(subDir);
|
||||
return subDir;
|
||||
});
|
||||
banks._base = `http://localhost:5432`;
|
||||
return { banks, files };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const directory = process.cwd();
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArgValue(flag) {
|
||||
const i = args.indexOf(flag);
|
||||
if (i !== -1) {
|
||||
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||
if (nextIsFlag) return true;
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
function getInput(query) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(query, (response) => {
|
||||
rl.close();
|
||||
resolve(response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let directory = getArgValue('--dir') || process.cwd();
|
||||
directory = resolve(directory);
|
||||
if (args.includes('--json')) {
|
||||
const { banks } = await getBanks(directory, getArgValue('--flat'));
|
||||
const json = JSON.stringify(banks);
|
||||
const outFile = resolve(directory, 'strudel.json');
|
||||
if (existsSync(outFile)) {
|
||||
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
writeFileSync(outFile, json, 'utf8');
|
||||
console.log(`Wrote json to ${outFile}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
cowsay.say({
|
||||
text: 'welcome to @strudel/sampler',
|
||||
e: 'oO',
|
||||
T: 'U ',
|
||||
}),
|
||||
);
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
const { banks, files } = await getBanks(directory);
|
||||
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
|
||||
if (req.url === '/') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.end(JSON.stringify(banks));
|
||||
@@ -72,7 +118,7 @@ const server = http.createServer(async (req, res) => {
|
||||
let subpath = decodeURIComponent(req.url);
|
||||
const filePath = join(directory, subpath.split('/').join(sep));
|
||||
|
||||
//console.log('GET:', filePath);
|
||||
// console.log('GET:', filePath);
|
||||
const isFound = existsSync(filePath);
|
||||
if (!isFound) {
|
||||
res.statusCode = 404;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -211,11 +211,29 @@ export function getVibratoOscillator(param, value, t) {
|
||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||
const constantNode = new ConstantSourceNode(audioContext);
|
||||
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
// 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
|
||||
const zeroGain = gainNode(0);
|
||||
zeroGain.connect(audioContext.destination);
|
||||
constantNode.connect(zeroGain);
|
||||
|
||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||
constantNode.onended = () => {
|
||||
// Ensure garbage collection
|
||||
try {
|
||||
zeroGain.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
try {
|
||||
constantNode.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
onComplete();
|
||||
};
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
return constantNode;
|
||||
}
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import reverbGen from './reverbGen.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.adjustLength = function (duration, buffer) {
|
||||
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
||||
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
||||
const newLength = buffer.sampleRate * duration;
|
||||
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||
@@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') {
|
||||
let newData = newBuffer.getChannelData(channel);
|
||||
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
newData[i] = oldData[i] || 0;
|
||||
// loop the buffer around to prevent
|
||||
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
|
||||
if (speed < 1) {
|
||||
position = position * -1;
|
||||
}
|
||||
|
||||
newData[i] = oldData.at(position) || 0;
|
||||
}
|
||||
}
|
||||
return newBuffer;
|
||||
};
|
||||
|
||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
|
||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||
const convolver = this.createConvolver();
|
||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
|
||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
||||
convolver.duration = d;
|
||||
convolver.fade = fade;
|
||||
convolver.lp = lp;
|
||||
convolver.dim = dim;
|
||||
convolver.ir = ir;
|
||||
convolver.irspeed = irspeed;
|
||||
convolver.irbegin = irbegin;
|
||||
if (ir) {
|
||||
convolver.buffer = this.adjustLength(d, ir);
|
||||
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
|
||||
} else {
|
||||
reverbGen.generateReverb(
|
||||
{
|
||||
@@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') {
|
||||
);
|
||||
}
|
||||
};
|
||||
convolver.generate(duration, fade, lp, dim, ir);
|
||||
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
return convolver;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@ export async function initAudioOnFirstClick(options) {
|
||||
return audioReady;
|
||||
}
|
||||
|
||||
let delays = {};
|
||||
const maxfeedback = 0.98;
|
||||
|
||||
let channelMerger, destinationGain;
|
||||
@@ -326,7 +325,7 @@ export const panic = () => {
|
||||
channelMerger == null;
|
||||
};
|
||||
|
||||
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (delayfeedback > maxfeedback) {
|
||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
}
|
||||
@@ -414,7 +413,7 @@ function connectToOrbit(node, orbit) {
|
||||
function setOrbit(audioContext, orbit, channels) {
|
||||
if (orbits[orbit] == null) {
|
||||
orbits[orbit] = {
|
||||
gain: new GainNode(audioContext, { gain: 1 }),
|
||||
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
|
||||
};
|
||||
connectToDestination(orbits[orbit].gain, channels);
|
||||
}
|
||||
@@ -442,19 +441,22 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1
|
||||
}
|
||||
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||
// If no reverb has been created for a given orbit, create one
|
||||
if (!orbits[orbit].reverbNode) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
connectToOrbit(reverb, orbit);
|
||||
orbits[orbit].reverbNode = reverb;
|
||||
}
|
||||
|
||||
if (
|
||||
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
|
||||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
|
||||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
|
||||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
|
||||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
|
||||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
|
||||
orbits[orbit].reverbNode.ir !== ir
|
||||
) {
|
||||
// only regenerate when something has changed
|
||||
@@ -462,7 +464,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||
// stack(s("a"), s("b").rsize(8)).room(.5)
|
||||
// this only works when args may stay undefined until here
|
||||
// setting default values breaks this
|
||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
|
||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
}
|
||||
return orbits[orbit].reverbNode;
|
||||
}
|
||||
@@ -619,6 +621,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
roomdim,
|
||||
roomsize,
|
||||
ir,
|
||||
irspeed,
|
||||
irbegin,
|
||||
i = getDefaultValue('i'),
|
||||
velocity = getDefaultValue('velocity'),
|
||||
analyze, // analyser wet
|
||||
@@ -832,7 +836,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delayNode, delay);
|
||||
audioNodes.push(delaySend);
|
||||
}
|
||||
@@ -850,7 +854,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||
}
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
audioNodes.push(reverbSend);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -4875,6 +4875,43 @@ exports[`runs examples > example "irand" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "irbegin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "iresponse" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]",
|
||||
@@ -4896,6 +4933,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "irspeed" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "isaw" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
|
||||
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
|
||||
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
|
||||
|
||||
# Signal chain
|
||||
|
||||
<img src="/img/strudel-signal-flow.png"></img>
|
||||
|
||||
The signal chain in Strudel is as follows:
|
||||
|
||||
- An sound-generating event is triggered by a pattern
|
||||
- This has a start time and a duration, which is usually
|
||||
controlled by the note length and ADSR parameters
|
||||
- If we exceed the max polyphony, old sounds begin to die off
|
||||
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
|
||||
- A sound is produced (through, say, a sample or an oscillator)
|
||||
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
|
||||
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
|
||||
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
|
||||
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
|
||||
again)
|
||||
- Phase vocoder (`stretch`)
|
||||
- Gain is applied (`gain`)
|
||||
- This is where the main (volume) ADSR happens
|
||||
- A lowpass filter (`lpf`)
|
||||
- A highpass filter (`hpf`)
|
||||
- A bandpass filter (`bandpass`)
|
||||
- A vowel filter (`vowel`)
|
||||
- Sample rate reduction (`coarse`)
|
||||
- Bit crushing (`crush`)
|
||||
- Waveshape distortion (`shape`)
|
||||
- Normal distortion (`distort`)
|
||||
- Tremolo (`tremolo`)
|
||||
- Compressor (`compressor`)
|
||||
- Panning (`pan`)
|
||||
- Phaser (`phaser`)
|
||||
- Postgain (`post`)
|
||||
- The sound is then split into multiple destinations
|
||||
- Dry output (amount controlled by `dry` parameter)
|
||||
- The sends
|
||||
- Analyzers
|
||||
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
|
||||
- Delay (amount controlled by `delay` parameter)
|
||||
- Reverb (amount controlled by `room` parameter)
|
||||
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
|
||||
- The `duck` effect affects the volume of all signals in the orbit
|
||||
- The orbit is then sent to the mixer
|
||||
|
||||
## Orbits
|
||||
|
||||
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
|
||||
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
|
||||
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
|
||||
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
|
||||
|
||||
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
|
||||
|
||||
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
|
||||
|
||||
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
|
||||
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
|
||||
|
||||
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
|
||||
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
|
||||
with a large reverb:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10)`}
|
||||
/>
|
||||
|
||||
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10)
|
||||
|
||||
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||
/>
|
||||
|
||||
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
|
||||
distinct:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10).orbit(2)
|
||||
|
||||
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||
/>
|
||||
|
||||
## Continuous changes
|
||||
|
||||
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
|
||||
modified continuously in time. For example,
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
|
||||
/>
|
||||
|
||||
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
|
||||
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
|
||||
/>
|
||||
|
||||
Some parameters _do_ induce continuous variations in time, though:
|
||||
|
||||
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
|
||||
- The pitch envelope curve (governed by `penv` and its associated ADSR)
|
||||
- The FM curve (`fmenv`)
|
||||
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
|
||||
- Tremolo (`tremolo`)
|
||||
- Phaser (`phaser`)
|
||||
- Vibrato (`vib`)
|
||||
- Ducking (`duckorbit`)
|
||||
|
||||
# Filters
|
||||
|
||||
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
|
||||
|
||||
@@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
## Randomness
|
||||
|
||||
Events with a "?" placed after them will have a 50% chance of being removed from the pattern:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?")`} punchcard />
|
||||
|
||||
Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?0.1")`} punchcard />
|
||||
|
||||
Events separated by a "|" will be chosen from at random:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4] | [a3,c3,e4] | [b3,d3,f#4]")`} punchcard />
|
||||
|
||||
## Mini-notation review
|
||||
|
||||
To recap what we've learned so far, compare the following patterns:
|
||||
@@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns:
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3|b3|e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
|
||||
## Euclidian rhythms
|
||||
|
||||
|
||||
@@ -178,6 +178,16 @@ the version number).
|
||||
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
|
||||
or from the dev console if you're technically minded, or by using a cache deleting extension).
|
||||
|
||||
## Generating strudel.json
|
||||
|
||||
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
|
||||
|
||||
```sh
|
||||
npx --yes @strudel/sampler --json > strudel.json
|
||||
```
|
||||
|
||||
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
|
||||
|
||||
## Github Shortcut
|
||||
|
||||
Because loading samples from github is common, there is a shortcut:
|
||||
|
||||
@@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl';
|
||||
|
||||
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
|
||||
|
||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
||||
|
||||
[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.
|
||||
|
||||
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
|
||||
|
||||
|
||||
@@ -72,21 +72,19 @@
|
||||
|
||||
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
|
||||
.cm-tooltip.cm-completionInfo {
|
||||
padding: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid var(--foreground) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
max-width: 500px !important;
|
||||
min-width: 300px !important;
|
||||
max-height: 400px !important;
|
||||
white-space: normal !important;
|
||||
overflow: auto !important;
|
||||
background-color: var(--lineHighlight) !important;
|
||||
}
|
||||
|
||||
/* Main tooltip container */
|
||||
.autocomplete-info-tooltip {
|
||||
.autocomplete-info-container {
|
||||
padding: 12px !important;
|
||||
border-radius: 4px !important;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
|
||||
@@ -95,6 +93,16 @@
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
min-width: 400px;
|
||||
white-space: normal !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-tooltip {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-name {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
|
||||
return (
|
||||
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import { useMemo } from 'react';
|
||||
import { getMetadata } from '../../../metadata_parser.js';
|
||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
@@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
@@ -311,7 +311,8 @@ export function SettingsTab({ started }) {
|
||||
onClick={() => {
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
settingsMap.set(defaultSettings);
|
||||
const { userPatterns } = settingsMap.get(); // keep current patterns
|
||||
settingsMap.set({ ...defaultSettings, userPatterns });
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { confirmDialog } from '@src/repl/util.mjs';
|
||||
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
|
||||
import { prebake } from '@src/repl/prebake.mjs';
|
||||
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
|
||||
export function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
|
||||
const { soundsFilter } = useSettings();
|
||||
const [search, setSearch] = useState('');
|
||||
const { BASE_URL } = import.meta.env;
|
||||
@@ -27,18 +32,19 @@ export function SoundsTab() {
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
if (soundsFilter === 'user') {
|
||||
if (soundsFilter === soundFilterType.USER) {
|
||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
if (soundsFilter === soundFilterType.DRUMS) {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'samples') {
|
||||
if (soundsFilter === soundFilterType.SAMPLES) {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'synths') {
|
||||
if (soundsFilter === soundFilterType.SYNTHS) {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
//TODO: tidy this up, it does not need to be saved in settings
|
||||
if (soundsFilter === 'importSounds') {
|
||||
return [];
|
||||
}
|
||||
@@ -57,10 +63,10 @@ export function SoundsTab() {
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
|
||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||
<div className=" flex shrink-0 flex-wrap">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
@@ -74,7 +80,26 @@ export function SoundsTab() {
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && (
|
||||
<ActionButton
|
||||
className="pl-2"
|
||||
label="delete-all"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const confirmed = await confirmDialog('Delete all imported user samples?');
|
||||
if (confirmed) {
|
||||
clearIDB(userSamplesDBConfig.dbName);
|
||||
soundMap.set({});
|
||||
await prebake();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
@@ -151,9 +176,7 @@ export function SoundsTab() {
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
||||
? 'No custom sounds loaded in this pattern (yet).'
|
||||
: ''}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,17 +12,21 @@ export const userSamplesDBConfig = {
|
||||
};
|
||||
|
||||
// deletes all of the databases, useful for debugging
|
||||
function clearIDB() {
|
||||
function clearAllIDB() {
|
||||
window.indexedDB
|
||||
.databases()
|
||||
.then((r) => {
|
||||
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
|
||||
for (var i = 0; i < r.length; i++) clearIDB(r[i].name);
|
||||
})
|
||||
.then(() => {
|
||||
alert('All data cleared.');
|
||||
});
|
||||
}
|
||||
|
||||
export function clearIDB(dbName) {
|
||||
return window.indexedDB.deleteDatabase(dbName);
|
||||
}
|
||||
|
||||
// queries the DB, and registers the sounds so they can be played
|
||||
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
|
||||
openDB(config, (objectStore) => {
|
||||
|
||||
@@ -8,6 +8,14 @@ export const audioEngineTargets = {
|
||||
osc: 'osc',
|
||||
};
|
||||
|
||||
export const soundFilterType = {
|
||||
USER: 'user',
|
||||
DRUMS: 'drums',
|
||||
SAMPLES: 'samples',
|
||||
SYNTHS: 'synths',
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -28,7 +36,7 @@ export const defaultSettings = {
|
||||
fontSize: 18,
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
soundsFilter: soundFilterType.ALL,
|
||||
patternFilter: 'community',
|
||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||
panelPosition: 'right',
|
||||
|
||||
Reference in New Issue
Block a user