diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml deleted file mode 100644 index 07fd91be8..000000000 --- a/.github/workflows/tauri.yml +++ /dev/null @@ -1,62 +0,0 @@ -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 diff --git a/package.json b/package.json index df50f1f41..01c3e7d5d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "jsdoc-json": "^2.0.2", "lerna": "^8.1.9", "prettier": "^3.4.2", - "vitest": "^3.0.4" + "vitest": "^3.0.4", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 6b37038ff..243500bff 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -11,7 +11,9 @@ export const flashField = StateField.define({ for (let e of tr.effects) { if (e.is(setFlash)) { if (e.value && tr.newDoc.length > 0) { - const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); + const mark = Decoration.mark({ + attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, + }); flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 44326b7c7..1c3d93c7c 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.1.0", + "version": "1.2.0", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index c2f49065a..6aa70471d 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre import teletext, { settings as teletextSettings } from './themes/teletext.mjs'; import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs'; import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; -import terminal, { settings as terminalSettings } from './themes/terminal.mjs'; -import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs'; +import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; +import redText, { settings as redTextSettings } from './themes/red-text.mjs'; +import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import aura, { settings as auraSettings } from './themes/aura.mjs'; -import bespin, { settings as bespinSettings } from './themes/bespin.mjs'; import darcula, { settings as darculaSettings } from './themes/darcula.mjs'; import dracula, { settings as draculaSettings } from './themes/dracula.mjs'; import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs'; -import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs'; import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs'; import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs'; import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs'; @@ -32,52 +31,47 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs'; import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs'; import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs'; -// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs'; import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs'; import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; import { setTheme } from '@strudel/draw'; - export const themes = { strudelTheme, - bluescreen, - blackscreen, - whitescreen, - teletext, algoboy, + androidstudio, atomone, aura, + bbedit, + blackscreen, + bluescreen, + CutiePi, darcula, dracula, - // todo: optimize - // bespin, - //abcdef, - androidstudio, duotoneDark, + eclipse, githubDark, - CutiePi, + githubLight, + greenText, gruvboxDark, + gruvboxLight, + sonicPink, materialDark, - nord, + materialLight, monokai, + noctisLilac, + nord, + redText, solarizedDark, + solarizedLight, sublime, + teletext, tokyoNight, + tokyoNightDay, tokyoNightStorm, vscodeDark, - //xcodeDark, - // LIGHT - bbedit, - //duotoneLight, - eclipse, - githubLight, - gruvboxLight, - materialLight, vscodeLight, - noctisLilac, - solarizedLight, - tokyoNightDay, + whitescreen, xcodeLight, }; @@ -88,21 +82,19 @@ export const settings = { whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, - terminal: terminalSettings, - abcdef: abcdefSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, bbedit: bbeditSettings, - bespin: bespinSettings, darcula: darculaSettings, dracula: draculaSettings, - duotoneLight: duotoneLightSettings, duotoneDark: duotoneDarkSettings, eclipse: eclipseSettings, CutiePi: CutiePiSettings, + sonicPink: sonicPinkSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, + greenText: greenTextSettings, gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, @@ -110,6 +102,7 @@ export const settings = { noctisLilac: noctisLilacSettings, nord: nordSettings, monokai: monokaiSettings, + redText: redTextSettings, solarizedLight: solarizedLightSettings, solarizedDark: solarizedDarkSettings, sublime: sublimeSettings, @@ -118,7 +111,6 @@ export const settings = { vscodeDark: vscodeDarkSettings, vscodeLight: vscodeLightSettings, xcodeLight: xcodeLightSettings, - //xcodeDark: xcodeDarkSettings, tokyoNightDay: tokyoNightDaySettings, }; diff --git a/packages/codemirror/themes/abcdef.mjs b/packages/codemirror/themes/abcdef.mjs deleted file mode 100644 index 917823b9f..000000000 --- a/packages/codemirror/themes/abcdef.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @name abcdef - * @author codemirror.net - * https://codemirror.net/5/theme/abcdef.css - */ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#0f0f0f', - lineBackground: '#0f0f0f99', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - gutterBackground: '#555', - gutterForeground: '#FFFFFF', - lineHighlight: '#314151', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#0f0f0f', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - // gutterBackground: '#555', - gutterBackground: 'transparent', - /* gutterForeground: '#FFFFFF', */ - gutterForeground: '#7a7b7c', - lineHighlight: '#0a6bcb3d', - }, - styles: [ - { tag: t.labelName, color: 'inherit' }, - { tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' }, - { tag: t.atom, color: '#77F' }, - { tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' }, - { tag: t.number, color: 'violet' }, - { tag: t.definition(t.variableName), color: '#fffabc' }, - { tag: t.variableName, color: '#abcdef' }, - { tag: t.function(t.variableName), color: '#fffabc' }, - { tag: t.typeName, color: '#FFDD44' }, - { tag: t.tagName, color: '#def' }, - { tag: t.string, color: '#2b4' }, - { tag: t.meta, color: '#C9F' }, - // { tag: t.qualifier, color: '#FFF700' }, - // { tag: t.builtin, color: '#30aabc' }, - { tag: t.bracket, color: '#8a8a8a' }, - { tag: t.attributeName, color: '#DDFF00' }, - { tag: t.heading, color: 'aquamarine', fontWeight: 'bold' }, - { tag: t.link, color: 'blueviolet', fontWeight: 'bold' }, - ], -}); diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 30812d126..049ed596d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,6 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] }, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index 85951d157..4c9fdec6e 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -38,12 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' }, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/bespin.mjs b/packages/codemirror/themes/bespin.mjs deleted file mode 100644 index 41af821b0..000000000 --- a/packages/codemirror/themes/bespin.mjs +++ /dev/null @@ -1,39 +0,0 @@ -// this is different from https://thememirror.net/bespin -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#28211c', - lineBackground: '#28211c99', - foreground: '#9d9b97', - caret: '#797977', - selection: '#36312e', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: 'rgba(255, 255, 255, 0.1)', -}; -export default createTheme({ - theme: 'dark', - settings: { - background: '#28211c', - foreground: '#9d9b97', - caret: '#797977', - selection: '#4f382b', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: '#ffffff1a', - }, - styles: [ - { tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' }, - { tag: t.comment, color: '#937121' }, - { tag: [t.keyword, t.tagName], color: '#cf6a4c' }, - { tag: t.string, color: '#f9ee98' }, - { tag: t.bracket, color: '#9d9b97' }, - { tag: [t.variableName], color: '#5ea6ea' }, - { tag: t.definition(t.variableName), color: '#cf7d34' }, - { tag: [t.function(t.variableName), t.className], color: '#cf7d34' }, - { tag: [t.propertyName, t.attributeName], color: '#54be0d' }, - ], -}); diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs new file mode 100644 index 000000000..bd21f6d3a --- /dev/null +++ b/packages/codemirror/themes/green-text.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs new file mode 100644 index 000000000..9fe7f516a --- /dev/null +++ b/packages/codemirror/themes/red-text.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs new file mode 100644 index 000000000..976597ecc --- /dev/null +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c']; + +export const settings = { + background: '#000000', + lineBackground: 'transparent', + foreground: hex[4], + selection: hex[6], + gutterBackground: hex[0], + gutterForeground: hex[5], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[4], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] }, + + { tag: t.comment, color: '#54636D' }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] }, + { tag: [t.attributeName, t.number], color: hex[3] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 7fd6bb651..7c4a3a919 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,14 +5,11 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', - // foreground: '#75baff', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', - // lineHighlight: '#8a91991a', // original lineHighlight: '#00000050', gutterBackground: 'transparent', - // gutterForeground: '#8a919966', gutterForeground: '#8a919966', }; @@ -20,6 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' }, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/xcodeDark.mjs b/packages/codemirror/themes/xcodeDark.mjs deleted file mode 100644 index 41440cfd1..000000000 --- a/packages/codemirror/themes/xcodeDark.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#292A30', - lineBackground: '#292A3099', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#2F3239', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#292A30', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#ffffff0f', - }, - styles: [ - { tag: [t.comment, t.quote], color: '#7F8C98' }, - { tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' }, - { tag: [t.string, t.meta], color: '#FF8170' }, - { tag: [t.typeName], color: '#DABAFF' }, - { tag: [t.definition(t.variableName)], color: '#6BDFFF' }, - { tag: [t.name], color: '#6BAA9F' }, - { tag: [t.variableName], color: '#ACF2E4' }, - { tag: [t.regexp, t.link], color: '#FF8170' }, - ], -}); diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 82330bb85..a6c650e03 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1766,14 +1766,46 @@ export const getControlName = (alias) => { * Sets properties in a batch. * * @name as - * @param {Array} mapping the control names that are set + * @param {String | Array} mapping the control names that are set * @example * "c:.5 a:1 f:.25 e:.8".as("note:clip") + * @example + * "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1) */ export const as = register('as', (mapping, pat) => { + mapping = Array.isArray(mapping) ? mapping : [mapping]; return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); return v; }); }); + +/** + * Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file + * in the optional array syntax ex: "0.5:2", the second value controls the speed of playback + * @name scrub + * @memberof Pattern + * @returns Pattern + * @example + * samples('github:switchangel/pad') + * s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8") + * @example + * samples('github:yaxu/clean-breaks/main'); + * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) + */ + +export const scrub = register( + 'scrub', + (beginPat, pat) => { + return beginPat.outerBind((v) => { + if (!Array.isArray(v)) { + v = [v]; + } + const [beginVal, speedMultiplier = 1] = v; + + return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1); + }); + }, + false, +); diff --git a/packages/core/package.json b/packages/core/package.json index 9123259a5..c6a686613 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.1.0", + "version": "1.2.0", "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 c1e95211d..6e294129e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2105,11 +2105,13 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. + * @name segment + * @synonyms seg * @param {number} segments number of segments per cycle * @example * note(saw.range(40,52).segment(24)) */ -export const segment = register('segment', function (rate, pat) { +export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2485,16 +2487,24 @@ export const bypass = register( ); /** - * Loops the pattern inside at `offset` for `cycles`. + * Loops the pattern inside an `offset` for `cycles`. + * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. + * @name ribbon + * @synonym rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example - * note("").ribbon(1, 2).fast(2) + * note("").ribbon(1, 2) * @example * // Looping a portion of randomness - * note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2) + * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) + * @example + * // rhythm generator + * s("bd!16?").ribbon(29,.5) */ -export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) => + pat.early(offset).restart(pure(1).slow(cycles)), +); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..66ac8a5a6 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -526,6 +526,9 @@ export const degradeByWith = register( * s("hh*8").degradeBy(0.2) * @example * s("[hh?0.2]*8") + * @example + * //beat generator + * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( 'degradeBy', diff --git a/packages/csound/package.json b/packages/csound/package.json index cf71dfbc3..dbd94d128 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.1.0", + "version": "1.2.1", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index e93f3ed22..3aec41ba4 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.1.0", + "version": "1.2.0", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 199c5d034..101ea42b9 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,38 +1,37 @@ { - "name": "@strudel/gamepad", - "version": "1.1.0", - "description": "Gamepad Inputs for strudel", - "main": "index.mjs", - "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, - "scripts": { - "build": "vite build", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" - }, - "keywords": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Yuta Nakayama ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*" - }, - "devDependencies": { - "vite": "^6.0.11" - } + "name": "@strudel/gamepad", + "version": "1.2.0", + "description": "Gamepad Inputs for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "vite": "^6.0.11" + } } - \ No newline at end of file diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index 34bb7f368..c650dfc83 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -1,6 +1,7 @@ { "name": "hs2js", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Haskell in JavaScript interpreter", "main": "src/index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index c687d5497..68d744124 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.1.0", + "version": "1.2.0", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index c7c5f4988..f2c31d55b 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.1.0", + "version": "1.2.1", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index d05d60364..cd910d003 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.1.0", + "version": "1.2.0", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index cc117a268..ebd3d997d 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,38 +1,37 @@ { - "name": "@strudel/motion", - "version": "1.1.0", - "description": "DeviceMotion API for strudel", - "main": "index.mjs", - "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, - "scripts": { - "build": "vite build", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" - }, - "keywords": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Yuta Nakayama ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*" - }, - "devDependencies": { + "name": "@strudel/motion", + "version": "1.2.0", + "description": "DeviceMotion API for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { "vite": "^6.0.11" - } + } } - \ No newline at end of file diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index ef5c3c680..f9d6be4f3 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.1.0", + "version": "1.2.0", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 4b9ab14e7..3c7b92d4e 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -37,7 +37,7 @@ function connect() { export function parseControlsFromHap(hap, cps) { hap.ensureObjectValue(); const cycle = hap.wholeOrPart().begin.valueOf(); - const delta = hap.duration.valueOf(); + const delta = hap.duration.valueOf() / cps; const controls = Object.assign({}, { cps, cycle, delta }, hap.value); // make sure n and note are numbers controls.n && (controls.n = parseNumeral(controls.n)); diff --git a/packages/osc/package.json b/packages/osc/package.json index 5c56bb2e3..ec05ac64c 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.1.0", + "version": "1.2.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "type": "module", diff --git a/packages/reference/package.json b/packages/reference/package.json index f69aa6536..fd7edadff 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.1.0", + "version": "1.2.0", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", @@ -31,8 +31,6 @@ "url": "https://github.com/tidalcycles/strudel/issues" }, "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - }, "devDependencies": { "vite": "^6.0.11" } diff --git a/packages/reference/vite.config.js b/packages/reference/vite.config.js index 5df3edc1b..44ff1e405 100644 --- a/packages/reference/vite.config.js +++ b/packages/reference/vite.config.js @@ -1,5 +1,4 @@ import { defineConfig } from 'vite'; -import { dependencies } from './package.json'; import { resolve } from 'path'; // https://vitejs.dev/config/ @@ -11,9 +10,6 @@ export default defineConfig({ formats: ['es'], fileName: (ext) => ({ es: 'index.mjs' })[ext], }, - rollupOptions: { - external: [...Object.keys(dependencies)], - }, target: 'esnext', }, }); diff --git a/packages/repl/package.json b/packages/repl/package.json index 7a48ad3fd..6e8ad29d1 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.1.0", + "version": "1.2.1", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { @@ -46,6 +46,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js index 674e0ebb4..c1ef1bf7f 100644 --- a/packages/repl/vite.config.js +++ b/packages/repl/vite.config.js @@ -1,11 +1,12 @@ import { defineConfig } from 'vite'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/sampler/package.json b/packages/sampler/package.json index 252fe9350..b0f27f86a 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.1.0", + "version": "0.2.0", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index 74cef80ab..8a0467bef 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.1.0", + "version": "1.2.0", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index af6d2b969..cc97ae826 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.1.0", + "version": "1.2.1", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..6bde69373 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.onended = () => { onComplete(); }; + return constantNode; } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 5fb8439bf..6cdf8b312 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.1.0", + "version": "1.2.1", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", @@ -32,7 +32,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "devDependencies": { - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f5e46d6bb..18d1b7797 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -344,7 +344,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { }; let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); - const stop = (endTime, playWholeBuffer) => {}; + const stop = (endTime) => { + bufferSource.stop(endTime); + }; const handle = { node: out, bufferSource, stop }; // cut groups diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..a1dc30b7c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -8,16 +8,35 @@ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod } from './util.mjs'; -import workletsUrl from './worklets.mjs?worker&url'; +import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +export const DEFAULT_MAX_POLYPHONY = 128; +const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; + +let maxPolyphony = DEFAULT_MAX_POLYPHONY; + +export function setMaxPolyphony(polyphony) { + maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; +} export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - soundMap.setKey(key.toLowerCase(), { onTrigger, data }); + key = key.toLowerCase().replace(/\s+/g, '_'); + 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) { @@ -85,6 +104,18 @@ export function getSound(s) { return soundMap.get()[s.toLowerCase()]; } +export const getAudioDevices = async () => { + await navigator.mediaDevices.getUserMedia({ audio: true }); + let mediaDevices = await navigator.mediaDevices.enumerateDevices(); + mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const devicesMap = new Map(); + devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, ''); + mediaDevices.forEach((device) => { + devicesMap.set(device.label, device.deviceId); + }); + return devicesMap; +}; + const defaultDefaultValues = { s: 'triangle', gain: 0.8, @@ -155,18 +186,40 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + const audioCtx = getAudioContext(); + workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); } + return workletsLoading; } // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; + setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; } - await getAudioContext().resume(); + + const audioCtx = getAudioContext(); + + if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) { + try { + const devices = await getAudioDevices(); + const id = devices.get(audioDeviceName); + const isValidID = (id ?? '').length > 0; + if (audioCtx.sinkId !== id && isValidID) { + await audioCtx.setSinkId(id); + } + logger( + `[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`, + ); + } catch { + logger('[superdough] failed to set audio interface', 'warning'); + } + } + + await audioCtx.resume(); if (disableWorklets) { logger('[superdough]: AudioWorklets disabled with disableWorklets'); return; @@ -374,6 +427,8 @@ export function resetGlobalEffects() { analysersData = {}; } +let activeSoundSources = new Map(); + export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; @@ -471,16 +526,32 @@ export const superdough = async (value, t, hapDuration) => { compressorRelease, } = value; - gain = nanFallback(gain, 1); + 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); + + // oldest audio nodes will be destroyed if maximum polyphony is exceeded + for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) { + const ch = activeSoundSources.entries().next(); + const source = ch.value[1]; + const chainID = ch.value[0]; + const endTime = t + 0.25; + source?.node?.gain?.linearRampToValueAtTime(0, endTime); + source?.stop?.(endTime); + 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 toDisconnect = []; // audio nodes that will be disconnected when the source has ended - const onended = () => { - toDisconnect.forEach((n) => n?.disconnect()); - }; + let audioNodes = []; + if (bank && s) { s = `${bank}_${s}`; value.s = s; @@ -492,10 +563,15 @@ export const superdough = async (value, t, hapDuration) => { sourceNode = source(t, value, hapDuration); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, value, onended); + const onEnded = () => { + audioNodes.forEach((n) => n?.disconnect()); + activeSoundSources.delete(chainID); + }; + const soundHandle = await onTrigger(t, value, onEnded); + if (soundHandle) { sourceNode = soundHandle.node; - soundHandle.stop(t + hapDuration); + activeSoundSources.set(chainID, soundHandle); } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -625,6 +701,7 @@ export const superdough = async (value, t, hapDuration) => { if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delyNode = getDelay(orbit, delaytime, delayfeedback, t); delaySend = effectSend(post, delyNode, delay); + audioNodes.push(delaySend); } // reverb let reverbSend; @@ -642,6 +719,7 @@ export const superdough = async (value, t, hapDuration) => { } const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); reverbSend = effectSend(post, reverbNode, room); + audioNodes.push(reverbSend); } // analyser @@ -649,14 +727,12 @@ export const superdough = async (value, t, hapDuration) => { if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); analyserSend = effectSend(post, analyserNode, 1); + audioNodes.push(analyserSend); } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - - // toDisconnect = all the node that should be disconnected in onended callback - // this is crucial for performance - toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); + audioNodes = audioNodes.concat(chain); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..8bb47bb7d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => { return Number(freq); }; +function destroyAudioWorkletNode(node) { + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +} const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const noises = ['pink', 'white', 'brown', 'crackle']; @@ -63,7 +67,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, @@ -110,10 +116,12 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -123,11 +131,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -169,10 +177,12 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -182,11 +192,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -229,7 +239,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, diff --git a/packages/superdough/vite.config.js b/packages/superdough/vite.config.js index d94e37f41..10ac0f06c 100644 --- a/packages/superdough/vite.config.js +++ b/packages/superdough/vite.config.js @@ -1,10 +1,11 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 61d5d96e9..03d3a966f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -710,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { } process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } if (currentTime <= params.begin[0]) { return true; } diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 1758732d6..8e4871617 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -1,6 +1,7 @@ { "name": "@strudel/tidal", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Tidal Code interpreter", "module": "tidal.mjs", "repository": { diff --git a/packages/tonal/package.json b/packages/tonal/package.json index e642fa4e3..e3589c1f1 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.1.0", + "version": "1.2.0", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 55140f802..deb407617 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.1.0", + "version": "1.2.0", "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/vite-plugin-bundle-audioworklet/package.json b/packages/vite-plugin-bundle-audioworklet/package.json new file mode 100644 index 000000000..f803ae065 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/package.json @@ -0,0 +1,16 @@ +{ + "name": "vite-plugin-bundle-audioworklet", + "main": "./vite-plugin-bundle-audioworklet.js", + "version": "0.1.1", + "description": "", + "keywords": [ + "vite", + "audioworklet" + ], + "author": "Felix Roos ", + "license": "MIT", + "type": "module", + "devDependencies": { + "vite": "^6.0.11" + } +} diff --git a/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js new file mode 100644 index 000000000..4c5634ba4 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js @@ -0,0 +1,48 @@ +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; diff --git a/packages/web/package.json b/packages/web/package.json index b06e5a489..2554f89d3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.1.0", + "version": "1.2.1", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { @@ -41,6 +41,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js index 99b93956f..671bf2825 100644 --- a/packages/web/vite.config.js +++ b/packages/web/vite.config.js @@ -2,11 +2,12 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'web.mjs'), diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 81b75c036..56e72e961 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.1.0", + "version": "1.2.1", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 74c3aba73..e7e5f2cee 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.1.0", + "version": "1.2.0", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fd711e2..a23c80924 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(@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(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -75,6 +75,9 @@ importers: prettier: specifier: ^3.4.2 version: 3.4.2 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:packages/vite-plugin-bundle-audioworklet vitest: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) @@ -428,6 +431,9 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet packages/sampler: dependencies: @@ -476,6 +482,9 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet packages/tidal: dependencies: @@ -540,6 +549,12 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/vite-plugin-bundle-audioworklet: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/web: dependencies: '@strudel/core': @@ -564,6 +579,9 @@ importers: vite: specifier: ^6.0.11 version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet packages/webaudio: dependencies: @@ -780,6 +798,9 @@ importers: sharp: specifier: ^0.33.5 version: 0.33.5 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../packages/vite-plugin-bundle-audioworklet workbox-window: specifier: ^7.3.0 version: 7.3.0 @@ -7561,7 +7582,6 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -10248,7 +10268,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@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))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ebd1c209d..2d88d8355 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -933,6 +933,39 @@ exports[`runs examples > example "as" example index 0 1`] = ` ] `; +exports[`runs examples > example "as" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | begin:0 s:sax_vib clip:1 ]", + "[ 1/4 → 3/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/8 → 1/2 | begin:0 s:sax_vib clip:1 ]", + "[ 1/2 → 5/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 5/8 → 3/4 | begin:0.3 s:sax_vib clip:1 ]", + "[ 3/4 → 7/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ (7/8 → 1/1) ⇝ 9/8 | begin:0 s:sax_vib clip:1 ]", + "[ 7/8 ⇜ (1/1 → 9/8) | begin:0 s:sax_vib clip:1 ]", + "[ 9/8 → 5/4 | begin:0.25 s:sax_vib clip:1 ]", + "[ 5/4 → 11/8 | begin:0 s:sax_vib clip:1 ]", + "[ 11/8 → 3/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 3/2 → 13/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 13/8 → 7/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/4 → 2/1 | begin:0 s:sax_vib clip:1 ]", + "[ 2/1 → 17/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 17/8 → 9/4 | begin:0 s:sax_vib clip:1 ]", + "[ 9/4 → 19/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 19/8 → 5/2 | begin:0.3 s:sax_vib clip:1 ]", + "[ 5/2 → 21/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 21/8 → 23/8 | begin:0 s:sax_vib clip:1 ]", + "[ 23/8 → 3/1 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/1 → 25/8 | begin:0 s:sax_vib clip:1 ]", + "[ 25/8 → 13/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 13/4 → 27/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 27/8 → 7/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/2 → 15/4 | begin:0 s:sax_vib clip:1 ]", + "[ 15/4 → 31/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 31/8 → 4/1 | begin:0 s:sax_vib clip:1 ]", +] +`; + exports[`runs examples > example "attack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 attack:0 ]", @@ -2289,6 +2322,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` ] `; +exports[`runs examples > example "degradeBy" example index 2 1`] = ` +[ + "[ 1/8 → 3/16 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 5/16 → 3/8 | s:bd ]", + "[ 1/2 → 9/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 21/16 → 11/8 | s:bd ]", + "[ 3/2 → 25/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 37/16 → 19/8 | s:bd ]", + "[ 5/2 → 41/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 53/16 → 27/8 | s:bd ]", + "[ 7/2 → 57/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "delay" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0 ]", @@ -7090,35 +7156,76 @@ exports[`runs examples > example "rev" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 0 1`] = ` [ - "[ 0/1 → 1/2 | note:d ]", - "[ 1/2 → 1/1 | note:e ]", - "[ 1/1 → 3/2 | note:d ]", - "[ 3/2 → 2/1 | note:e ]", - "[ 2/1 → 5/2 | note:d ]", - "[ 5/2 → 3/1 | note:e ]", - "[ 3/1 → 7/2 | note:d ]", - "[ 7/2 → 4/1 | note:e ]", + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", ] `; exports[`runs examples > example "ribbon" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | note:G3 ]", + "[ 0/1 → 1/4 | note:A3 ]", "[ 1/4 → 1/2 | note:C3 ]", "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:F3 ]", - "[ 1/1 → 5/4 | note:Eb3 ]", - "[ 5/4 → 3/2 | note:Ab3 ]", - "[ 3/2 → 7/4 | note:G3 ]", - "[ 7/4 → 2/1 | note:C4 ]", - "[ 2/1 → 9/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", "[ 9/4 → 5/2 | note:C3 ]", "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:F3 ]", - "[ 3/1 → 13/4 | note:Eb3 ]", - "[ 13/4 → 7/2 | note:Ab3 ]", - "[ 7/2 → 15/4 | note:G3 ]", - "[ 15/4 → 4/1 | note:C4 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "ribbon" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", ] `; @@ -7764,6 +7871,52 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "scrub" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", +] +`; + +exports[`runs examples > example "scrub" example index 1 1`] = ` +[ + "[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40 ]", diff --git a/vitest.config.ts b/vitest.config.mjs similarity index 71% rename from vitest.config.ts rename to vitest.config.mjs index 026f0b7c6..dcdde1133 100644 --- a/vitest.config.ts +++ b/vitest.config.mjs @@ -1,7 +1,9 @@ -import { configDefaults, defineConfig } from 'vitest/config'; +import { defineConfig } from 'vitest/config'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; /// export default defineConfig({ + plugins: [bundleAudioWorkletPlugin()], test: { reporters: 'verbose', isolate: false, diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 631584220..151c48fc1 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,6 +5,7 @@ import remarkToc from 'remark-toc'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeUrls from 'rehype-urls'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; @@ -134,6 +135,7 @@ export default defineConfig({ site, base, vite: { + plugins: [bundleAudioWorkletPlugin()], ssr: { // Example: Force a broken package to skip SSR processing, if needed // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 diff --git a/website/package.json b/website/package.json index 257ff6430..8e980f1ca 100644 --- a/website/package.json +++ b/website/package.json @@ -73,6 +73,7 @@ "@vite-pwa/astro": "^0.5.0", "html-escaper": "^3.0.3", "sharp": "^0.33.5", - "workbox-window": "^7.3.0" + "workbox-window": "^7.3.0", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 442596174..890de3aae 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first # Sound Banks -If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc.. +If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc.. We _could_ use them like this: @@ -180,11 +180,12 @@ It assumes a `strudel.json` file to be present at the root of the repository: https://raw.githubusercontent.com////strudel.json ``` -## From Disk via "Import Sounds" +## From Disk via "Import Sounds Folder" 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 press "import sounds". -Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files. +Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar. +Press the "import sounds folder" button and select a folder that contains audio files. +The folder you select can also contain subfolders with audio files. Example: ``` @@ -200,12 +201,13 @@ Example: ``` In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files. -If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)` +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)` The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`. +The samples within each sound use zero-based indexing in alphabetical order. ## From Disk via @strudel/sampler -Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server. +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. The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler): ```sh diff --git a/website/src/pages/learn/stepwise.mdx b/website/src/pages/learn/stepwise.mdx index d3ca334af..e632ee7ec 100644 --- a/website/src/pages/learn/stepwise.mdx +++ b/website/src/pages/learn/stepwise.mdx @@ -24,7 +24,7 @@ With the new stepwise `stepcat` function, the steps of the two patterns will be By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step. -However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each. +However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattern are twice as long, and so counted as two steps each. ## Pacing the steps diff --git a/website/src/pages/udels/index.astro b/website/src/pages/udels/index.astro index 4ab699e73..157e53151 100644 --- a/website/src/pages/udels/index.astro +++ b/website/src/pages/udels/index.astro @@ -1,12 +1,14 @@ --- +import HeadCommon from '@components/HeadCommon.astro'; import { Udels } from '../../components/Udels/Udels.jsx'; - - -const { BASE_URL } = import.meta.env; -const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; --- - - - - \ No newline at end of file + + + + Strudel UDELS + + + + + diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx index ec8d8be33..1d9de037d 100644 --- a/website/src/repl/components/UserFacingErrorMessage.jsx +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -4,5 +4,9 @@ export default function UserFacingErrorMessage(Props) { if (error == null) { return; } - return
{error.message || 'Unknown Error :-/'}
; + return ( +
+ Error: {error.message || 'Unknown Error :-/'} +
+ ); } diff --git a/website/src/repl/components/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx index d1f13c22d..288ffc0a5 100644 --- a/website/src/repl/components/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../../util.mjs'; + import { SelectInput } from './SelectInput'; +import { getAudioDevices } from '@strudel/webaudio'; const initdevices = new Map(); @@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) { if (!devicesInitialized) { return; } - const deviceID = devices.get(deviceName); onChange(deviceName); - setAudioDevice(deviceID); }; const options = new Map(); Array.from(devices.keys()).forEach((deviceName) => { diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index 7862b7669..5574c396c 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -20,25 +20,42 @@ export default function ImportSoundsButton({ onComplete }) { }); return ( - +
+ +
); } diff --git a/website/src/repl/components/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx index a28c39362..b47809671 100644 --- a/website/src/repl/components/panel/SelectInput.jsx +++ b/website/src/repl/components/panel/SelectInput.jsx @@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) { onChange(e.target.value)} > @@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) { ); } -function FormItem({ label, children }) { +function FormItem({ label, children, sublabel }) { return (
@@ -105,6 +107,7 @@ export function SettingsTab({ started }) { audioDeviceName, audioEngineTarget, togglePanelTrigger, + maxPolyphony, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -139,6 +142,26 @@ export function SettingsTab({ started }) { }} /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v; + setMaxPolyphony(v); + settingsMap.setKey('maxPolyphony', v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v); + }} + type="number" + placeholder="" + value={maxPolyphony ?? ''} + /> + settingsMap.setKey('theme', theme)} /> @@ -179,25 +202,7 @@ export function SettingsTab({ started }) { value={togglePanelTrigger} onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)} items={{ click: 'Click', hover: 'Hover' }} - > - {/* { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'click'); - } - }} - value={togglePanelTrigger != 'hover'} /> - { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'hover'); - } - }} - value={togglePanelTrigger == 'hover'} - /> */} { if (!sounds) { @@ -37,6 +39,9 @@ export function SoundsTab() { if (soundsFilter === 'synths') { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === 'importSounds') { + return []; + } return filtered; }, [sounds, soundsFilter, search]); @@ -51,7 +56,6 @@ export function SoundsTab() { ref?.stop(getAudioContext().currentTime + 0.01); }); }); - return (
setSearch(v)} /> @@ -65,9 +69,9 @@ export function SoundsTab() { drums: 'drum-machines', synths: 'Synths', user: 'User', + importSounds: 'import-sounds', }} > - settingsMap.setKey('soundsFilter', 'user')} />
@@ -101,7 +105,55 @@ export function SoundsTab() { ); })} - {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''} + {!soundEntries.length && soundsFilter === 'importSounds' ? ( +
+ settingsMap.setKey('soundsFilter', 'user')} /> +

+ To import sounds into strudel, they must be contained{' '} + + within a folder or subfolder + + . The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or + soundbanks (see diagram below).{' '} +

+
+              {`└─ samples <-- import this folder
+   ├─ swoop
+   │  ├─ swoopshort.wav
+   │  ├─ swooplong.wav
+   │  └─ swooptight.wav
+   └─ smash
+      ├─ smashhigh.wav
+      ├─ smashlow.wav
+      └─ smashmiddle.wav`}
+            
+

+ 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! +

+

+ For more information, and other ways to use your own sounds in strudel,{' '} + + check out the docs + + ! +

+

Preview Sounds

+
+              n("0 1 2 3 4 5").s("sample-name")
+            
+

+ 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. +

+
+ ) : ( + '' + )} + {!soundEntries.length && soundsFilter !== 'importSounds' + ? 'No custom sounds loaded in this pattern (yet).' + : ''}
); diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx index f391dd6d9..6c3bc3283 100644 --- a/website/src/repl/components/textbox/Textbox.jsx +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -3,7 +3,10 @@ import cx from '@src/cx.mjs'; export function Textbox({ onChange, className, ...inputProps }) { return ( onChange(e.target.value)} {...inputProps} /> diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 70ccd8f8f..5fc62c576 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -54,25 +54,23 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; const blob = soundFile.blob; - // Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted - if (typeof blob === 'string') { - const soundPaths = sounds.get(parentDirectory) ?? new Set(); - soundPaths.add(blob); - sounds.set(parentDirectory, soundPaths); - return; - } - return blobToDataUrl(blob).then((soundPath) => { - const soundPaths = sounds.get(parentDirectory) ?? new Set(); - soundPaths.add(soundPath); - sounds.set(parentDirectory, soundPaths); + const titlePathMap = sounds.get(parentDirectory) ?? new Map(); + + titlePathMap.set(title, soundPath); + + sounds.set(parentDirectory, titlePathMap); return; }); }), ) .then(() => { - sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); + sounds.forEach((titlePathMap, key) => { + const value = Array.from(titlePathMap.keys()) + .sort((a, b) => { + return a.localeCompare(b); + }) + .map((title) => titlePathMap.get(title)); registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ba8616272..f0895aaa4 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -14,7 +14,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, } from '@strudel/webaudio'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -28,7 +28,7 @@ import { setViewingPatternData, } from '../user_pattern_utils.mjs'; import { superdirtOutput } from '@strudel/osc/superdirtoutput'; -import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; +import { audioEngineTargets } from '../settings.mjs'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode } = settingsMap.get(); +const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick(); + audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); @@ -159,20 +159,6 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_settings]); - // on first load, set stored audio device if possible - useEffect(() => { - const { audioDeviceName } = _settings; - if (audioDeviceName !== defaultAudioDeviceName) { - getAudioDevices().then((devices) => { - const deviceID = devices.get(audioDeviceName); - if (deviceID == null) { - return; - } - setAudioDevice(deviceID); - }); - } - }, []); - // // UI Actions // diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..f49dd568f 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; -import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; +import { settingPatterns } from '../settings.mjs'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,38 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; -export const getAudioDevices = async () => { - await navigator.mediaDevices.getUserMedia({ audio: true }); - let mediaDevices = await navigator.mediaDevices.enumerateDevices(); - mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); - const devicesMap = new Map(); - devicesMap.set(defaultAudioDeviceName, ''); - mediaDevices.forEach((device) => { - devicesMap.set(device.label, device.deviceId); - }); - return devicesMap; -}; - -export const setAudioDevice = async (id) => { - let audioCtx = getAudioContext(); - if (audioCtx.sinkId === id) { - return; - } - await audioCtx.suspend(); - await audioCtx.close(); - audioCtx = setDefaultAudioContext(); - await audioCtx.resume(); - const isValidID = (id ?? '').length > 0; - if (isValidID) { - try { - await audioCtx.setSinkId(id); - } catch { - logger('failed to set audio interface', 'warning'); - } - } - initializeAudioOutput(); -}; - export function setVersionDefaultsFrom(code) { try { const metadata = getMetadata(code); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index cd3d89366..1b4ba33f9 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -3,8 +3,6 @@ import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; -export const defaultAudioDeviceName = 'System Standard'; - export const audioEngineTargets = { webaudio: 'webaudio', osc: 'osc', @@ -36,10 +34,10 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: defaultAudioDeviceName, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, + maxPolyphony: 128, }; let search = null;