diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml new file mode 100644 index 00000000..2de6670c --- /dev/null +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -0,0 +1,37 @@ +name: Build and Deploy to beta (warm.strudel.cc) + +on: [workflow_dispatch] + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: docker + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + # cache: "pnpm" + - name: Install Dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Deploy + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc \ No newline at end of file diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 561fee65..0643f9be 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Build and Deploy +name: Build and Deploy to live (strudel.cc) on: [workflow_dispatch] @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2be3ee69..7532adaf 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,20 @@ tidal-drum-machines webaudiofontdata src-tauri/target + + +### BEGIN Visual Studio Code ### +# Blanket, recursive exclude for .vscode directory and files +.vscode/**/* +# Unexclude specific files and directories within .vscode +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +### END Visual Studio Code ### + + + # BEGIN JetBrains -> END JetBrains # for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 69aa3bc5..b8a569be 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,6 +1,9 @@ import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; +import { Scale } from '@tonaljs/tonal'; +import { soundMap } from 'superdough'; +import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { const div = document.createElement('div'); @@ -75,6 +78,51 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); +export function bankCompletions() { + const soundDict = soundMap.get(); + const banks = new Set(); + for (const key of Object.keys(soundDict)) { + const [bank, suffix] = key.split('_'); + if (suffix && bank) banks.add(bank); + } + return Array.from(banks) + .sort() + .map((name) => ({ label: name, type: 'bank' })); +} + +// Attempt to get all scale names from Tonal +let scaleCompletions = []; +try { + scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); +} catch (e) { + console.warn('[autocomplete] Could not load scale names from Tonal:', e); +} + +// Valid mode values for voicing +const modeCompletions = [ + { label: 'below', type: 'mode' }, + { label: 'above', type: 'mode' }, + { label: 'duck', type: 'mode' }, + { label: 'root', type: 'mode' }, +]; + +// Valid chord symbols from ireal dictionary plus empty string for major triads +const chordSymbols = ['', ...Object.keys(complex)].sort(); +const chordSymbolCompletions = chordSymbols.map((symbol) => { + if (symbol === '') { + return { + label: 'major', + apply: '', + type: 'chord-symbol', + }; + } + return { + label: symbol, + apply: symbol, + type: 'chord-symbol', + }; +}); + export const getSynonymDoc = (doc, synonym) => { const synonyms = doc.synonyms || []; const docLabel = getDocLabel(doc); @@ -113,19 +161,299 @@ const jsdocCompletions = (() => { return completions; })(); -export const strudelAutocomplete = (context) => { - const word = context.matchBefore(/\w*/); - if (word.from === word.to && !context.explicit) return null; +// --- Handler functions for each context --- +const pitchNames = [ + 'C', + 'C#', + 'Db', + 'D', + 'D#', + 'Eb', + 'E', + 'E#', + 'Fb', + 'F', + 'F#', + 'Gb', + 'G', + 'G#', + 'Ab', + 'A', + 'A#', + 'Bb', + 'B', + 'B#', + 'Cb', +]; +// Cached regex patterns for scaleHandler +const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/; +const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/; +const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/; +const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/; +const SCALE_SPACES_TO_COLON_REGEX = /\s+/g; + +function scaleHandler(context) { + // First check for scale context without quotes - block with empty completions + let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX); + if (scaleNoQuotesContext) { + return { + from: scaleNoQuotesContext.to, + options: [], + }; + } + + // Check for after-colon context first (more specific) + let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX); + if (scaleAfterColonContext) { + const text = scaleAfterColonContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx !== -1) { + const fragment = text.slice(colonIdx + 1); + const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); + const options = filteredScales.map((s) => ({ + ...s, + apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'), + })); + const from = scaleAfterColonContext.from + colonIdx + 1; + return { + from, + options, + }; + } + } + + // Then check for pre-colon context + let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX); + if (scalePreColonContext) { + if (!scalePreColonContext.text.includes(':')) { + if (context.explicit) { + const text = scalePreColonContext.text; + const match = text.match(SCALE_PITCH_MATCH_REGEX); + const fragment = match ? match[0] : ''; + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const from = scalePreColonContext.to - fragment.length; + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + return { from, options }; + } else { + return { from: scalePreColonContext.to, options: [] }; + } + } + } + return null; +} + +// Cached regex patterns for soundHandler +const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/; +const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/; +const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/; + +function soundHandler(context) { + // First check for sound context without quotes - block with empty completions + let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX); + if (soundNoQuotesContext) { + return { + from: soundNoQuotesContext.to, + options: [], + }; + } + + // Then check for sound context with quotes - provide completions + let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX); + if (!soundContext) return null; + + const text = soundContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX); + const fragment = fragMatch ? fragMatch[1] : inside; + const soundNames = Object.keys(soundMap.get()).sort(); + const filteredSounds = soundNames.filter((name) => name.includes(fragment)); + let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); + const from = soundContext.to - fragment.length; return { - from: word.from, - options: jsdocCompletions, - /* options: [ - { label: 'match', type: 'keyword' }, - { label: 'hello', type: 'variable', info: '(World)' }, - { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' }, - ], */ + from, + options, }; +} + +// Cached regex patterns for bankHandler +const BANK_NO_QUOTES_REGEX = /bank\(\s*$/; +const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/; + +function bankHandler(context) { + // First check for bank context without quotes - block with empty completions + let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX); + if (bankNoQuotesContext) { + return { + from: bankNoQuotesContext.to, + options: [], + }; + } + + // Then check for bank context with quotes - provide completions + let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX); + if (!bankMatch) return null; + + const text = bankMatch.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragment = inside; + let banks = bankCompletions(); + const filteredBanks = banks.filter((b) => b.label.startsWith(fragment)); + const from = bankMatch.to - fragment.length; + return { + from, + options: filteredBanks, + }; +} + +// Cached regex patterns for modeHandler +const MODE_NO_QUOTES_REGEX = /mode\(\s*$/; +const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/; +const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/; +const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/; + +function modeHandler(context) { + // First check for mode context without quotes - block with empty completions + let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX); + if (modeNoQuotesContext) { + return { + from: modeNoQuotesContext.to, + options: [], + }; + } + + // Check for after-colon context first (more specific) + let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX); + if (modeAfterColonContext) { + const text = modeAfterColonContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx !== -1) { + const fragment = text.slice(colonIdx + 1); + // For anchor after colon, we can suggest pitch names + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + const from = modeAfterColonContext.from + colonIdx + 1; + return { + from, + options, + }; + } + } + + // Then check for pre-colon context + let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX); + if (!modeContext) return null; + + const text = modeContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX); + const fragment = fragMatch ? fragMatch[1] : inside; + const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment)); + const from = modeContext.to - fragment.length; + return { + from, + options: filteredModes, + }; +} + +// Cached regex patterns for chordHandler +const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/; +const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/; +const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/; + +function chordHandler(context) { + // First check for chord context without quotes - block with empty completions + let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX); + if (chordNoQuotesContext) { + return { + from: chordNoQuotesContext.to, + options: [], + }; + } + + // Then check for chord context with quotes - provide completions + let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX); + if (!chordContext) return null; + + const text = chordContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + + // Use same fragment matching as sound/mode for expressions like "" + const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX); + const fragment = fragMatch ? fragMatch[1] : inside; + + // Check if fragment contains any pitch name at start (for root + symbol) + let rootMatch = null; + let symbolFragment = fragment; + for (const pitch of pitchNames) { + if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) { + rootMatch = pitch; + symbolFragment = fragment.slice(pitch.length); + break; + } + } + + if (rootMatch) { + // We have a root, now complete chord symbols + const filteredSymbols = chordSymbolCompletions.filter((s) => + s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()), + ); + + // Create completions that replace the entire chord, not just the symbol part + const options = filteredSymbols; + + const from = chordContext.to - symbolFragment.length; + return { from, options }; + } else { + // No root yet, complete with pitch names + const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' })); + const from = chordContext.to - fragment.length; + return { from, options }; + } +} + +// Cached regex patterns for fallbackHandler +const FALLBACK_WORD_REGEX = /\w*/; + +function fallbackHandler(context) { + const word = context.matchBefore(FALLBACK_WORD_REGEX); + if (word && word.from === word.to && !context.explicit) return null; + if (word) { + return { + from: word.from, + options: jsdocCompletions, + }; + } + return null; +} + +const handlers = [ + soundHandler, + bankHandler, + chordHandler, + scaleHandler, + modeHandler, + // this handler *must* be last + fallbackHandler, +]; + +export const strudelAutocomplete = (context) => { + for (const handler of handlers) { + const result = handler(context); + if (result) { + return result; + } + } + return null; }; export const isAutoCompletionEnabled = (enabled) => diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 803b33f3..e54c4a42 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.5", + "version": "1.2.6", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -46,8 +46,11 @@ "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", - "nanostores": "^0.11.3" + "@tonaljs/tonal": "^4.10.0", + "nanostores": "^0.11.3", + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7e3c2a8e..a3ca638a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1887,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', export const { shape } = registerControl(['shape', 'shapevol']); /** * Wave shaping distortion. CAUTION: it can get loud. - * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. + * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type. * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort * @synonyms dist - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") + * @example + * s("bd:4*4").bank("tr808").distort("3:0.5:diode") * */ -export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist'); +export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); + +/** + * Postgain for waveshaping distortion. + * + * @name distortvol + * @synonyms distvol + * @param {number | Pattern} volume linear postgain of the distortion + * @example + * s("bd*4").bank("tr909").distort(2).distortvol(0.8) + */ +export const { distortvol } = registerControl('distortvol', 'distvol'); + +/** + * Type of waveshaping distortion to apply. + * + * @name distorttype + * @synonyms disttype + * @param {number | string | Pattern} type type of distortion to apply + * @example + * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") + * + * @example + * s("sine").note("F1*2").release(1) + * .penv(24).pdecay(0.05) + * .distort(rand.range(1, 8)) + * .distorttype("") + */ +export const { distorttype } = registerControl('distorttype', 'disttype'); + /** * Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")` * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) @@ -2287,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch'); // TODO: what is this? export const { polyTouch } = registerControl('polyTouch'); +/** + * The host to send open sound control messages to. Requires running the OSC bridge. + * @name oschost + * @param {string | Pattern} oschost e.g. 'localhost' + * @example + * note("c4").oschost('127.0.0.1').oscport(57120).osc(); + */ +export const { oschost } = registerControl('oschost'); + +/** + * The port to send open sound control messages to. Requires running the OSC bridge. + * @name oscport + * @param {number | Pattern} oscport e.g. 57120 + * @example + * note("c4").oschost('127.0.0.1').oscport(57120).osc(); + */ +export const { oscport } = registerControl('oscport'); + export const getControlName = (alias) => { if (controlAlias.has(alias)) { return controlAlias.get(alias); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ad096103..59e410c5 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -57,7 +57,7 @@ export class Cyclist { } // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08ac..3e412074 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -38,8 +38,7 @@ export class NeoCyclist { if (this.started === false) { return; } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea..c33432f1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.4", + "version": "1.2.5", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f59..f5b0c676 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -98,10 +98,7 @@ export class Pattern { // runs func on query state withState(func) { - return this.withHaps((haps, state) => { - func(state); - return haps; - }); + return new Pattern((state) => this.query(func(state))); } /** @@ -3001,6 +2998,24 @@ export const extend = stepRegister('extend', function (factor, pat) { return pat.fast(factor).expand(factor); }); +/** + * *Experimental* + * + * `replicate` is similar to `fast` in that it increases its density, but it also increases the step count + * accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas + * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. + * + * TODO: find out how this function differs from extend + * @example + * stepcat( + * sound("bd bd - cp").replicate(2), + * sound("bd - sd -") + * ).pace(8) + */ +export const replicate = stepRegister('replicate', function (factor, pat) { + return pat.repeatCycles(factor).fast(factor).expand(factor); +}); + /** * *Experimental* * @@ -3536,3 +3551,76 @@ export const morph = (frompat, topat, bypat) => { bypat = reify(bypat); return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; + +/** + * Soft-clipping distortion + * + * @name soft + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Hard-clipping distortion + * + * @name hard + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Cubic polynomial distortion + * + * @name cubic + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Diode-emulating distortion + * + * @name diode + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Asymmetrical diode distortion + * + * @name asym + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Wavefolding distortion + * + * @name fold + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Wavefolding distortion composed with sinusoid + * + * @name sinefold + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Distortion via Chebyshev polynomials + * + * @name chebyshev + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of distAlgoNames) { + // Add aliases for distortion algorithms + Pattern.prototype[name] = function (args) { + const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); + return this.distort(argsPat); + }; +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 53562f21..171697eb 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -214,7 +214,10 @@ export function repl({ } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - let patterns = Object.values(pPatterns); + let patterns = []; + for (const [key, value] of Object.entries(pPatterns)) { + patterns.push(value.withState((state) => state.setControls({ id: key }))); + } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); @@ -228,6 +231,7 @@ export function repl({ pattern = allTransforms[i](pattern); } } + if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 162dc7da..8aa58195 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -19,9 +19,9 @@ export class State { return this.setSpan(func(this.span)); } - // Returns new State with different controls + // Returns new State with added controls. setControls(controls) { - return new State(this.span, controls); + return new State(this.span, { ...this.controls, ...controls }); } } diff --git a/packages/csound/package.json b/packages/csound/package.json index 90130a10..058b8d77 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.5", + "version": "1.2.6", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index a429395f..95f3aed5 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) { state.controls.painters = []; } state.controls.painters.push(painter); + return state; }); }; +// TODO - Why isn't this pure deep copy not working? +// Pattern.prototype.onPaint = function (painter) { +// return this.withState((state) => { +// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter]; +// return new State(state.span, { ...state.controls, painters }); +// }); +// }; + Pattern.prototype.getPainters = function () { let painters = []; this.queryArc(0, 0, { painters }); diff --git a/packages/draw/package.json b/packages/draw/package.json index 6a4c5754..ac212398 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.4", + "version": "1.2.5", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 555eac03..8605e35c 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.4", + "version": "1.2.5", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 7ba79d5d..eccdde52 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.4", + "version": "1.2.5", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 2342cf7e..ebb07fc3 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.5", + "version": "1.2.6", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index 6eeaab0d..05bbd019 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.4", + "version": "1.2.5", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 9b1edb5a..a230c3cb 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -5,7 +5,7 @@ import { slow, seq, stepcat, - extend, + replicate, expand, pace, chooseIn, @@ -37,7 +37,7 @@ lib.square = (...args) => stepcat(...args).setSteps(1); lib.angle = (...args) => stepcat(...args).pace(1); lib['*'] = fast; lib['/'] = slow; -lib['!'] = extend; +lib['!'] = replicate; lib['@'] = expand; lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. diff --git a/packages/mondough/package.json b/packages/mondough/package.json index c81d76cb..593d2264 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.4", + "version": "1.1.5", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index 57cac9cc..b0a4d556 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.4", + "version": "1.2.5", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index aef01bd9..d74de342 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function ( cx.connect(props); } return this.withHap((hap) => { - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic; if (!cx || !cx.isConnected()) { return; diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 2e32825f..1b34a4ea 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.4", + "version": "1.2.5", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index fe069152..68caab3a 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import OSC from 'osc-js'; - import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; let connection; // Promise @@ -13,19 +11,18 @@ function connect() { if (!connection) { // make sure this runs only once connection = new Promise((resolve, reject) => { - const osc = new OSC(); - osc.open(); - osc.on('open', () => { - const url = osc.options?.plugin?.socket?.url; - logger(`[osc] connected${url ? ` to ${url}` : ''}`); - resolve(osc); + const ws = new WebSocket('ws://localhost:8080'); + ws.addEventListener('open', (event) => { + logger(`[osc] websocket connected`); + resolve(ws); }); - osc.on('close', () => { + ws.addEventListener('close', (event) => { + logger(`[osc] websocket closed`); connection = undefined; // allows new connection afterwards console.log('[osc] disconnected'); reject('OSC connection closed'); }); - osc.on('error', (err) => reject(err)); + ws.addEventListener('error', (err) => reject(err)); }).catch((err) => { connection = undefined; throw new Error('Could not connect to OSC server. Is it running?'); @@ -61,15 +58,19 @@ export function parseControlsFromHap(hap, cps) { const collator = new ClockCollator({}); export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { - const osc = await connect(); + const ws = await connect(); const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); + const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000; + const msg = { address: '/dirt/play', args: keyvals, timestamp: ts }; - const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000); - const message = new OSC.Message('/dirt/play', ...keyvals); - const bundle = new OSC.Bundle([message], ts); - bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60 - osc.send(bundle); + if ('oschost' in hap.value) { + msg['host'] = hap.value['oschost']; + } + if ('oscport' in hap.value) { + msg['port'] = hap.value['oscport']; + } + ws.send(JSON.stringify(msg)); } /** diff --git a/packages/osc/package.json b/packages/osc/package.json index bc828d79..173b7674 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.10", + "version": "1.3.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", @@ -38,7 +38,8 @@ "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", - "osc-js": "^2.4.1" + "osc": "^2.4.5", + "ws": "^8.18.3" }, "devDependencies": { "pkg": "^5.8.1", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 index d7ec21c4..ede35e97 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -6,70 +6,59 @@ Copyright (C) 2022 Strudel contributors - see . */ -import OSC from 'osc-js'; +// import OSC from 'osc-js'; -const args = process.argv.slice(2); -function getArgValue(flag) { - const i = args.indexOf(flag); - if (i !== -1) { - const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; - if (nextIsFlag) return true; - return args[i + 1]; - } -} +import { WebSocketServer } from 'ws'; +import osc from 'osc'; -let udpClientPort = Number(getArgValue('--port')) || 57120; -let debug = Number(getArgValue('--debug')) || 0; +const WS_PORT = 8080; // WebSocket server port +const OSC_REMOTE_IP = '127.0.0.1'; +const OSC_REMOTE_PORT = 57120; -const config = { - receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client - udpServer: { - host: 'localhost', // @param {string} Hostname of udp server to bind to - port: 57121, // @param {number} Port of udp client for messaging - // enabling the following line will receive tidal messages: - // port: 57120, // @param {number} Port of udp client for messaging - exclusive: false, // @param {boolean} Exclusive flag - }, - udpClient: { - host: 'localhost', // @param {string} Hostname of udp client for messaging - port: udpClientPort, // @param {number} Port of udp client for messaging - }, - wsServer: { - host: 'localhost', // @param {string} Hostname of WebSocket server - port: 8080, // @param {number} Port of WebSocket server - }, -}; - -const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); - -if (debug) { - osc.on('*', (message) => { - const { address, args } = message; - let str = ''; - for (let i = 0; i < args.length; i += 2) { - str += `${args[i]}: ${args[i + 1]} `; - } - console.log(`${address} ${str}`); - }); -} - -osc.on('error', (message) => { - if (message.toString().includes('EADDRINUSE')) { - console.log(`------ ERROR ------- -a server is already running on port 57121! to stop it: -1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) -2. re-run the osc server -`); - } else { - console.log(message); - } +const udpPort = new osc.UDPPort({ + localAddress: '0.0.0.0', + localPort: 0, + remoteAddress: OSC_REMOTE_IP, + remotePort: OSC_REMOTE_PORT, }); -osc.open(); +udpPort.open(); +console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`); -console.log('osc client running on port', config.udpClient.port); -console.log('osc server running on port', config.udpServer.port); -console.log('websocket server running on port', config.wsServer.port); -if (debug) { - console.log('debug logs enabled. incoming messages will appear below'); -} +udpPort.on('error', (e) => { + console.log('Error: ', e); +}); + +const wss = new WebSocketServer({ port: WS_PORT }); +console.log(`[Listening WS] ws://localhost:${WS_PORT}`); + +wss.on('connection', (ws) => { + console.log('New WebSocket connection'); + + ws.on('message', (message) => { + let osc_host = '127.0.0.1'; + let osc_port = 57120; + + try { + const data = JSON.parse(message); + if ('host' in data) { + osc_host = data['host']; + } + if ('port' in data) { + osc_port = data['port']; + } + let msg = { address: data['address'], args: data['args'] }; + if ('timestamp' in data) { + msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] }; + } + + udpPort.send(msg, osc_host, osc_port); + } catch (err) { + console.error('Error parsing message:', err); + } + }); + + ws.on('close', () => { + console.log('WebSocket connection closed'); + }); +}); diff --git a/packages/repl/package.json b/packages/repl/package.json index 41a165c3..1ad173c6 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.6", + "version": "1.2.7", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 3f421f98..26d875c2 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -20,10 +20,13 @@ export async function prebake() { // import('@strudel/osc'), ); // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main'; // TODO: move this onto the strudel repo - const ts = 'https://raw.githubusercontent.com/todepond/samples/main/'; + const ts = 'https://raw.githubusercontent.com/todepond/samples/main'; + + const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main'; + await Promise.all([ modulesLoading, registerSynthSounds(), @@ -36,9 +39,9 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), + samples(`${tc}/strudel.json`), ]); aliasBank(`${ts}/tidal-drum-machines-alias.json`); diff --git a/packages/serial/package.json b/packages/serial/package.json index 9ed89cf2..d73b05a1 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.4", + "version": "1.2.5", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 07e35674..8c41a5d5 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.5", + "version": "1.2.6", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 0c6e4f14..f5947d83 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples') The format is `github://`. +If `` and `` are not specified, they will default to `samples` and `main` respectively. It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. The format is also expected to be the same as explained above. diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs new file mode 100644 index 00000000..71e01d57 --- /dev/null +++ b/packages/superdough/audioContext.mjs @@ -0,0 +1,18 @@ +let audioContext; + +export const setDefaultAudioContext = () => { + audioContext = new AudioContext(); + return audioContext; +}; + +export const getAudioContext = () => { + if (!audioContext) { + return setDefaultAudioContext(); + } + + return audioContext; +}; + +export function getAudioContextCurrentTime() { + return getAudioContext().currentTime; +} diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index 5b02ad29..aac08061 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let worklet; export async function dspWorklet(ac, code) { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47bca330..a471da9d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,6 +1,7 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; +import { logger } from './logger.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -381,6 +382,102 @@ export function applyFM(param, value, begin) { return { stop }; } +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) +const _mod = (n, m) => ((n % m) + m) % m; + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); + +const _fold = (x, k) => { + // Closed form folding for audio rate + let y = (1 + 0.5 * k) * x; + const window = _mod(y + 1, 4); + return 1 - Math.abs(window - 2); +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _cubic = (x, k) => { + const t = __squash(Math.log1p(k)); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + 2 * k; // gain + const t = __squash(Math.log1p(k)); + const bias = 0.07 * t; + const pos = _soft(x + bias, 2 * k); + const neg = _soft(asym ? bias : -x + bias, 2 * k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _chebyshev = (x, k) => { + const kl = 10 * Math.log1p(k); + let tnm1 = 1; + let tnm2 = x; + let tn; + let y = 0; + for (let i = 1; i < 64; i++) { + if (i < 2) { + // Already set inital conditions + y += i == 0 ? tnm1 : tnm2; + continue; + } + tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition + tnm2 = tnm1; + tnm1 = tn; + if (i % 2 === 0) { + y += Math.min((1.3 * kl) / i, 2) * tn; + } + } + // Soft clip + return _soft(y, kl / 20); +}; + +export const distortionAlgorithms = { + scurve: _scurve, + soft: _soft, + hard: _hard, + cubic: _cubic, + diode: _diode, + asym: _asym, + fold: _fold, + sinefold: _sineFold, + chebyshev: _chebyshev, +}; +const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); + +export const getDistortionAlgorithm = (algo) => { + let index = algo; + if (typeof algo === 'string') { + index = _algoNames.indexOf(algo); + if (index === -1) { + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${_algoNames[0]}.`); + index = 0; + } + } + const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number + return distortionAlgorithms[name]; +}; + +export const getDistortion = (distort, postgain, algorithm) => { + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); +}; + export const getFrequencyFromValue = (value, defaultNote = 36) => { let { note, freq } = value; note = note || defaultNote; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index a7e87ffa..a382bd15 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -11,4 +11,5 @@ export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; export * from './dspworklet.mjs'; +export * from './audioContext.mjs'; export * from './wavetable.mjs'; diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 5411f2f2..816dd252 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,5 +1,5 @@ import { drywet } from './helpers.mjs'; -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a8211777..a95fc5ed 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.5", + "version": "1.2.6", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index b195ba79..84bac358 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,6 @@ -import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; -import { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { registerSound, registerWaveTable } from './index.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -120,13 +121,18 @@ function githubPath(base, subpath = '') { if (!base.startsWith('github:')) { throw new Error('expected "github:" at the start of pseudoUrl'); } - let [_, path] = base.split('github:'); + let path = base.slice('github:'.length); path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; - } - return `https://raw.githubusercontent.com/${path}/${subpath}`; + + let components = path.split('/'); + let user = components[0]; + let repo = components.length >= 2 ? components[1] : 'samples'; + let branch = components.length >= 3 ? components[2] : 'main'; + let other = components.slice(3); + other.push(subpath ? subpath : ''); + other = other.join('/'); + + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 52ed9bff..36bd6896 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,10 +9,11 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; @@ -154,6 +155,7 @@ let defaultDefaultValues = { phaserdepth: 0.75, shapevol: 1, distortvol: 1, + distorttype: 0, delay: 0, byteBeatExpression: '0', delayfeedback: 0.5, @@ -200,25 +202,6 @@ export function setVersionDefaults(version) { export const resetLoadedSounds = () => soundMap.set({}); -let audioContext; - -export const setDefaultAudioContext = () => { - audioContext = new AudioContext({ latencyHint: 'playback' }); - return audioContext; -}; - -export const getAudioContext = () => { - if (!audioContext) { - return setDefaultAudioContext(); - } - - return audioContext; -}; - -export function getAudioContextCurrentTime() { - return getAudioContext().currentTime; -} - let externalWorklets = []; export function registerWorklet(url) { externalWorklets.push(url); @@ -473,6 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), + distorttype = getDefaultValue('distorttype'), pan, vowel, delay = getDefaultValue('delay'), @@ -647,7 +631,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype)); if (tremolosync != null) { tremolo = cps * tremolosync; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 5dc5dc08..e35b9880 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,6 @@ import { clamp } from './util.mjs'; -import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; +import { registerSound, soundMap } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { applyFM, destroyAudioWorkletNode, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f78..01d4eb83 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,6 +1,7 @@ import { getAudioContext, registerSound } from './index.mjs'; import { getCommonSampleInfo } from './util.mjs'; import { + applyFM, applyParameterModulators, destroyAudioWorkletNode, getADSRValues, @@ -230,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { begin: t, end: envEnd, frequency, - detune: value.detune, + freqspread: value.detune, position: value.wt, warp: value.warp, warpMode: warpmode, voices: Math.max(value.unison ?? 1, 1), - spread: value.spread, + panspread: value.spread, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, }, { outputChannelCount: [2] }, @@ -308,6 +309,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, ); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fm = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); @@ -318,6 +320,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { () => { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); + fm?.stop(); node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ccfaf510..7d7ef8e2 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,6 +4,7 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const mod = (n, m) => ((n % m) + m) % m; @@ -430,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor { ]; } - constructor() { + constructor({ processorOptions }) { super(); this.started = false; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm); } process(inputs, outputs, parameters) { @@ -444,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - - const shape = Math.expm1(parameters.distort[0]); - const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; + const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); + const shape = Math.expm1(pv(parameters.distort, n)); + for (let ch = 0; ch < input.length; ch++) { + const x = input[ch][n]; + output[ch][n] = postgain * this.algorithm(x, shape); } } return true; @@ -1049,14 +1050,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return [ { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, - { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, - { name: 'detune', defaultValue: 0.18 }, - { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, - { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'frequency', defaultValue: 440, min: Number.EPSILON }, + { name: 'detune', defaultValue: 0 }, + { name: 'freqspread', defaultValue: 0.18, min: 0 }, + { name: 'position', defaultValue: 0, min: 0, max: 1 }, + { name: 'warp', defaultValue: 0, min: 0, max: 1 }, { name: 'warpMode', defaultValue: 0 }, - { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, - { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'voices', defaultValue: 1, min: 1 }, + { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, + { name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, ]; } @@ -1234,10 +1236,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { _sampleFrame(frame, phase) { const len = frame.length; const pos = phase * len; - const i = pos | 0; + let i = pos | 0; + if (i >= len) i = 0; // fast wrap const frac = pos - i; const a = frame[i]; - const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + let i1 = i + 1; + if (i1 >= len) i1 = 0; const b = frame[i1]; return a + (b - a) * frac; } @@ -1267,6 +1271,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); + const freqspread = pv(parameters.freqspread, i); const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; @@ -1275,9 +1280,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); - const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; - const gain1 = Math.sqrt(0.5 - 0.5 * spread); - const gain2 = Math.sqrt(0.5 + 0.5 * spread); + const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; + const gain1 = Math.sqrt(0.5 - 0.5 * panspread); + const gain2 = Math.sqrt(0.5 + 0.5 * panspread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune const normalizer = 1 / Math.sqrt(voices); @@ -1290,7 +1295,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const dPhase = fVoice * this.invSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index a6af8260..32db0395 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -1,6 +1,7 @@ //import { ZZFX } from 'zzfx'; import { midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; export const getZZFX = (value, t) => { diff --git a/packages/superdough/zzfx_fork.mjs b/packages/superdough/zzfx_fork.mjs index 7235d1bf..f3ee6a05 100644 --- a/packages/superdough/zzfx_fork.mjs +++ b/packages/superdough/zzfx_fork.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; // https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 // changes: replaced this.volume with 1 + using sampleRate from getAudioContext() diff --git a/packages/supradough/package.json b/packages/supradough/package.json index 7e465c0a..50304371 100644 --- a/packages/supradough/package.json +++ b/packages/supradough/package.json @@ -1,6 +1,6 @@ { "name": "supradough", - "version": "1.2.3", + "version": "1.2.4", "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", "main": "index.mjs", "type": "module", @@ -32,6 +32,5 @@ "vite": "^6.0.11", "vite-plugin-bundle-audioworklet": "workspace:*", "wav-encoder": "^1.3.0" - }, - "dependencies": {} + } } diff --git a/packages/tonal/index.mjs b/packages/tonal/index.mjs index a3c0cba3..ebbcf085 100644 --- a/packages/tonal/index.mjs +++ b/packages/tonal/index.mjs @@ -1,9 +1,9 @@ import './tonal.mjs'; import './voicings.mjs'; +import './ireal.mjs'; export * from './tonal.mjs'; export * from './voicings.mjs'; - -import './ireal.mjs'; +export * from './ireal.mjs'; export const packageName = '@strudel/tonal'; diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 1461bdc8..611df676 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.4", + "version": "1.2.5", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 18722bdc..bd28a19f 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.4", + "version": "1.2.5", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index df21f405..aef044c7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.5", + "version": "1.2.6", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 49da00f2..2d5cd420 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.5", + "version": "1.2.6", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 0a6736d9..4015c437 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.4", + "version": "1.2.5", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f..23903fca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) @@ -212,12 +212,21 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw + '@strudel/tonal': + specifier: workspace:* + version: link:../tonal '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + '@tonaljs/tonal': + specifier: ^4.10.0 + version: 4.10.0 nanostores: specifier: ^0.11.3 version: 0.11.3 + superdough: + specifier: workspace:* + version: link:../superdough devDependencies: vite: specifier: ^6.0.11 @@ -406,9 +415,12 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core - osc-js: - specifier: ^2.4.1 - version: 2.4.1 + osc: + specifier: ^2.4.5 + version: 2.4.5 + ws: + specifier: ^8.18.3 + version: 8.18.3 devDependencies: pkg: specifier: ^5.8.1 @@ -2420,6 +2432,70 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + '@shikijs/core@1.29.1': resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==} @@ -3672,6 +3748,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -5281,6 +5366,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5664,6 +5752,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5722,6 +5813,9 @@ packages: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + node-addon-api@8.3.0: resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} engines: {node: ^18 || ^20 || >= 21} @@ -5760,6 +5854,10 @@ packages: resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==} engines: {node: '>= 0.6.0'} + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -5918,8 +6016,8 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - osc-js@2.4.1: - resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==} + osc@2.4.5: + resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==} own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} @@ -6680,6 +6778,10 @@ packages: serialize-javascript@4.0.0: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -6767,6 +6869,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slip@1.0.2: + resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -7642,6 +7747,9 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + wolfy87-eventemitter@5.2.9: + resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7764,6 +7872,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlcreate@2.0.4: resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} @@ -9760,6 +9880,76 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@serialport/bindings-interface@1.2.2': + optional: true + + '@serialport/parser-byte-length@12.0.0': + optional: true + + '@serialport/parser-cctalk@12.0.0': + optional: true + + '@serialport/parser-delimiter@11.0.0': + optional: true + + '@serialport/parser-delimiter@12.0.0': + optional: true + + '@serialport/parser-inter-byte-timeout@12.0.0': + optional: true + + '@serialport/parser-packet-length@12.0.0': + optional: true + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + optional: true + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + optional: true + + '@serialport/parser-ready@12.0.0': + optional: true + + '@serialport/parser-regex@12.0.0': + optional: true + + '@serialport/parser-slip-encoder@12.0.0': + optional: true + + '@serialport/parser-spacepacket@12.0.0': + optional: true + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + '@shikijs/core@1.29.1': dependencies: '@shikijs/engine-javascript': 1.29.1 @@ -10364,7 +10554,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 @@ -11272,6 +11462,11 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.4: + dependencies: + ms: 2.1.2 + optional: true + debug@4.4.0: dependencies: ms: 2.1.3 @@ -13141,6 +13336,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@4.0.0: {} + longest-streak@3.1.0: {} loupe@3.1.2: {} @@ -13818,6 +14015,9 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: + optional: true + ms@2.1.3: {} multimatch@5.0.0: @@ -13867,6 +14067,9 @@ snapshots: dependencies: semver: 7.6.3 + node-addon-api@7.0.0: + optional: true + node-addon-api@8.3.0: {} node-domexception@1.0.0: {} @@ -13893,6 +14096,9 @@ snapshots: node-getopt@0.3.2: {} + node-gyp-build@4.6.0: + optional: true + node-gyp-build@4.8.4: {} node-gyp@10.3.1: @@ -14144,11 +14350,17 @@ snapshots: os-tmpdir@1.0.2: {} - osc-js@2.4.1: + osc@2.4.5: dependencies: + long: 4.0.0 + slip: 1.0.2 + wolfy87-eventemitter: 5.2.9 ws: 8.18.0 + optionalDependencies: + serialport: 12.0.0 transitivePeerDependencies: - bufferutil + - supports-color - utf-8-validate own-keys@1.0.1: @@ -15043,6 +15255,26 @@ snapshots: dependencies: randombytes: 2.1.0 + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -15185,6 +15417,8 @@ snapshots: slash@3.0.0: {} + slip@1.0.2: {} + smart-buffer@4.2.0: {} socks-proxy-agent@8.0.5: @@ -16094,6 +16328,8 @@ snapshots: dependencies: string-width: 7.2.0 + wolfy87-eventemitter@5.2.9: {} + word-wrap@1.2.5: {} wordwrap@0.0.3: {} @@ -16293,6 +16529,8 @@ snapshots: ws@8.18.0: {} + ws@8.18.3: {} + xmlcreate@2.0.4: {} xtend@4.0.2: {} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 73278947..c39bb87b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2829,6 +2829,82 @@ exports[`runs examples > example "distort" example index 1 1`] = ` ] `; +exports[`runs examples > example "distort" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distorttype" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", +] +`; + +exports[`runs examples > example "distorttype" example index 1 1`] = ` +[ + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distortvol" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", +] +`; + exports[`runs examples > example "djf" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", @@ -6882,6 +6958,24 @@ exports[`runs examples > example "orbit" example index 0 1`] = ` ] `; +exports[`runs examples > example "oschost" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", +] +`; + +exports[`runs examples > example "oscport" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", +] +`; + exports[`runs examples > example "outside" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:A3 ]", @@ -8231,6 +8325,33 @@ exports[`runs examples > example "repeatCycles" example index 0 1`] = ` ] `; +exports[`runs examples > example "replicate" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 3/8 → 1/2 | s:cp ]", + "[ 1/2 → 5/8 | s:bd ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 7/8 → 1/1 | s:cp ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 5/4 → 11/8 | s:sd ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 15/8 → 2/1 | s:cp ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 17/8 → 9/4 | s:bd ]", + "[ 19/8 → 5/2 | s:cp ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 11/4 → 23/8 | s:sd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 25/8 → 13/4 | s:bd ]", + "[ 27/8 → 7/2 | s:cp ]", + "[ 7/2 → 29/8 | s:bd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 31/8 → 4/1 | s:cp ]", +] +`; + exports[`runs examples > example "reset" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index f9c6b3a2..c454dcce 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp .room(0.5)`} /> +## Write your own chained function + +You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. + + pat + .s("sawtooth") + .cutoff(500) + //.delay(0.5) + .room(0.5) + ) +note("a3 c#4 e4 a4").effectChain()`} +/> + +Try adding `.rev()` after `effectChain()` to hear further effects added. + # Comments The `//` in the example above is a line comment, resulting in the `delay` function being ignored. diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf..201d57df 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 21a9b917..0fcc4136 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all: .pitchJump(0) // +/- pitch change after pitchJumpTime .pitchJumpTime(0) // >0 time after pitchJump is applied .lfo(0) // >0 resets slide + pitchJump + sets tremolo speed - .tremolo(0) // 0-1 lfo volume modulation amount + .tremolo(0.5) // 0-1 lfo volume modulation amount //.duration(.2) // overwrite strudel event duration //.gain(1) // change volume ._scope() // vizualise waveform (not zzfx related) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 1fbc8402..8a6ccc7e 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -6,6 +6,7 @@ import './files.mjs'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; +const baseCDN = 'https://strudel.b-cdn.net'; export async function prebake() { // https://archive.org/details/SalamanderGrandPianoV3 @@ -19,23 +20,23 @@ export async function prebake() { // => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically // seems to be a problem with soundfont2 import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), - samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }), + samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }), - samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', { + samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }), + samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { + samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, { prebake: true, }), - samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }), samples( { casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'], @@ -145,14 +146,14 @@ export async function prebake() { 'num/20.wav', ], }, - 'github:tidalcycles/dirt-samples', + `${baseCDN}/Dirt-Samples/`, { prebake: true, }, ), ]); - aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`); + aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`); } const maxPan = noteToMidi('C8');