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", "jsdoc-json": "^2.0.2",
"lerna": "^8.1.9", "lerna": "^8.1.9",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"vitest": "^3.0.4", "vitest": "^3.0.4"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "1.2.1", "version": "1.1.0",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+2 -2
View File
@@ -1,5 +1,5 @@
/* /**
* Cutie Pi * @name Cutie Pi
* by Switch Angel * by Switch Angel
*/ */
import { tags as t } from '@lezer/highlight'; 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 { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/* /**
* Atom One * @name Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * 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 * Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains * From IntelliJ IDEA by JetBrains
*/ */
+2 -1
View File
@@ -1,5 +1,6 @@
/* /**
* @name dracula * @name dracula
* @author dracula
* Michael Kaminsky (http://github.com/mkaminsky11) * Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme) * Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/ */
+3 -3
View File
@@ -1,6 +1,6 @@
/* /**
* duotone * @name duotone
* author Bram de Haan * @author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) * 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'; import { tags as t } from '@lezer/highlight';
+3 -3
View File
@@ -1,6 +1,6 @@
/* /**
* duotone * @name duotone
* author Bram de Haan * @author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) * 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'; import { tags as t } from '@lezer/highlight';
+3
View File
@@ -1,3 +1,6 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
+3
View File
@@ -1,3 +1,6 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/* /**
* Atom One * @name Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * https://github.com/atom/one-dark-syntax
+4 -3
View File
@@ -1,6 +1,7 @@
/* /**
* gruvbox-dark * @name gruvbox-dark
* author morhetz * @author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/ */
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
+4 -3
View File
@@ -1,6 +1,7 @@
/* /**
* gruvbox-light * @name gruvbox-light
* author morhetz * @author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/ */
import { tags as t } from '@lezer/highlight'; 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 * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * 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 * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * 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'); 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. * Phaser audio effect that approximates popular guitar pedals.
* *
@@ -1496,7 +1461,7 @@ export const { vowel } = registerControl('vowel');
* @name waveloss * @name waveloss
*/ */
export const { waveloss } = registerControl('waveloss'); export const { waveloss } = registerControl('waveloss');
/** /*
* Noise crackle density * Noise crackle density
* *
* @name density * @name density
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/core", "name": "@strudel/core",
"version": "1.2.1", "version": "1.1.0",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+2 -1
View File
@@ -156,7 +156,8 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin(); 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. * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit * @name inhabit
* @synonyms pickSqueeze * @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. * 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. * Each part will be played exactly once per cycle.
* @name shuffle
* @example * @example
* note("c d e f").sound("piano").shuffle(4) * note("c d e f").sound("piano").shuffle(4)
* @example * @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`, * 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. * but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @example * @example
* note("c d e f").sound("piano").scramble(4) * note("c d e f").sound("piano").scramble(4)
* @example * @example
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/csound", "name": "@strudel/csound",
"version": "1.2.2", "version": "1.1.0",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/draw", "name": "@strudel/draw",
"version": "1.2.1", "version": "1.1.0",
"description": "Helpers for drawing with Strudel", "description": "Helpers for drawing with Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/gamepad", "name": "@strudel/gamepad",
"version": "1.2.1", "version": "1.1.0",
"description": "Gamepad Inputs for strudel", "description": "Gamepad Inputs for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,3 +35,4 @@
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"name": "hs2js", "name": "hs2js",
"version": "0.2.0", "version": "0.1.0",
"private": true,
"description": "Experimental Haskell in JavaScript interpreter", "description": "Experimental Haskell in JavaScript interpreter",
"main": "src/index.mjs", "main": "src/index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/hydra", "name": "@strudel/hydra",
"version": "1.2.1", "version": "1.1.0",
"description": "Hydra integration for strudel", "description": "Hydra integration for strudel",
"main": "hydra.mjs", "main": "hydra.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/midi", "name": "@strudel/midi",
"version": "1.2.2", "version": "1.1.0",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mini", "name": "@strudel/mini",
"version": "1.2.1", "version": "1.1.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/motion", "name": "@strudel/motion",
"version": "1.2.1", "version": "1.1.0",
"description": "DeviceMotion API for strudel", "description": "DeviceMotion API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,3 +35,4 @@
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mqtt", "name": "@strudel/mqtt",
"version": "1.2.1", "version": "1.1.0",
"description": "MQTT API for strudel", "description": "MQTT API for strudel",
"main": "mqtt.mjs", "main": "mqtt.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/osc", "name": "@strudel/osc",
"version": "1.2.1", "version": "1.1.0",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"type": "module", "type": "module",
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/reference", "name": "@strudel/reference",
"version": "1.2.0", "version": "1.1.0",
"description": "Headless reference of all strudel functions", "description": "Headless reference of all strudel functions",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -31,6 +31,8 @@
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
},
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11"
} }
+4
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@@ -10,6 +11,9 @@ export default defineConfig({
formats: ['es'], formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs' })[ext],
}, },
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext', target: 'esnext',
}, },
}); });
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/repl", "name": "@strudel/repl",
"version": "1.2.2", "version": "1.1.0",
"description": "Strudel REPL as a Web Component", "description": "Strudel REPL as a Web Component",
"module": "index.mjs", "module": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -46,7 +46,6 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -2
View File
@@ -1,12 +1,11 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/sampler", "name": "@strudel/sampler",
"version": "0.2.0", "version": "0.1.0",
"description": "", "description": "",
"keywords": [ "keywords": [
"tidalcycles", "tidalcycles",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/serial", "name": "@strudel/serial",
"version": "1.2.1", "version": "1.1.0",
"description": "Webserial API for strudel", "description": "Webserial API for strudel",
"main": "serial.mjs", "main": "serial.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/soundfonts", "name": "@strudel/soundfonts",
"version": "1.2.2", "version": "1.1.0",
"description": "Soundsfont support for strudel", "description": "Soundsfont support for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "superdough", "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.", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -32,8 +32,7 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": { "devDependencies": {
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
}, },
"dependencies": { "dependencies": {
"nanostores": "^0.11.3" "nanostores": "^0.11.3"
+25 -60
View File
@@ -8,7 +8,7 @@ import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.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 { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -18,16 +18,9 @@ export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY; let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) { export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
} }
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map(); export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) { export function registerSound(key, onTrigger, data = {}) {
@@ -35,16 +28,6 @@ export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(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) { function aliasBankMap(aliasMap) {
// Make all bank keys lower case for case insensitivity // Make all bank keys lower case for case insensitivity
for (const key in aliasMap) { for (const key in aliasMap) {
@@ -201,15 +184,8 @@ function loadWorklets() {
// this function should be called on first user interaction (to avoid console warning) // this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) { export async function initAudio(options = {}) {
const { const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
disableWorklets = false,
maxPolyphony,
audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME,
multiChannelOrbits = false,
} = options;
setMaxPolyphony(maxPolyphony); setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
@@ -290,7 +266,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
}); });
stereoMix.connect(splitter); stereoMix.connect(splitter);
channels.forEach((ch, i) => { 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; channelMerger == null;
}; };
function getDelay(orbit, delaytime, delayfeedback, t, channels) { function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) { if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); //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 ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed.. 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] = dly;
} }
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); 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]; return delays[orbit];
} }
export function getLfo(audioContext, time, end, properties = {}) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
return getWorklet(audioContext, 'lfo-processor', { //gain
frequency: 1, 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, depth: 1,
skew: 0, skew: 0,
phaseoffset: 0, phaseoffset: 0,
@@ -330,13 +313,8 @@ export function getLfo(audioContext, time, end, properties = {}) {
end, end,
shape: 1, shape: 1,
dcoffset: -0.5, dcoffset: -0.5,
...properties,
}); });
} lfo.connect(lfoGain);
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 });
//filters //filters
const numStages = 2; //num of filters in series const numStages = 2; //num of filters in series
@@ -367,12 +345,12 @@ function getFilterType(ftype) {
let reverbs = {}; let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before; 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 no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) { if (!reverbs[orbit]) {
const ac = getAudioContext(); const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir); const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, channels); connectToDestination(reverb, [0, 1]);
reverbs[orbit] = reverb; reverbs[orbit] = reverb;
} }
if ( if (
@@ -439,11 +417,6 @@ export function resetGlobalEffects() {
} }
let activeSoundSources = new Map(); 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) => { export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext(); const ac = getAudioContext();
@@ -506,7 +479,7 @@ export const superdough = async (value, t, hapDuration) => {
bpsustain, bpsustain,
bprelease, bprelease,
bandq = getDefaultValue('bandq'), bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser //phaser
phaserrate: phaser, phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'), phaserdepth = getDefaultValue('phaserdepth'),
@@ -542,18 +515,7 @@ export const superdough = async (value, t, hapDuration) => {
compressorRelease, compressorRelease,
} = value; } = value;
const orbitChannels = mapChannelNumbers( gain = nanFallback(gain, 1);
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
const chainID = Math.round(Math.random() * 1000000); const chainID = Math.round(Math.random() * 1000000);
@@ -568,6 +530,9 @@ export const superdough = async (value, t, hapDuration) => {
activeSoundSources.delete(chainID); 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 = []; let audioNodes = [];
if (bank && s) { if (bank && s) {
@@ -717,7 +682,7 @@ export const superdough = async (value, t, hapDuration) => {
// delay // delay
let delaySend; let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { 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); delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend); audioNodes.push(delaySend);
} }
@@ -735,7 +700,7 @@ export const superdough = async (value, t, hapDuration) => {
} }
roomIR = await loadBuffer(url, ac, ir, 0); 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); reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend); audioNodes.push(reverbSend);
} }
+5 -24
View File
@@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs'; import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getLfo } from './superdough.mjs'; import { registerSound, getAudioContext } from './superdough.mjs';
import { import {
applyFM, applyFM,
gainNode, gainNode,
@@ -26,9 +26,6 @@ const getFrequencyFromValue = (value) => {
return Number(freq); return Number(freq);
}; };
function destroyAudioWorkletNode(node) { function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect(); node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0); node.parameters.get('end')?.setValueAtTime(0, 0);
} }
@@ -148,20 +145,7 @@ export function registerSynthSounds() {
'pulse', 'pulse',
(begin, value, onended) => { (begin, value, onended) => {
const ac = getAudioContext(); const ac = getAudioContext();
let { pwrate, pwsweep } = value; let { duration, n: pulsewidth = 0.5 } = 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;
const frequency = getFrequencyFromValue(value); const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues( const [attack, decay, sustain, release] = getADSRValues(
@@ -169,8 +153,10 @@ export function registerSynthSounds() {
'linear', 'linear',
[0.001, 0.05, 0.6, 0.01], [0.001, 0.05, 0.6, 0.01],
); );
const holdend = begin + duration; const holdend = begin + duration;
const end = holdend + release + 0.01; const end = holdend + release + 0.01;
let o = getWorklet( let o = getWorklet(
ac, ac,
'pulse-oscillator', 'pulse-oscillator',
@@ -192,16 +178,11 @@ export function registerSynthSounds() {
envGain = o.connect(envGain); envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); 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( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect(); envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
+1 -2
View File
@@ -1,11 +1,10 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json'; import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"name": "@strudel/tidal", "name": "@strudel/tidal",
"version": "0.2.0", "version": "0.1.0",
"private": true,
"description": "Experimental Tidal Code interpreter", "description": "Experimental Tidal Code interpreter",
"module": "tidal.mjs", "module": "tidal.mjs",
"repository": { "repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/tonal", "name": "@strudel/tonal",
"version": "1.2.1", "version": "1.1.0",
"description": "Tonal functions for strudel", "description": "Tonal functions for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/transpiler", "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.", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "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", "name": "@strudel/web",
"version": "1.2.2", "version": "1.1.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.", "description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs", "module": "web.mjs",
"publishConfig": { "publishConfig": {
@@ -41,7 +41,6 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -2
View File
@@ -2,12 +2,11 @@ import { defineConfig } from 'vite';
import { dependencies } from './package.json'; import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'web.mjs'), entry: resolve(__dirname, 'web.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/webaudio", "name": "@strudel/webaudio",
"version": "1.2.2", "version": "1.1.0",
"description": "Web Audio helpers for Strudel", "description": "Web Audio helpers for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/xen", "name": "@strudel/xen",
"version": "1.2.1", "version": "1.1.0",
"description": "Xenharmonic API for strudel", "description": "Xenharmonic API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+3 -23
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7 version: 2.2.7
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 3.0.4 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': '@vitest/ui':
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4)
@@ -75,9 +75,6 @@ importers:
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.4.2
version: 3.4.2 version: 3.4.2
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:packages/vite-plugin-bundle-audioworklet
vitest: vitest:
specifier: ^3.0.4 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) 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: vite:
specifier: ^6.0.11 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) 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: packages/sampler:
dependencies: dependencies:
@@ -482,9 +476,6 @@ importers:
vite: vite:
specifier: ^6.0.11 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) 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: packages/tidal:
dependencies: dependencies:
@@ -549,12 +540,6 @@ importers:
specifier: ^3.0.4 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) 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: packages/web:
dependencies: dependencies:
'@strudel/core': '@strudel/core':
@@ -579,9 +564,6 @@ importers:
vite: vite:
specifier: ^6.0.11 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) 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: packages/webaudio:
dependencies: dependencies:
@@ -798,9 +780,6 @@ importers:
sharp: sharp:
specifier: ^0.33.5 specifier: ^0.33.5
version: 0.33.5 version: 0.33.5
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../packages/vite-plugin-bundle-audioworklet
workbox-window: workbox-window:
specifier: ^7.3.0 specifier: ^7.3.0
version: 7.3.0 version: 7.3.0
@@ -7582,6 +7561,7 @@ packages:
workbox-google-analytics@7.0.0: workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} 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: workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -10268,7 +10248,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@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`] = ` exports[`runs examples > example "detune" example index 0 1`] = `
[ [
"[ 0/1 → 1/12 | note:d s:supersaw detune: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 "queryArc" example index 0 1`] = `[]`;
exports[`runs examples > example "rand" 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`] = ` exports[`runs examples > example "scrub" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip: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 ]", "[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d 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 ]", "[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]",
+1 -3
View File
@@ -1,9 +1,7 @@
import { defineConfig } from 'vitest/config'; import { configDefaults, defineConfig } from 'vitest/config';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
/// <reference types="vitest" /> /// <reference types="vitest" />
export default defineConfig({ export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
test: { test: {
reporters: 'verbose', reporters: 'verbose',
isolate: false, isolate: false,
-2
View File
@@ -5,7 +5,6 @@ import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug'; import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls'; import rehypeUrls from 'rehype-urls';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind'; import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro'; import AstroPWA from '@vite-pwa/astro';
@@ -135,7 +134,6 @@ export default defineConfig({
site, site,
base, base,
vite: { vite: {
plugins: [bundleAudioWorkletPlugin()],
ssr: { ssr: {
// Example: Force a broken package to skip SSR processing, if needed // Example: Force a broken package to skip SSR processing, if needed
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 // 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", "@vite-pwa/astro": "^0.5.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+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. 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 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 ## 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 # 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: 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 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. 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. Go to the `sounds` tab in the REPL and press "import sounds".
Press the "import sounds folder" button and select a folder that contains audio files. Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
The folder you select can also contain subfolders with audio files.
Example: Example:
``` ```
@@ -201,13 +200,12 @@ Example:
``` ```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files. 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 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 ## 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): The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh ```sh
+18 -11
View File
@@ -296,16 +296,23 @@ The whole modulation will now take 8 cycles to repeat.
## Recap ## Recap
| name | example | | Name | Description | Example |
| ------- | ---------------------------------------------------------------------------------------------------------------- | | ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> | | lpf | Low-pass filter, cuts high frequencies | <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>")`} /> | | vowel | Vowel-like filter | <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")`} /> | | gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> | | attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> | | decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> | | sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> | | release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> |
| signals | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain (saw)`} /> | | adsr | ADSR: Short notation (attack:decay:sustain:release) | <MiniRepl client:visible tune={`note("c3").s("sawtooth").adsr(".1:.2:.5:.3")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> | | 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). 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: Let's recap what we've learned in this chapter:
| Concept | Syntax | Example | | Concept | Syntax | Description | Example |
| --------- | ------ | -------------------------------------------------------- | | --------------- | ------ | ---------------------------------------------- | -------------------------------------------------------------------------- |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> | | Slow down | / | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> | | Alternate | \<\> | Play one element per cycle from a sequence | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> |
| Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> | | Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <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: 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. 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: This is what we've learned so far:
| Concept | Syntax | Example | | Concept | Syntax | Description | Example |
| ----------------- | -------- | ----------------------------------------------------------------------- | | ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> | | Sequence | space | Play sounds one after another | <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")`} /> | | 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 ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> | | 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>")`} /> | | 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-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> | | Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> | | Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> | | 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: 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 ## Mini Notation
| Concept | Syntax | Example | | Concept | Syntax | Description | Example |
| ----------------- | -------- | --------------------------------------------------------------------- | | ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> | | Sequence | space | Play sounds one after another | <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")`} /> | | Sample Number | :x | Select a specific sample from a sound | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> | | Rests | ~ | Add silence into the sequence | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> | | Sub-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> | | Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> | | Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> | | Parallel | , | Play multiple sequences at the same time | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> | | Slow down | \/ | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`note("c <e g>")`} /> | | Alternate | \<\> | Play one element per cycle from a sequence| <MiniRepl client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> | | Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <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 ## Sounds
@@ -43,20 +44,26 @@ This page is just a listing of all functions covered in the workshop!
## Audio Effects ## Audio Effects
| name | example | | Name | Description | Example |
| ----- | --------------------------------------------------------------------------------------- | | ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> | | lpf | Low-pass filter, cuts high frequencies | <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>")`} /> | | vowel | Vowel-like filter | <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")`} /> | | gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> | | attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> | | decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> | | sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> | | release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> |
| range | <MiniRepl client:visible tune={`s("hh*32").lpf(saw.range(200,4000))`} /> | | 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 ## 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)`} /> | | 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)`} /> | | fast | speed up | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").fast(2)`} /> |
@@ -20,26 +20,10 @@ export default function ImportSoundsButton({ onComplete }) {
}); });
return ( return (
<div>
<label <label
style={{ alignItems: 'center', borderColor: 'red', border: 1 }} style={{ alignItems: 'center' }}
className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer" className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md 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 <input
disabled={isUploading} disabled={isUploading}
ref={fileUploadRef} ref={fileUploadRef}
@@ -54,8 +38,7 @@ export default function ImportSoundsButton({ onComplete }) {
onChange(); onChange();
}} }}
/> />
{isUploading ? 'importing...' : 'import sounds folder'} {isUploading ? 'importing...' : 'import sounds'}
</label> </label>
</div>
); );
} }
@@ -6,7 +6,7 @@ import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; 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 }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
@@ -108,7 +108,6 @@ export function SettingsTab({ started }) {
audioEngineTarget, audioEngineTarget,
togglePanelTrigger, togglePanelTrigger,
maxPolyphony, maxPolyphony,
multiChannelOrbits,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -163,22 +162,6 @@ export function SettingsTab({ started }) {
value={maxPolyphony ?? ''} value={maxPolyphony ?? ''}
/> />
</FormItem> </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"> <FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} /> <SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem> </FormItem>
@@ -14,8 +14,6 @@ export function SoundsTab() {
const sounds = useStore(soundMap); const sounds = useStore(soundMap);
const { soundsFilter } = useSettings(); const { soundsFilter } = useSettings();
const [search, setSearch] = useState(''); 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(() => { const soundEntries = useMemo(() => {
if (!sounds) { if (!sounds) {
@@ -39,9 +37,6 @@ export function SoundsTab() {
if (soundsFilter === 'synths') { if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
} }
if (soundsFilter === 'importSounds') {
return [];
}
return filtered; return filtered;
}, [sounds, soundsFilter, search]); }, [sounds, soundsFilter, search]);
@@ -56,6 +51,7 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01); ref?.stop(getAudioContext().currentTime + 0.01);
}); });
}); });
return ( 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 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -69,9 +65,9 @@ export function SoundsTab() {
drums: 'drum-machines', drums: 'drum-machines',
synths: 'Synths', synths: 'Synths',
user: 'User', user: 'User',
importSounds: 'import-sounds',
}} }}
></ButtonGroup> ></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div> </div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2"> <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> </span>
); );
})} })}
{!soundEntries.length && soundsFilter === 'importSounds' ? ( {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
<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).'
: ''}
</div> </div>
</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'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob; 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) => { return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map(); const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
titlePathMap.set(title, soundPath); sounds.set(parentDirectory, soundPaths);
sounds.set(parentDirectory, titlePathMap);
return; return;
}); });
}), }),
) )
.then(() => { .then(() => {
sounds.forEach((titlePathMap, key) => { sounds.forEach((soundPaths, key) => {
const value = Array.from(titlePathMap.keys()) const value = Array.from(soundPaths);
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample', type: 'sample',
+3 -7
View File
@@ -18,7 +18,7 @@ import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra'; import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { parseBoolean, settingsMap, useSettings } from '../settings.mjs'; import { settingsMap, useSettings } from '../settings.mjs';
import { import {
setActivePattern, setActivePattern,
setLatestCode, setLatestCode,
@@ -36,15 +36,11 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers'; import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady; let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick({ audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName });
maxPolyphony,
audioDeviceName,
multiChannelOrbits: parseBoolean(multiChannelOrbits),
});
modulesLoading = loadModules(); modulesLoading = loadModules();
presets = prebake(); presets = prebake();
drawContext = getDrawContext(); drawContext = getDrawContext();
+1 -3
View File
@@ -38,7 +38,6 @@ export const defaultSettings = {
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128, maxPolyphony: 128,
multiChannelOrbits: false,
}; };
let search = null; let search = null;
@@ -51,7 +50,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings); 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() { export function useSettings() {
const state = useStore(settingsMap); const state = useStore(settingsMap);
@@ -82,7 +81,6 @@ export function useSettings() {
isPanelPinned: parseBoolean(state.isPanelPinned), isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen), isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns, userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
}; };
} }