Compare commits

..

1 Commits

Author SHA1 Message Date
方程 e477268417 docs(workshop): Update the recap of Mini-Notation and audio effects 2025-04-23 18:10:08 +08:00
68 changed files with 366 additions and 790 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
+1 -2
View File
@@ -71,7 +71,6 @@
"jsdoc-json": "^2.0.2",
"lerna": "^8.1.9",
"prettier": "^3.4.2",
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
"vitest": "^3.0.4"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.1",
"version": "1.1.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Cutie Pi
/**
* @name Cutie Pi
* by Switch Angel
*/
import { tags as t } from '@lezer/highlight';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* androidstudio
/**
* @name androidstudio
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Atom One
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
+3 -2
View File
@@ -1,5 +1,6 @@
/*
* darcula
/**
* @name darcula
* @author darcula
* Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains
*/
+2 -1
View File
@@ -1,5 +1,6 @@
/*
/**
* @name dracula
* @author dracula
* Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/
+3 -3
View File
@@ -1,6 +1,6 @@
/*
* duotone
* author Bram de Haan
/**
* @name duotone
* @author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
+3 -3
View File
@@ -1,6 +1,6 @@
/*
* duotone
* author Bram de Haan
/**
* @name duotone
* @author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
+3
View File
@@ -1,3 +1,6 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
+3
View File
@@ -1,3 +1,6 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Atom One
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
+4 -3
View File
@@ -1,6 +1,7 @@
/*
* gruvbox-dark
* author morhetz
/**
* @name gruvbox-dark
* @author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/
import { tags as t } from '@lezer/highlight';
+4 -3
View File
@@ -1,6 +1,7 @@
/*
* gruvbox-light
* author morhetz
/**
* @name gruvbox-light
* @author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/
import { tags as t } from '@lezer/highlight';
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Atom One
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Atom One
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
+1 -36
View File
@@ -458,41 +458,6 @@ export const { drive } = registerControl('drive');
*/
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
* @example
* note("{f a c e}%16").s("pulse").pw(".8:1:.2")
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0 .75 .5 1")
*/
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwrate } = registerControl('pwrate');
/**
* controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwsweep } = registerControl('pwsweep');
/**
* Phaser audio effect that approximates popular guitar pedals.
*
@@ -1496,7 +1461,7 @@ export const { vowel } = registerControl('vowel');
* @name waveloss
*/
export const { waveloss } = registerControl('waveloss');
/**
/*
* Noise crackle density
*
* @name density
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.1",
"version": "1.1.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+2 -1
View File
@@ -156,7 +156,8 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
+2 -2
View File
@@ -279,9 +279,9 @@ const _rearrangeWith = (ipat, n, pat) => {
};
/**
* @name shuffle
* Slices a pattern into the given number of parts, then plays those parts in random order.
* Each part will be played exactly once per cycle.
* @name shuffle
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
@@ -292,9 +292,9 @@ export const shuffle = register('shuffle', (n, pat) => {
});
/**
* @name scramble
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
* but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.2",
"version": "1.1.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.1",
"version": "1.1.0",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+36 -35
View File
@@ -1,37 +1,38 @@
{
"name": "@strudel/gamepad",
"version": "1.2.1",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
}
"name": "@strudel/gamepad",
"version": "1.1.0",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
{
"name": "hs2js",
"version": "0.2.0",
"private": true,
"version": "0.1.0",
"description": "Experimental Haskell in JavaScript interpreter",
"main": "src/index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.1",
"version": "1.1.0",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.2",
"version": "1.1.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.1",
"version": "1.1.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+35 -34
View File
@@ -1,37 +1,38 @@
{
"name": "@strudel/motion",
"version": "1.2.1",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"name": "@strudel/motion",
"version": "1.1.0",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.1",
"version": "1.1.0",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.1",
"version": "1.1.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.2.0",
"version": "1.1.0",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
@@ -31,6 +31,8 @@
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
},
"devDependencies": {
"vite": "^6.0.11"
}
+4
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
@@ -10,6 +11,9 @@ export default defineConfig({
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.2",
"version": "1.1.0",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
@@ -46,7 +46,6 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
"vite": "^6.0.11"
}
}
+1 -2
View File
@@ -1,12 +1,11 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [bundleAudioWorkletPlugin()],
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.0",
"version": "0.1.0",
"description": "",
"keywords": [
"tidalcycles",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.1",
"version": "1.1.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.2",
"version": "1.1.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.2",
"version": "1.1.0",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
@@ -32,8 +32,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
"vite": "^6.0.11"
},
"dependencies": {
"nanostores": "^0.11.3"
+25 -60
View File
@@ -8,7 +8,7 @@ import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import workletsUrl from './worklets.mjs?worker&url';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
@@ -18,16 +18,9 @@ export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
}
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -35,16 +28,6 @@ export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
let gainCurveFunc = (val) => val;
export function applyGainCurve(val) {
return gainCurveFunc(val);
}
export function setGainCurve(newGainCurveFunc) {
gainCurveFunc = newGainCurveFunc;
}
function aliasBankMap(aliasMap) {
// Make all bank keys lower case for case insensitivity
for (const key in aliasMap) {
@@ -201,15 +184,8 @@ function loadWorklets() {
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
disableWorklets = false,
maxPolyphony,
audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME,
multiChannelOrbits = false,
} = options;
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') {
return;
}
@@ -290,7 +266,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
});
};
@@ -303,7 +279,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`);
}
@@ -312,7 +288,7 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, channels);
connectToDestination(dly, [0, 1]);
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
@@ -320,9 +296,16 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
return delays[orbit];
}
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
//gain
const ac = getAudioContext();
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep * 2;
// centerFrequency = centerFrequency * 2;
// sweep = sweep * 1.5;
const lfo = getWorklet(ac, 'lfo-processor', {
frequency,
depth: 1,
skew: 0,
phaseoffset: 0,
@@ -330,13 +313,8 @@ export function getLfo(audioContext, time, end, properties = {}) {
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
lfo.connect(lfoGain);
//filters
const numStages = 2; //num of filters in series
@@ -367,12 +345,12 @@ function getFilterType(ftype) {
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
function getReverb(orbit, duration, fade, lp, dim, ir) {
// If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, channels);
connectToDestination(reverb, [0, 1]);
reverbs[orbit] = reverb;
}
if (
@@ -439,11 +417,6 @@ export function resetGlobalEffects() {
}
let activeSoundSources = new Map();
//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0
function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext();
@@ -506,7 +479,7 @@ export const superdough = async (value, t, hapDuration) => {
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser
phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'),
@@ -542,18 +515,7 @@ export const superdough = async (value, t, hapDuration) => {
compressorRelease,
} = value;
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
gain = nanFallback(gain, 1);
const chainID = Math.round(Math.random() * 1000000);
@@ -568,6 +530,9 @@ export const superdough = async (value, t, hapDuration) => {
activeSoundSources.delete(chainID);
}
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
let audioNodes = [];
if (bank && s) {
@@ -717,7 +682,7 @@ export const superdough = async (value, t, hapDuration) => {
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend);
}
@@ -735,7 +700,7 @@ export const superdough = async (value, t, hapDuration) => {
}
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);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
+5 -24
View File
@@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import {
applyFM,
gainNode,
@@ -26,9 +26,6 @@ const getFrequencyFromValue = (value) => {
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
@@ -148,20 +145,7 @@ export function registerSynthSounds() {
'pulse',
(begin, value, onended) => {
const ac = getAudioContext();
let { pwrate, pwsweep } = value;
if (pwsweep == null) {
if (pwrate != null) {
pwsweep = 0.3;
} else {
pwsweep = 0;
}
}
if (pwrate == null && pwsweep != null) {
pwrate = 1;
}
let { duration, pw: pulsewidth = 0.5 } = value;
let { duration, n: pulsewidth = 0.5 } = value;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
@@ -169,8 +153,10 @@ export function registerSynthSounds() {
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'pulse-oscillator',
@@ -192,16 +178,11 @@ export function registerSynthSounds() {
envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let lfo;
if (pwsweep != 0) {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect();
onended();
fm?.stop();
+1 -2
View File
@@ -1,11 +1,10 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
+1 -2
View File
@@ -1,7 +1,6 @@
{
"name": "@strudel/tidal",
"version": "0.2.0",
"private": true,
"version": "0.1.0",
"description": "Experimental Tidal Code interpreter",
"module": "tidal.mjs",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.1",
"version": "1.1.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.1",
"version": "1.1.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
@@ -1,16 +0,0 @@
{
"name": "vite-plugin-bundle-audioworklet",
"main": "./vite-plugin-bundle-audioworklet.js",
"version": "0.1.1",
"description": "",
"keywords": [
"vite",
"audioworklet"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "MIT",
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
}
}
@@ -1,48 +0,0 @@
import { createLogger, build } from 'vite';
const end = '?audioworklet';
function bundleAudioWorkletPlugin() /* : PluginOption */ {
let viteConfig /* : UserConfig */;
return {
name: 'vite-plugin-bundle-audioworklet',
/* apply: 'build', */
enforce: 'post',
config(config) {
viteConfig = config;
},
async transform(_code, id) {
if (!id.endsWith(end)) {
return;
}
const entry = id.replace(end, '');
const quietLogger = createLogger();
quietLogger.info = () => undefined;
const output = await build({
configFile: false,
clearScreen: false,
customLogger: quietLogger,
build: {
lib: {
entry,
name: '_',
formats: ['iife'],
},
write: false,
},
});
if (!(output instanceof Array)) {
throw new Error('Expected output to be Array');
}
const iife = output[0].output[0].code;
const encoded = Buffer.from(iife, 'utf8').toString('base64');
return `export default "data:text/javascript;base64,${encoded}";`;
},
};
}
export default bundleAudioWorkletPlugin;
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.2",
"version": "1.1.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
@@ -41,7 +41,6 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
"vite": "^6.0.11"
}
}
+1 -2
View File
@@ -2,12 +2,11 @@ import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [bundleAudioWorkletPlugin()],
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.2",
"version": "1.1.0",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.1",
"version": "1.1.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+3 -23
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4)
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -75,9 +75,6 @@ importers:
prettier:
specifier: ^3.4.2
version: 3.4.2
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:packages/vite-plugin-bundle-audioworklet
vitest:
specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
@@ -431,9 +428,6 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/sampler:
dependencies:
@@ -482,9 +476,6 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/tidal:
dependencies:
@@ -549,12 +540,6 @@ importers:
specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/vite-plugin-bundle-audioworklet:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/web:
dependencies:
'@strudel/core':
@@ -579,9 +564,6 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/webaudio:
dependencies:
@@ -798,9 +780,6 @@ importers:
sharp:
specifier: ^0.33.5
version: 0.33.5
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../packages/vite-plugin-bundle-audioworklet
workbox-window:
specifier: ^7.3.0
version: 7.3.0
@@ -7582,6 +7561,7 @@ packages:
workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -10268,7 +10248,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
+6 -249
View File
@@ -2403,27 +2403,6 @@ exports[`runs examples > example "delaytime" example index 0 1`] = `
]
`;
exports[`runs examples > example "density" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
"[ 1/4 → 1/2 | s:crackle density:0.01 ]",
"[ 1/2 → 3/4 | s:crackle density:0.01 ]",
"[ 3/4 → 1/1 | s:crackle density:0.01 ]",
"[ 1/1 → 5/4 | s:crackle density:0.01 ]",
"[ 5/4 → 3/2 | s:crackle density:0.01 ]",
"[ 3/2 → 7/4 | s:crackle density:0.01 ]",
"[ 7/4 → 2/1 | s:crackle density:0.01 ]",
"[ 2/1 → 9/4 | s:crackle density:0.01 ]",
"[ 9/4 → 5/2 | s:crackle density:0.01 ]",
"[ 5/2 → 11/4 | s:crackle density:0.01 ]",
"[ 11/4 → 3/1 | s:crackle density:0.01 ]",
"[ 3/1 → 13/4 | s:crackle density:0.01 ]",
"[ 13/4 → 7/2 | s:crackle density:0.01 ]",
"[ 7/2 → 15/4 | s:crackle density:0.01 ]",
"[ 15/4 → 4/1 | s:crackle density:0.01 ]",
]
`;
exports[`runs examples > example "detune" example index 0 1`] = `
[
"[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]",
@@ -6742,186 +6721,6 @@ exports[`runs examples > example "pure" example index 0 1`] = `
]
`;
exports[`runs examples > example "pw" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/16 → 1/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/8 → 3/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/16 → 1/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/4 → 5/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/16 → 3/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/8 → 7/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/16 → 1/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/2 → 9/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/16 → 5/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/8 → 11/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/16 → 3/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/4 → 13/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/16 → 7/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/8 → 15/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/16 → 1/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/1 → 17/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 17/16 → 9/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/8 → 19/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 19/16 → 5/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/4 → 21/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 21/16 → 11/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/8 → 23/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 23/16 → 3/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/2 → 25/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 25/16 → 13/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/8 → 27/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 27/16 → 7/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/4 → 29/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 29/16 → 15/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/8 → 31/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 31/16 → 2/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 2/1 → 33/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 33/16 → 17/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 17/8 → 35/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 35/16 → 9/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/4 → 37/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 37/16 → 19/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 19/8 → 39/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 39/16 → 5/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/2 → 41/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 41/16 → 21/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 21/8 → 43/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 43/16 → 11/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/4 → 45/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 45/16 → 23/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 23/8 → 47/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 47/16 → 3/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/1 → 49/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 49/16 → 25/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 25/8 → 51/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 51/16 → 13/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/4 → 53/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 53/16 → 27/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 27/8 → 55/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 55/16 → 7/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/2 → 57/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 57/16 → 29/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 29/8 → 59/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 59/16 → 15/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/4 → 61/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 61/16 → 31/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 31/8 → 63/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 63/16 → 4/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
]
`;
exports[`runs examples > example "pw" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.75 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:1 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:1 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.75 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:1 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:1 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.75 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:1 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:1 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.75 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:1 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:1 ]",
]
`;
exports[`runs examples > example "pwrate" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
]
`;
exports[`runs examples > example "pwsweep" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
]
`;
exports[`runs examples > example "queryArc" example index 0 1`] = `[]`;
exports[`runs examples > example "rand" example index 0 1`] = `
@@ -8072,52 +7871,6 @@ but parts might be played more than once, or not at all, per cycle." example ind
]
`;
exports[`runs examples > example "scramble" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d s:piano ]",
"[ 1/2 → 3/4 | note:d s:piano ]",
"[ 3/4 → 1/1 | note:c s:piano ]",
"[ 1/1 → 5/4 | note:e s:piano ]",
"[ 5/4 → 3/2 | note:e s:piano ]",
"[ 3/2 → 7/4 | note:e s:piano ]",
"[ 7/4 → 2/1 | note:c s:piano ]",
"[ 2/1 → 9/4 | note:f s:piano ]",
"[ 9/4 → 5/2 | note:d s:piano ]",
"[ 5/2 → 11/4 | note:d s:piano ]",
"[ 11/4 → 3/1 | note:e s:piano ]",
"[ 3/1 → 13/4 | note:c s:piano ]",
"[ 13/4 → 7/2 | note:f s:piano ]",
"[ 7/2 → 15/4 | note:d s:piano ]",
"[ 15/4 → 4/1 | note:f s:piano ]",
]
`;
exports[`runs examples > example "scramble" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]",
"[ 1/4 → 3/8 | note:d s:piano ]",
"[ 3/8 → 1/2 | note:c s:piano ]",
"[ 1/2 → 1/1 | note:g s:piano ]",
"[ 1/1 → 9/8 | note:e s:piano ]",
"[ 9/8 → 5/4 | note:e s:piano ]",
"[ 5/4 → 11/8 | note:e s:piano ]",
"[ 11/8 → 3/2 | note:c s:piano ]",
"[ 3/2 → 2/1 | note:g s:piano ]",
"[ 2/1 → 17/8 | note:f s:piano ]",
"[ 17/8 → 9/4 | note:d s:piano ]",
"[ 9/4 → 19/8 | note:d s:piano ]",
"[ 19/8 → 5/2 | note:e s:piano ]",
"[ 5/2 → 3/1 | note:g s:piano ]",
"[ 3/1 → 25/8 | note:c s:piano ]",
"[ 25/8 → 13/4 | note:f s:piano ]",
"[ 13/4 → 27/8 | note:d s:piano ]",
"[ 27/8 → 7/2 | note:f s:piano ]",
"[ 7/2 → 4/1 | note:g s:piano ]",
]
`;
exports[`runs examples > example "scrub" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
@@ -8548,7 +8301,9 @@ exports[`runs examples > example "shrink" example index 3 1`] = `
]
`;
exports[`runs examples > example "shuffle" example index 0 1`] = `
exports[`runs examples > example "shuffle
Slices a pattern into the given number of parts, then plays those parts in random order.
Each part will be played exactly once per cycle." example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d s:piano ]",
@@ -8569,7 +8324,9 @@ exports[`runs examples > example "shuffle" example index 0 1`] = `
]
`;
exports[`runs examples > example "shuffle" example index 1 1`] = `
exports[`runs examples > example "shuffle
Slices a pattern into the given number of parts, then plays those parts in random order.
Each part will be played exactly once per cycle." example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]",
+1 -3
View File
@@ -1,9 +1,7 @@
import { defineConfig } from 'vitest/config';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import { configDefaults, defineConfig } from 'vitest/config';
/// <reference types="vitest" />
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
test: {
reporters: 'verbose',
isolate: false,
-2
View File
@@ -5,7 +5,6 @@ import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
@@ -135,7 +134,6 @@ export default defineConfig({
site,
base,
vite: {
plugins: [bundleAudioWorkletPlugin()],
ssr: {
// Example: Force a broken package to skip SSR processing, if needed
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
+1 -2
View File
@@ -73,7 +73,6 @@
"@vite-pwa/astro": "^0.5.0",
"html-escaper": "^3.0.3",
"sharp": "^0.33.5",
"workbox-window": "^7.3.0",
"vite-plugin-bundle-audioworklet": "workspace:*"
"workbox-window": "^7.3.0"
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ is equivalent to this Pattern without Mini Notation:
Similarly, there is an equivalent function for every aspect of the mini notation.
Which representation to use is a matter of context. As a rule of thumb, functions
are better suited in a larger context, while mini notation is more practical for individual rhythms.
are better suited in a larger context, while mini notation is more practical for individiual rhythms.
## Limits of Mini Notation
+6 -8
View File
@@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first
# Sound Banks
If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
We _could_ use them like this:
@@ -180,12 +180,11 @@ It assumes a `strudel.json` file to be present at the root of the repository:
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
```
## From Disk via "Import Sounds Folder"
## From Disk via "Import Sounds"
If you don't want to upload your samples to the internet, you can also load them from your local disk.
Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar.
Press the "import sounds folder" button and select a folder that contains audio files.
The folder you select can also contain subfolders with audio files.
Go to the `sounds` tab in the REPL and press "import sounds".
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
Example:
```
@@ -201,13 +200,12 @@ Example:
```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
If you select that `samples` folder, the `user` tab (next to the `import-sounds` tab) will then contain 2 new sounds: `swoop(3) smash(3)`
If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
The samples within each sound use zero-based indexing in alphabetical order.
## From Disk via @strudel/sampler
Instead of loading your samples into your browser with the "import sounds folder" button, you can also serve the samples from a local file server.
Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh
+18 -11
View File
@@ -296,16 +296,23 @@ The whole modulation will now take 8 cycles to repeat.
## Recap
| name | example |
| ------- | ---------------------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| signals | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain (saw)`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
| Name | Description | Example |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| lpf | Low-pass filter, cuts high frequencies | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | Vowel-like filter | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> |
| decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> |
| sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> |
| release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> |
| adsr | ADSR: Short notation (attack:decay:sustain:release) | <MiniRepl client:visible tune={`note("c3").s("sawtooth").adsr(".1:.2:.5:.3")`} /> |
| delay | Creates echoes | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | Simulates room reverb | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | Pans sound left (0) to right (1) | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | Changes playback speed (and pitch) | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| fast | Speeds up pattern playback (outside mini-notation) | <MiniRepl client:visible tune={`s("bd sd").fast(2)`} /> |
| slow | Slows down pattern playback (outside mini-notation)| <MiniRepl client:visible tune={`s("bd sd").slow(2)`} /> |
| signals | Modulate parameters with waveforms (`sine`, etc.) | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain(saw)`} /> |
| range | Set min/max values for signal modulation | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
+8 -6
View File
@@ -305,12 +305,14 @@ What's the difference?
Let's recap what we've learned in this chapter:
| Concept | Syntax | Example |
| --------- | ------ | -------------------------------------------------------- |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> |
| Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Concept | Syntax | Description | Example |
| --------------- | ------ | ---------------------------------------------- | -------------------------------------------------------------------------- |
| Slow down | / | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | Play one element per cycle from a sequence | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> |
| Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | Repeat an element multiple times | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Switch sounds | space | Cycle through sounds within the `sound` func | <MiniRepl client:visible tune={`note("c e").sound("piano sawtooth")`} /> |
| Stack sounds | , | Play multiple sounds together in `sound` func | <MiniRepl client:visible tune={`note("c e").sound("piano, sawtooth")`} /> |
New functions:
+10 -9
View File
@@ -270,16 +270,17 @@ This is shorter and more readable than:
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
This is what we've learned so far:
| Concept | Syntax | Example |
| ----------------- | -------- | ----------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | - or ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
| Concept | Syntax | Description | Example |
| ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| Sequence | space | Play sounds one after another | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> |
| Sample Number | :x | Select a specific sample from a sound | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | - or ~ | Add silence into the sequence | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Sub-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | Play multiple sequences at the same time | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Multiple-Lines | \`\` | Write sequences across multiple lines | <MiniRepl client:visible tune={'sound(`bd*2, \n hh*4`)'} /> |
The Mini-Notation is usually used inside some function. These are the functions we've seen so far:
+31 -24
View File
@@ -11,19 +11,20 @@ This page is just a listing of all functions covered in the workshop!
## Mini Notation
| Concept | Syntax | Example |
| ----------------- | -------- | --------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Concept | Syntax | Description | Example |
| ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| Sequence | space | Play sounds one after another | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> |
| Sample Number | :x | Select a specific sample from a sound | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | Add silence into the sequence | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | Play multiple sequences at the same time | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | Play one element per cycle from a sequence| <MiniRepl client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | Repeat an element multiple times | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Multiple-Lines | \`\` | Write sequences across multiple lines | <MiniRepl client:visible tune={'sound(`bd*2, \n hh*4`)'} /> |
## Sounds
@@ -43,20 +44,26 @@ This page is just a listing of all functions covered in the workshop!
## Audio Effects
| name | example |
| ----- | --------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*4")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*32").lpf(saw.range(200,4000))`} /> |
| Name | Description | Example |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| lpf | Low-pass filter, cuts high frequencies | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> |
| vowel | Vowel-like filter | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> |
| decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> |
| sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> |
| release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> |
| adsr | ADSR: Short notation (attack:decay:sustain:release) | <MiniRepl client:visible tune={`note("c3").s("sawtooth").adsr(".1:.2:.5:.3")`} /> |
| delay | Creates echoes | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | Simulates room reverb | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | Pans sound left (0) to right (1) | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | Changes playback speed (and pitch) | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| signals | Modulate parameters with waveforms (`sine`, etc.) | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain(saw)`} /> |
| range | Set min/max values for signal modulation | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
## Pattern Effects
| name | description | example |
| Name | Description | Example |
| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
| fast | speed up | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").fast(2)`} /> |
@@ -20,42 +20,25 @@ export default function ImportSoundsButton({ onComplete }) {
});
return (
<div>
<label
style={{ alignItems: 'center', borderColor: 'red', border: 1 }}
className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6 mr-2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m0-3-3-3m0 0-3 3m3-3v11.25m6-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75"
/>
</svg>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds folder'}
</label>
</div>
<label
style={{ alignItems: 'center' }}
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds'}
</label>
);
}
@@ -6,7 +6,7 @@ import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio';
function Checkbox({ label, value, onChange, disabled = false }) {
return (
@@ -108,7 +108,6 @@ export function SettingsTab({ started }) {
audioEngineTarget,
togglePanelTrigger,
maxPolyphony,
multiChannelOrbits,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -163,22 +162,6 @@ export function SettingsTab({ started }) {
value={maxPolyphony ?? ''}
/>
</FormItem>
<FormItem>
<Checkbox
label="Multi Channel Orbits"
onChange={(cbEvent) => {
const val = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
return window.location.reload();
}
});
}}
value={multiChannelOrbits}
/>
</FormItem>
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
@@ -14,8 +14,6 @@ export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const soundEntries = useMemo(() => {
if (!sounds) {
@@ -39,9 +37,6 @@ export function SoundsTab() {
if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
if (soundsFilter === 'importSounds') {
return [];
}
return filtered;
}, [sounds, soundsFilter, search]);
@@ -56,6 +51,7 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01);
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -69,9 +65,9 @@ export function SoundsTab() {
drums: 'drum-machines',
synths: 'Synths',
user: 'User',
importSounds: 'import-sounds',
}}
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
@@ -105,55 +101,7 @@ export function SoundsTab() {
</span>
);
})}
{!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p>
To import sounds into strudel, they must be contained{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
within a folder or subfolder
</a>
. The best way to do this is to upload a samples folder containing subfolders of individual sounds or
soundbanks (see diagram below).{' '}
</p>
<pre className="bg-background" key={'sample-diagram'}>
{`└─ samples <-- import this folder
swoop
swoopshort.wav
swooplong.wav
swooptight.wav
smash
smashhigh.wav
smashlow.wav
smashmiddle.wav`}
</pre>
<p>
The name of a subfolder corresponds to the sound name under the user tab. Multiple samples within a
subfolder are all labelled with the same name, but can be accessed using .n( ) - remember sounds are
zero-indexed and in alphabetical order!
</p>
<p>
For more information, and other ways to use your own sounds in strudel,{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
check out the docs
</a>
!
</p>
<h3>Preview Sounds</h3>
<pre className="bg-background" key={'sample-preview'}>
n("0 1 2 3 4 5").s("sample-name")
</pre>
<p>
Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your
sample as it appears under the "user" tab.
</p>
</div>
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
</div>
</div>
);
+13 -11
View File
@@ -54,23 +54,25 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
// Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted
if (typeof blob === 'string') {
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(blob);
sounds.set(parentDirectory, soundPaths);
return;
}
return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths);
return;
});
}),
)
.then(() => {
sounds.forEach((titlePathMap, key) => {
const value = Array.from(titlePathMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths);
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
+3 -7
View File
@@ -18,7 +18,7 @@ import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react';
import { parseBoolean, settingsMap, useSettings } from '../settings.mjs';
import { settingsMap, useSettings } from '../settings.mjs';
import {
setActivePattern,
setLatestCode,
@@ -36,15 +36,11 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser';
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick({
maxPolyphony,
audioDeviceName,
multiChannelOrbits: parseBoolean(multiChannelOrbits),
});
audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName });
modulesLoading = loadModules();
presets = prebake();
drawContext = getDrawContext();
+1 -3
View File
@@ -38,7 +38,6 @@ export const defaultSettings = {
isButtonRowHidden: false,
isCSSAnimationDisabled: false,
maxPolyphony: 128,
multiChannelOrbits: false,
};
let search = null;
@@ -51,7 +50,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings);
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export function useSettings() {
const state = useStore(settingsMap);
@@ -82,7 +81,6 @@ export function useSettings() {
isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
};
}