Compare commits

..

1 Commits

Author SHA1 Message Date
alex 3568f0e437 fix ops to work with lists 2025-04-06 20:04:54 +01:00
69 changed files with 492 additions and 708 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
+1 -2
View File
@@ -71,7 +71,6 @@
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"lerna": "^8.1.9", "lerna": "^8.1.9",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"vitest": "^3.0.4", "vitest": "^3.0.4"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -3
View File
@@ -11,9 +11,7 @@ export const flashField = StateField.define({
for (let e of tr.effects) { for (let e of tr.effects) {
if (e.is(setFlash)) { if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) { if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
});
flash = Decoration.set([mark.range(0, tr.newDoc.length)]); flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else { } else {
flash = Decoration.set([]); flash = Decoration.set([]);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "1.2.0", "version": "1.1.0",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+32 -24
View File
@@ -5,15 +5,16 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext.mjs'; import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs'; import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
import redText, { settings as redTextSettings } from './themes/red-text.mjs'; import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
import aura, { settings as auraSettings } from './themes/aura.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 darcula, { settings as darculaSettings } from './themes/darcula.mjs';
import dracula, { settings as draculaSettings } from './themes/dracula.mjs'; import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.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 eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs'; import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs'; import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
@@ -31,47 +32,52 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs'; import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs'; import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.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 xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
import { setTheme } from '@strudel/draw'; import { setTheme } from '@strudel/draw';
export const themes = { export const themes = {
strudelTheme, strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy, algoboy,
androidstudio,
atomone, atomone,
aura, aura,
bbedit,
blackscreen,
bluescreen,
CutiePi,
darcula, darcula,
dracula, dracula,
// todo: optimize
// bespin,
//abcdef,
androidstudio,
duotoneDark, duotoneDark,
eclipse,
githubDark, githubDark,
githubLight, CutiePi,
greenText,
gruvboxDark, gruvboxDark,
gruvboxLight,
sonicPink,
materialDark, materialDark,
materialLight,
monokai,
noctisLilac,
nord, nord,
redText, monokai,
solarizedDark, solarizedDark,
solarizedLight,
sublime, sublime,
teletext,
tokyoNight, tokyoNight,
tokyoNightDay,
tokyoNightStorm, tokyoNightStorm,
vscodeDark, vscodeDark,
//xcodeDark,
// LIGHT
bbedit,
//duotoneLight,
eclipse,
githubLight,
gruvboxLight,
materialLight,
vscodeLight, vscodeLight,
whitescreen, noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight, xcodeLight,
}; };
@@ -82,19 +88,21 @@ export const settings = {
whitescreen: whitescreenSettings, whitescreen: whitescreenSettings,
teletext: teletextSettings, teletext: teletextSettings,
algoboy: algoboySettings, algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: abcdefSettings,
androidstudio: androidstudioSettings, androidstudio: androidstudioSettings,
atomone: atomOneSettings, atomone: atomOneSettings,
aura: auraSettings, aura: auraSettings,
bbedit: bbeditSettings, bbedit: bbeditSettings,
bespin: bespinSettings,
darcula: darculaSettings, darcula: darculaSettings,
dracula: draculaSettings, dracula: draculaSettings,
duotoneLight: duotoneLightSettings,
duotoneDark: duotoneDarkSettings, duotoneDark: duotoneDarkSettings,
eclipse: eclipseSettings, eclipse: eclipseSettings,
CutiePi: CutiePiSettings, CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
githubLight: githubLightSettings, githubLight: githubLightSettings,
githubDark: githubDarkSettings, githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings, gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings, gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings, materialDark: materialDarkSettings,
@@ -102,7 +110,6 @@ export const settings = {
noctisLilac: noctisLilacSettings, noctisLilac: noctisLilacSettings,
nord: nordSettings, nord: nordSettings,
monokai: monokaiSettings, monokai: monokaiSettings,
redText: redTextSettings,
solarizedLight: solarizedLightSettings, solarizedLight: solarizedLightSettings,
solarizedDark: solarizedDarkSettings, solarizedDark: solarizedDarkSettings,
sublime: sublimeSettings, sublime: sublimeSettings,
@@ -111,6 +118,7 @@ export const settings = {
vscodeDark: vscodeDarkSettings, vscodeDark: vscodeDarkSettings,
vscodeLight: vscodeLightSettings, vscodeLight: vscodeLightSettings,
xcodeLight: xcodeLightSettings, xcodeLight: xcodeLightSettings,
//xcodeDark: xcodeDarkSettings,
tokyoNightDay: tokyoNightDaySettings, tokyoNightDay: tokyoNightDaySettings,
}; };
+55
View File
@@ -0,0 +1,55 @@
/**
* @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' },
],
});
-1
View File
@@ -54,7 +54,6 @@ export default createTheme({
tag: [t.keyword, t.tagName, t.arithmeticOperator], tag: [t.keyword, t.tagName, t.arithmeticOperator],
color: palette[1], 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.function(t.variableName), t.propertyName], color: palette[0] },
{ tag: t.atom, color: palette[1] }, { tag: t.atom, color: palette[1] },
], ],
-2
View File
@@ -38,14 +38,12 @@ export default createTheme({
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: 'hsl(207, 82%, 66%)', 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.tagName, t.heading], color: '#e06c75' },
{ tag: t.comment, color: '#54636D' }, { tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
{ tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
{ tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' },
{ tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
], ],
}); });
+39
View File
@@ -0,0 +1,39 @@
// 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' },
],
});
-39
View File
@@ -1,39 +0,0 @@
/**
* @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] },
],
});
-39
View File
@@ -1,39 +0,0 @@
/**
* @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] },
],
});
-39
View File
@@ -1,39 +0,0 @@
/**
* @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] },
],
});
+3 -1
View File
@@ -5,11 +5,14 @@ export const settings = {
background: '#222', background: '#222',
lineBackground: '#22222299', lineBackground: '#22222299',
foreground: '#fff', foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00', caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050', lineHighlight: '#00000050',
gutterBackground: 'transparent', gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966', gutterForeground: '#8a919966',
}; };
@@ -17,7 +20,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
{ tag: t.labelName, color: '#89ddff' }, { tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' }, { tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' }, { tag: t.operator, color: '#89ddff' },
+34
View File
@@ -0,0 +1,34 @@
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' },
],
});
-29
View File
@@ -1780,32 +1780,3 @@ export const as = register('as', (mapping, pat) => {
return v; 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,
);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/core", "name": "@strudel/core",
"version": "1.2.0", "version": "1.1.0",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+3 -1
View File
@@ -21,6 +21,7 @@ import {
numeralArgs, numeralArgs,
parseNumeral, parseNumeral,
pairs, pairs,
listifyOp,
} from './util.mjs'; } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -1049,7 +1050,8 @@ function _composeOp(a, b, func) {
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
// generate methods to do what and how // generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) { for (const [what, [_op, preprocess]] of Object.entries(composers)) {
const op = listifyOp(_op);
// make plain version, e.g. pat._add(value) adds that plain value // make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat // to all the values in pat
Pattern.prototype['_' + what] = function (value) { Pattern.prototype['_' + what] = function (value) {
+10
View File
@@ -466,6 +466,16 @@ export function getCurrentKeyboardState() {
return { ...keyState }; // Return a shallow copy of the key state object return { ...keyState }; // Return a shallow copy of the key state object
} }
export function listifyOp(op) {
return function (a, b) {
if (!Array.isArray(a)) {
return op(a, Array.isArray(b) ? b[0] : b);
}
b = Array.isArray(b) ? b : [b];
return a.map((x, i) => (i < b.length ? op(x, b[i]) : x));
};
}
// Floating point versions, see Fraction for rational versions // Floating point versions, see Fraction for rational versions
// // greatest common divisor // // greatest common divisor
// export const gcd = function (x, y, ...z) { // export const gcd = function (x, y, ...z) {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/csound", "name": "@strudel/csound",
"version": "1.2.1", "version": "1.1.0",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/draw", "name": "@strudel/draw",
"version": "1.2.0", "version": "1.1.0",
"description": "Helpers for drawing with Strudel", "description": "Helpers for drawing with Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/gamepad", "name": "@strudel/gamepad",
"version": "1.2.0", "version": "1.1.0",
"description": "Gamepad Inputs for strudel", "description": "Gamepad Inputs for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,3 +35,4 @@
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"name": "hs2js", "name": "hs2js",
"version": "0.2.0", "version": "0.1.0",
"private": true,
"description": "Experimental Haskell in JavaScript interpreter", "description": "Experimental Haskell in JavaScript interpreter",
"main": "src/index.mjs", "main": "src/index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/hydra", "name": "@strudel/hydra",
"version": "1.2.0", "version": "1.1.0",
"description": "Hydra integration for strudel", "description": "Hydra integration for strudel",
"main": "hydra.mjs", "main": "hydra.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/midi", "name": "@strudel/midi",
"version": "1.2.1", "version": "1.1.0",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mini", "name": "@strudel/mini",
"version": "1.2.0", "version": "1.1.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/motion", "name": "@strudel/motion",
"version": "1.2.0", "version": "1.1.0",
"description": "DeviceMotion API for strudel", "description": "DeviceMotion API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,3 +35,4 @@
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mqtt", "name": "@strudel/mqtt",
"version": "1.2.0", "version": "1.1.0",
"description": "MQTT API for strudel", "description": "MQTT API for strudel",
"main": "mqtt.mjs", "main": "mqtt.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -37,7 +37,7 @@ function connect() {
export function parseControlsFromHap(hap, cps) { export function parseControlsFromHap(hap, cps) {
hap.ensureObjectValue(); hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf(); const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf() / cps; const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value); const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers // make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n)); controls.n && (controls.n = parseNumeral(controls.n));
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/osc", "name": "@strudel/osc",
"version": "1.2.0", "version": "1.1.0",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"type": "module", "type": "module",
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/reference", "name": "@strudel/reference",
"version": "1.2.0", "version": "1.1.0",
"description": "Headless reference of all strudel functions", "description": "Headless reference of all strudel functions",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -31,6 +31,8 @@
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
},
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11"
} }
+4
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@@ -10,6 +11,9 @@ export default defineConfig({
formats: ['es'], formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs' })[ext],
}, },
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext', target: 'esnext',
}, },
}); });
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/repl", "name": "@strudel/repl",
"version": "1.2.1", "version": "1.1.0",
"description": "Strudel REPL as a Web Component", "description": "Strudel REPL as a Web Component",
"module": "index.mjs", "module": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -46,7 +46,6 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -2
View File
@@ -1,12 +1,11 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/sampler", "name": "@strudel/sampler",
"version": "0.2.0", "version": "0.1.0",
"description": "", "description": "",
"keywords": [ "keywords": [
"tidalcycles", "tidalcycles",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/serial", "name": "@strudel/serial",
"version": "1.2.0", "version": "1.1.0",
"description": "Webserial API for strudel", "description": "Webserial API for strudel",
"main": "serial.mjs", "main": "serial.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/soundfonts", "name": "@strudel/soundfonts",
"version": "1.2.1", "version": "1.1.0",
"description": "Soundsfont support for strudel", "description": "Soundsfont support for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
-1
View File
@@ -212,7 +212,6 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.onended = () => { constantNode.onended = () => {
onComplete(); onComplete();
}; };
return constantNode;
} }
const mod = (freq, range = 1, type = 'sine') => { const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext(); const ctx = getAudioContext();
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "superdough", "name": "superdough",
"version": "1.2.1", "version": "1.1.0",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -32,8 +32,7 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": { "devDependencies": {
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
}, },
"dependencies": { "dependencies": {
"nanostores": "^0.11.3" "nanostores": "^0.11.3"
+1 -3
View File
@@ -344,9 +344,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
}; };
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (endTime) => { const stop = (endTime, playWholeBuffer) => {};
bufferSource.stop(endTime);
};
const handle = { node: out, bufferSource, stop }; const handle = { node: out, bufferSource, stop };
// cut groups // cut groups
+16 -91
View File
@@ -8,20 +8,12 @@ import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs'; import { clamp, nanFallback, _mod } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet'; import workletsUrl from './worklets.mjs?worker&url';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.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 const soundMap = map();
export function registerSound(key, onTrigger, data = {}) { export function registerSound(key, onTrigger, data = {}) {
@@ -29,16 +21,6 @@ export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data }); soundMap.setKey(key, { onTrigger, data });
} }
let gainCurveFunc = (val) => val;
export function applyGainCurve(val) {
return gainCurveFunc(val);
}
export function setGainCurve(newGainCurveFunc) {
gainCurveFunc = newGainCurveFunc;
}
function aliasBankMap(aliasMap) { function aliasBankMap(aliasMap) {
// Make all bank keys lower case for case insensitivity // Make all bank keys lower case for case insensitivity
for (const key in aliasMap) { for (const key in aliasMap) {
@@ -104,18 +86,6 @@ export function getSound(s) {
return soundMap.get()[s.toLowerCase()]; 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 = { const defaultDefaultValues = {
s: 'triangle', s: 'triangle',
gain: 0.8, gain: 0.8,
@@ -186,40 +156,18 @@ export function getAudioContextCurrentTime() {
let workletsLoading; let workletsLoading;
function loadWorklets() { function loadWorklets() {
if (!workletsLoading) { if (!workletsLoading) {
const audioCtx = getAudioContext(); workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
} }
return workletsLoading; return workletsLoading;
} }
// this function should be called on first user interaction (to avoid console warning) // this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) { export async function initAudio(options = {}) {
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; const { disableWorklets = false } = options;
setMaxPolyphony(maxPolyphony);
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; 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) { if (disableWorklets) {
logger('[superdough]: AudioWorklets disabled with disableWorklets'); logger('[superdough]: AudioWorklets disabled with disableWorklets');
return; return;
@@ -427,8 +375,6 @@ export function resetGlobalEffects() {
analysersData = {}; analysersData = {};
} }
let activeSoundSources = new Map();
export const superdough = async (value, t, hapDuration) => { export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext(); const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
@@ -526,32 +472,16 @@ export const superdough = async (value, t, hapDuration) => {
compressorRelease, compressorRelease,
} = value; } = value;
gain = applyGainCurve(nanFallback(gain, 1)); gain = 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 //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
let audioNodes = []; 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());
};
if (bank && s) { if (bank && s) {
s = `${bank}_${s}`; s = `${bank}_${s}`;
value.s = s; value.s = s;
@@ -563,15 +493,10 @@ export const superdough = async (value, t, hapDuration) => {
sourceNode = source(t, value, hapDuration); sourceNode = source(t, value, hapDuration);
} else if (getSound(s)) { } else if (getSound(s)) {
const { onTrigger } = getSound(s); const { onTrigger } = getSound(s);
const onEnded = () => { const soundHandle = await onTrigger(t, value, onended);
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, soundHandle); soundHandle.stop(t + hapDuration);
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -701,7 +626,6 @@ export const superdough = async (value, t, hapDuration) => {
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t); const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay); delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend);
} }
// reverb // reverb
let reverbSend; let reverbSend;
@@ -719,7 +643,6 @@ export const superdough = async (value, t, hapDuration) => {
} }
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
reverbSend = effectSend(post, reverbNode, room); reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
} }
// analyser // analyser
@@ -727,12 +650,14 @@ export const superdough = async (value, t, hapDuration) => {
if (analyze) { if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1); analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
} }
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
audioNodes = audioNodes.concat(chain);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
}; };
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {
+12 -24
View File
@@ -25,10 +25,6 @@ const getFrequencyFromValue = (value) => {
return Number(freq); return Number(freq);
}; };
function destroyAudioWorkletNode(node) {
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const noises = ['pink', 'white', 'brown', 'crackle']; const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -67,9 +63,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
stop: (endTime) => { stop: (releaseTime) => {},
stop(endTime);
},
}; };
}, },
{ type: 'synth', prebake: true }, { type: 'synth', prebake: true },
@@ -116,12 +110,10 @@ export function registerSynthSounds() {
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); webAudioTimeout(
let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); o.disconnect();
envGain.disconnect(); envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
@@ -131,11 +123,11 @@ export function registerSynthSounds() {
end, end,
); );
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return { return {
node: envGain, node: envGain,
stop: (time) => { stop: (time) => {},
timeoutNode.stop(time);
},
}; };
}, },
{ prebake: true, type: 'synth' }, { prebake: true, type: 'synth' },
@@ -177,12 +169,10 @@ export function registerSynthSounds() {
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); webAudioTimeout(
let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); o.disconnect();
envGain.disconnect(); envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
@@ -192,11 +182,11 @@ export function registerSynthSounds() {
end, end,
); );
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
return { return {
node: envGain, node: envGain,
stop: (time) => { stop: (time) => {},
timeoutNode.stop(time);
},
}; };
}, },
{ prebake: true, type: 'synth' }, { prebake: true, type: 'synth' },
@@ -239,9 +229,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
stop: (endTime) => { stop: (releaseTime) => {},
stop(endTime);
},
}; };
}, },
{ type: 'synth', prebake: true }, { type: 'synth', prebake: true },
+1 -2
View File
@@ -1,11 +1,10 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json'; import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
-3
View File
@@ -710,9 +710,6 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
} }
process(inputs, outputs, params) { process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) { if (currentTime <= params.begin[0]) {
return true; return true;
} }
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"name": "@strudel/tidal", "name": "@strudel/tidal",
"version": "0.2.0", "version": "0.1.0",
"private": true,
"description": "Experimental Tidal Code interpreter", "description": "Experimental Tidal Code interpreter",
"module": "tidal.mjs", "module": "tidal.mjs",
"repository": { "repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/tonal", "name": "@strudel/tonal",
"version": "1.2.0", "version": "1.1.0",
"description": "Tonal functions for strudel", "description": "Tonal functions for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/transpiler", "name": "@strudel/transpiler",
"version": "1.2.0", "version": "1.1.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -1,16 +0,0 @@
{
"name": "vite-plugin-bundle-audioworklet",
"main": "./vite-plugin-bundle-audioworklet.js",
"version": "0.1.1",
"description": "",
"keywords": [
"vite",
"audioworklet"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "MIT",
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
}
}
@@ -1,48 +0,0 @@
import { createLogger, build } from 'vite';
const end = '?audioworklet';
function bundleAudioWorkletPlugin() /* : PluginOption */ {
let viteConfig /* : UserConfig */;
return {
name: 'vite-plugin-bundle-audioworklet',
/* apply: 'build', */
enforce: 'post',
config(config) {
viteConfig = config;
},
async transform(_code, id) {
if (!id.endsWith(end)) {
return;
}
const entry = id.replace(end, '');
const quietLogger = createLogger();
quietLogger.info = () => undefined;
const output = await build({
configFile: false,
clearScreen: false,
customLogger: quietLogger,
build: {
lib: {
entry,
name: '_',
formats: ['iife'],
},
write: false,
},
});
if (!(output instanceof Array)) {
throw new Error('Expected output to be Array');
}
const iife = output[0].output[0].code;
const encoded = Buffer.from(iife, 'utf8').toString('base64');
return `export default "data:text/javascript;base64,${encoded}";`;
},
};
}
export default bundleAudioWorkletPlugin;
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/web", "name": "@strudel/web",
"version": "1.2.1", "version": "1.1.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.", "description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs", "module": "web.mjs",
"publishConfig": { "publishConfig": {
@@ -41,7 +41,6 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11", "vite": "^6.0.11"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -2
View File
@@ -2,12 +2,11 @@ import { defineConfig } from 'vite';
import { dependencies } from './package.json'; import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [bundleAudioWorkletPlugin()], plugins: [],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'web.mjs'), entry: resolve(__dirname, 'web.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/webaudio", "name": "@strudel/webaudio",
"version": "1.2.1", "version": "1.1.0",
"description": "Web Audio helpers for Strudel", "description": "Web Audio helpers for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/xen", "name": "@strudel/xen",
"version": "1.2.0", "version": "1.1.0",
"description": "Xenharmonic API for strudel", "description": "Xenharmonic API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+3 -23
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7 version: 2.2.7
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 3.0.4 specifier: 3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui': '@vitest/ui':
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4)
@@ -75,9 +75,6 @@ importers:
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.4.2
version: 3.4.2 version: 3.4.2
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:packages/vite-plugin-bundle-audioworklet
vitest: vitest:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
@@ -431,9 +428,6 @@ importers:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/sampler: packages/sampler:
dependencies: dependencies:
@@ -482,9 +476,6 @@ importers:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/tidal: packages/tidal:
dependencies: dependencies:
@@ -549,12 +540,6 @@ importers:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/vite-plugin-bundle-audioworklet:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/web: packages/web:
dependencies: dependencies:
'@strudel/core': '@strudel/core':
@@ -579,9 +564,6 @@ importers:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/webaudio: packages/webaudio:
dependencies: dependencies:
@@ -798,9 +780,6 @@ importers:
sharp: sharp:
specifier: ^0.33.5 specifier: ^0.33.5
version: 0.33.5 version: 0.33.5
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../packages/vite-plugin-bundle-audioworklet
workbox-window: workbox-window:
specifier: ^7.3.0 specifier: ^7.3.0
version: 7.3.0 version: 7.3.0
@@ -7582,6 +7561,7 @@ packages:
workbox-google-analytics@7.0.0: workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0: workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -10268,7 +10248,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)': '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
-46
View File
@@ -7871,52 +7871,6 @@ 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`] = ` exports[`runs examples > example "segment" example index 0 1`] = `
[ [
"[ 0/1 → 1/24 | note:40 ]", "[ 0/1 → 1/24 | note:40 ]",
+1 -3
View File
@@ -1,9 +1,7 @@
import { defineConfig } from 'vitest/config'; import { configDefaults, defineConfig } from 'vitest/config';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
/// <reference types="vitest" /> /// <reference types="vitest" />
export default defineConfig({ export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
test: { test: {
reporters: 'verbose', reporters: 'verbose',
isolate: false, isolate: false,
-2
View File
@@ -5,7 +5,6 @@ import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug'; import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls'; import rehypeUrls from 'rehype-urls';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind'; import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro'; import AstroPWA from '@vite-pwa/astro';
@@ -135,7 +134,6 @@ export default defineConfig({
site, site,
base, base,
vite: { vite: {
plugins: [bundleAudioWorkletPlugin()],
ssr: { ssr: {
// Example: Force a broken package to skip SSR processing, if needed // Example: Force a broken package to skip SSR processing, if needed
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
+1 -2
View File
@@ -73,7 +73,6 @@
"@vite-pwa/astro": "^0.5.0", "@vite-pwa/astro": "^0.5.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0"
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+6 -8
View File
@@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first
# Sound Banks # Sound Banks
If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc.. If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
We _could_ use them like this: We _could_ use them like this:
@@ -180,12 +180,11 @@ It assumes a `strudel.json` file to be present at the root of the repository:
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
``` ```
## From Disk via "Import Sounds Folder" ## From Disk via "Import Sounds"
If you don't want to upload your samples to the internet, you can also load them from your local disk. If you don't want to upload your samples to the internet, you can also load them from your local disk.
Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar. Go to the `sounds` tab in the REPL and press "import sounds".
Press the "import sounds folder" button and select a folder that contains audio files. Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
The folder you select can also contain subfolders with audio files.
Example: Example:
``` ```
@@ -201,13 +200,12 @@ Example:
``` ```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files. In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
If you select that `samples` folder, the `user` tab (next to the `import-sounds` tab) will then contain 2 new sounds: `swoop(3) smash(3)` If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`. The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
The samples within each sound use zero-based indexing in alphabetical order.
## From Disk via @strudel/sampler ## From Disk via @strudel/sampler
Instead of loading your samples into your browser with the "import sounds folder" button, you can also serve the samples from a local file server. Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler): The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh ```sh
+5 -7
View File
@@ -1,14 +1,12 @@
--- ---
import HeadCommon from '@components/HeadCommon.astro';
import { Udels } from '../../components/Udels/Udels.jsx'; 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;
--- ---
<html lang="en" class="m-0">
<head>
<HeadCommon />
<title>Strudel UDELS</title>
</head>
<body class="m-0"> <body class="m-0">
<Udels client:only="react" /> <Udels client:only="react" />
</body> </body>
</html>
@@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
import { SelectInput } from './SelectInput'; import { SelectInput } from './SelectInput';
import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map(); const initdevices = new Map();
@@ -22,7 +21,9 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
if (!devicesInitialized) { if (!devicesInitialized) {
return; return;
} }
const deviceID = devices.get(deviceName);
onChange(deviceName); onChange(deviceName);
setAudioDevice(deviceID);
}; };
const options = new Map(); const options = new Map();
Array.from(devices.keys()).forEach((deviceName) => { Array.from(devices.keys()).forEach((deviceName) => {
@@ -20,26 +20,10 @@ export default function ImportSoundsButton({ onComplete }) {
}); });
return ( return (
<div>
<label <label
style={{ alignItems: 'center', borderColor: 'red', border: 1 }} style={{ alignItems: 'center' }}
className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer" className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
> >
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6 mr-2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m0-3-3-3m0 0-3 3m3-3v11.25m6-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75"
/>
</svg>
<input <input
disabled={isUploading} disabled={isUploading}
ref={fileUploadRef} ref={fileUploadRef}
@@ -54,8 +38,7 @@ export default function ImportSoundsButton({ onComplete }) {
onChange(); onChange();
}} }}
/> />
{isUploading ? 'importing...' : 'import sounds folder'} {isUploading ? 'importing...' : 'import sounds'}
</label> </label>
</div>
); );
} }
@@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
<select <select
disabled={isDisabled} disabled={isDisabled}
onClick={onClick} onClick={onClick}
className="p-2 bg-background rounded-md text-foreground border-foreground" className="p-2 bg-background rounded-md text-foreground"
value={value ?? ''} value={value ?? ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
> >
@@ -1,12 +1,10 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror'; import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { isUdels } from '../../util.mjs'; import { isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio';
function Checkbox({ label, value, onChange, disabled = false }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
@@ -20,7 +18,7 @@ function Checkbox({ label, value, onChange, disabled = false }) {
function SelectInput({ value, options, onChange }) { function SelectInput({ value, options, onChange }) {
return ( return (
<select <select
className="p-2 bg-background rounded-md text-foreground border-foreground" className="p-2 bg-background rounded-md text-foreground"
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
> >
@@ -55,7 +53,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
); );
} }
function FormItem({ label, children, sublabel }) { function FormItem({ label, children }) {
return ( return (
<div className="grid gap-2"> <div className="grid gap-2">
<label>{label}</label> <label>{label}</label>
@@ -107,7 +105,6 @@ export function SettingsTab({ started }) {
audioDeviceName, audioDeviceName,
audioEngineTarget, audioEngineTarget,
togglePanelTrigger, togglePanelTrigger,
maxPolyphony,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -142,26 +139,6 @@ export function SettingsTab({ started }) {
}} }}
/> />
</FormItem> </FormItem>
<FormItem label="Maximum Polyphony">
<Textbox
min={1}
max={Infinity}
onBlur={(e) => {
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 ?? ''}
/>
</FormItem>
<FormItem label="Theme"> <FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} /> <SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem> </FormItem>
@@ -14,8 +14,6 @@ export function SoundsTab() {
const sounds = useStore(soundMap); const sounds = useStore(soundMap);
const { soundsFilter } = useSettings(); const { soundsFilter } = useSettings();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const soundEntries = useMemo(() => { const soundEntries = useMemo(() => {
if (!sounds) { if (!sounds) {
@@ -39,9 +37,6 @@ export function SoundsTab() {
if (soundsFilter === 'synths') { if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
} }
if (soundsFilter === 'importSounds') {
return [];
}
return filtered; return filtered;
}, [sounds, soundsFilter, search]); }, [sounds, soundsFilter, search]);
@@ -56,6 +51,7 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01); ref?.stop(getAudioContext().currentTime + 0.01);
}); });
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -69,9 +65,9 @@ export function SoundsTab() {
drums: 'drum-machines', drums: 'drum-machines',
synths: 'Synths', synths: 'Synths',
user: 'User', user: 'User',
importSounds: 'import-sounds',
}} }}
></ButtonGroup> ></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div> </div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2"> <div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
@@ -105,55 +101,7 @@ export function SoundsTab() {
</span> </span>
); );
})} })}
{!soundEntries.length && soundsFilter === 'importSounds' ? ( {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p>
To import sounds into strudel, they must be contained{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
within a folder or subfolder
</a>
. The best way to do this is to upload a samples folder containing subfolders of individual sounds or
soundbanks (see diagram below).{' '}
</p>
<pre className="bg-background" key={'sample-diagram'}>
{`└─ samples <-- import this folder
├─ swoop
│ ├─ swoopshort.wav
│ ├─ swooplong.wav
│ └─ swooptight.wav
└─ smash
├─ smashhigh.wav
├─ smashlow.wav
└─ smashmiddle.wav`}
</pre>
<p>
The name of a subfolder corresponds to the sound name under the user tab. Multiple samples within a
subfolder are all labelled with the same name, but can be accessed using .n( ) - remember sounds are
zero-indexed and in alphabetical order!
</p>
<p>
For more information, and other ways to use your own sounds in strudel,{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
check out the docs
</a>
!
</p>
<h3>Preview Sounds</h3>
<pre className="bg-background" key={'sample-preview'}>
n("0 1 2 3 4 5").s("sample-name")
</pre>
<p>
Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your
sample as it appears under the "user" tab.
</p>
</div>
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
</div> </div>
</div> </div>
); );
@@ -3,10 +3,7 @@ import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) { export function Textbox({ onChange, className, ...inputProps }) {
return ( return (
<input <input
className={cx( className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
'p-2 bg-background rounded-md border-foreground text-foreground placeholder-foreground',
className,
)}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
{...inputProps} {...inputProps}
/> />
+13 -11
View File
@@ -54,23 +54,25 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob; const blob = soundFile.blob;
// Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted
if (typeof blob === 'string') {
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(blob);
sounds.set(parentDirectory, soundPaths);
return;
}
return blobToDataUrl(blob).then((soundPath) => { return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map(); const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
titlePathMap.set(title, soundPath); sounds.set(parentDirectory, soundPaths);
sounds.set(parentDirectory, titlePathMap);
return; return;
}); });
}), }),
) )
.then(() => { .then(() => {
sounds.forEach((titlePathMap, key) => { sounds.forEach((soundPaths, key) => {
const value = Array.from(titlePathMap.keys()) const value = Array.from(soundPaths);
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample', type: 'sample',
+18 -4
View File
@@ -14,7 +14,7 @@ import {
resetLoadedSounds, resetLoadedSounds,
initAudioOnFirstClick, initAudioOnFirstClick,
} from '@strudel/webaudio'; } from '@strudel/webaudio';
import { setVersionDefaultsFrom } from './util.mjs'; import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra'; import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
@@ -28,7 +28,7 @@ import {
setViewingPatternData, setViewingPatternData,
} from '../user_pattern_utils.mjs'; } from '../user_pattern_utils.mjs';
import { superdirtOutput } from '@strudel/osc/superdirtoutput'; import { superdirtOutput } from '@strudel/osc/superdirtoutput';
import { audioEngineTargets } from '../settings.mjs'; import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { prebake } from './prebake.mjs'; import { prebake } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
@@ -36,11 +36,11 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers'; import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); const { latestCode } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady; let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); audioReady = initAudioOnFirstClick();
modulesLoading = loadModules(); modulesLoading = loadModules();
presets = prebake(); presets = prebake();
drawContext = getDrawContext(); drawContext = getDrawContext();
@@ -159,6 +159,20 @@ export function useReplContext() {
editorRef.current?.updateSettings(editorSettings); editorRef.current?.updateSettings(editorSettings);
}, [_settings]); }, [_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 // UI Actions
// //
+34 -2
View File
@@ -1,6 +1,6 @@
import { evalScope, hash2code, logger } from '@strudel/core'; import { evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns } from '../settings.mjs'; import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
import { setVersionDefaults } from '@strudel/webaudio'; import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
import { isTauri } from '../tauri.mjs'; import { isTauri } from '../tauri.mjs';
import './Repl.css'; import './Repl.css';
@@ -159,6 +159,38 @@ export const isUdels = () => {
return window.top?.location?.pathname.includes('udels'); 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) { export function setVersionDefaultsFrom(code) {
try { try {
const metadata = getMetadata(code); const metadata = getMetadata(code);
+3 -1
View File
@@ -3,6 +3,8 @@ import { useStore } from '@nanostores/react';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs'; import { isUdels } from './repl/util.mjs';
export const defaultAudioDeviceName = 'System Standard';
export const audioEngineTargets = { export const audioEngineTargets = {
webaudio: 'webaudio', webaudio: 'webaudio',
osc: 'osc', osc: 'osc',
@@ -34,10 +36,10 @@ export const defaultSettings = {
isPanelOpen: true, isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover togglePanelTrigger: 'click', //click | hover
userPatterns: '{}', userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128,
}; };
let search = null; let search = null;