mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39013c09ef | |||
| 9d425dfff6 |
@@ -1,37 +0,0 @@
|
|||||||
name: Build and Deploy to beta (warm.strudel.cc)
|
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: docker
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 9.12.2
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
# cache: "pnpm"
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
run: |
|
|
||||||
eval $(ssh-agent -s)
|
|
||||||
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
|
||||||
apt update && apt install -y rsync
|
|
||||||
mkdir ~/.ssh
|
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
|
||||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy to live (strudel.cc)
|
name: Build and Deploy
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ jobs:
|
|||||||
apt update && apt install -y rsync
|
apt update && apt install -y rsync
|
||||||
mkdir ~/.ssh
|
mkdir ~/.ssh
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
|
||||||
-14
@@ -45,20 +45,6 @@ tidal-drum-machines
|
|||||||
webaudiofontdata
|
webaudiofontdata
|
||||||
src-tauri/target
|
src-tauri/target
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### BEGIN Visual Studio Code ###
|
|
||||||
# Blanket, recursive exclude for .vscode directory and files
|
|
||||||
.vscode/**/*
|
|
||||||
# Unexclude specific files and directories within .vscode
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
### END Visual Studio Code ###
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# BEGIN JetBrains -> END JetBrains
|
# BEGIN JetBrains -> END JetBrains
|
||||||
|
|
||||||
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ https://strudel.cc/
|
|||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
1. Install [Node.js](https://nodejs.org/) 18 or newer
|
1. Install [Node.js](https://nodejs.org/)
|
||||||
2. Install [pnpm](https://pnpm.io/installation)
|
2. Install [pnpm](https://pnpm.io/installation)
|
||||||
3. Install dependencies by running the following command:
|
3. Install dependencies by running the following command:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script src="https://unpkg.com/@strudel/web@1.2.6"></script>
|
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
||||||
<button id="play">PLAY</button>
|
<button id="play">PLAY</button>
|
||||||
<script>
|
<script>
|
||||||
initStrudel({
|
initStrudel({
|
||||||
|
|||||||
@@ -12,16 +12,13 @@
|
|||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/codemirror": "1.2.6",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "1.2.5",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "1.2.5",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/mini": "1.2.5",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/soundfonts": "1.2.6",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tonal": "1.2.5",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "1.2.5",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel/webaudio": "1.2.6"
|
"@strudel/webaudio": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||||
samples('github:felixroos/samples')
|
samples('github:felixroos/samples')
|
||||||
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||||
// make it 22 edo
|
// make it 22 edo
|
||||||
@@ -28,12 +28,13 @@ samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/ti
|
|||||||
.mask("<0@4 1@12>/4")
|
.mask("<0@4 1@12>/4")
|
||||||
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
||||||
// wait for it...
|
// wait for it...
|
||||||
).fast(4/3)
|
).fast(2/3)
|
||||||
//.crush(6) // remove "//" if you dare`;
|
//.crush(6) // remove "//" if you dare`;
|
||||||
|
|
||||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||||
|
|
||||||
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('github:felixroos/samples')
|
||||||
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
addVoicings('hip', {
|
addVoicings('hip', {
|
||||||
m11: ['2M 3m 4P 7m'],
|
m11: ['2M 3m 4P 7m'],
|
||||||
@@ -60,14 +61,16 @@ stack(
|
|||||||
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.late("[0 .05]*2").late(12).fast(2)
|
.late("[0 .05]*2").late(12)
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const funk42 = `// froos - how to funk in 42 lines of code
|
export const funk42 = `// froos - how to funk in 42 lines of code
|
||||||
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
||||||
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||||
|
|
||||||
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('github:felixroos/samples')
|
||||||
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
setcps(.5)
|
setcps(.5)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<button id="c">C</button>
|
<button id="c">C</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { initStrudel, hush } from '@strudel/web';
|
import { initStrudel } from '@strudel/web';
|
||||||
initStrudel({
|
initStrudel({
|
||||||
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/web": "1.2.6"
|
"@strudel/web": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,5 @@
|
|||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel/webaudio": "workspace:*",
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*"
|
"@strudel/tonal": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,5 @@
|
|||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"vitest": "^3.0.4",
|
"vitest": "^3.0.4",
|
||||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
import { h } from './html';
|
import { h } from './html';
|
||||||
//TODO: fix tonal scale import
|
|
||||||
// import { Scale } from '@tonaljs/tonal';
|
|
||||||
// import { soundMap } from '@strudel/webaudio';
|
|
||||||
let soundMap = undefined;
|
|
||||||
import { complex } from '@strudel/tonal';
|
|
||||||
|
|
||||||
const escapeHtml = (str) => {
|
const escapeHtml = (str) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -80,53 +75,6 @@ const isValidDoc = (doc) => {
|
|||||||
const hasExcludedTags = (doc) =>
|
const hasExcludedTags = (doc) =>
|
||||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||||
|
|
||||||
export function bankCompletions() {
|
|
||||||
// TODO: FIX IMPORT
|
|
||||||
const soundDict = soundMap?.get() ?? {};
|
|
||||||
|
|
||||||
const banks = new Set();
|
|
||||||
for (const key of Object.keys(soundDict)) {
|
|
||||||
const [bank, suffix] = key.split('_');
|
|
||||||
if (suffix && bank) banks.add(bank);
|
|
||||||
}
|
|
||||||
return Array.from(banks)
|
|
||||||
.sort()
|
|
||||||
.map((name) => ({ label: name, type: 'bank' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
|
|
||||||
let scaleCompletions = [];
|
|
||||||
// try {
|
|
||||||
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
|
||||||
// } catch (e) {
|
|
||||||
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Valid mode values for voicing
|
|
||||||
const modeCompletions = [
|
|
||||||
{ label: 'below', type: 'mode' },
|
|
||||||
{ label: 'above', type: 'mode' },
|
|
||||||
{ label: 'duck', type: 'mode' },
|
|
||||||
{ label: 'root', type: 'mode' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Valid chord symbols from ireal dictionary plus empty string for major triads
|
|
||||||
const chordSymbols = ['', ...Object.keys(complex)].sort();
|
|
||||||
const chordSymbolCompletions = chordSymbols.map((symbol) => {
|
|
||||||
if (symbol === '') {
|
|
||||||
return {
|
|
||||||
label: 'major',
|
|
||||||
apply: '',
|
|
||||||
type: 'chord-symbol',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: symbol,
|
|
||||||
apply: symbol,
|
|
||||||
type: 'chord-symbol',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSynonymDoc = (doc, synonym) => {
|
export const getSynonymDoc = (doc, synonym) => {
|
||||||
const synonyms = doc.synonyms || [];
|
const synonyms = doc.synonyms || [];
|
||||||
const docLabel = getDocLabel(doc);
|
const docLabel = getDocLabel(doc);
|
||||||
@@ -165,299 +113,19 @@ const jsdocCompletions = (() => {
|
|||||||
return completions;
|
return completions;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// --- Handler functions for each context ---
|
export const strudelAutocomplete = (context) => {
|
||||||
const pitchNames = [
|
const word = context.matchBefore(/\w*/);
|
||||||
'C',
|
if (word.from === word.to && !context.explicit) return null;
|
||||||
'C#',
|
|
||||||
'Db',
|
|
||||||
'D',
|
|
||||||
'D#',
|
|
||||||
'Eb',
|
|
||||||
'E',
|
|
||||||
'E#',
|
|
||||||
'Fb',
|
|
||||||
'F',
|
|
||||||
'F#',
|
|
||||||
'Gb',
|
|
||||||
'G',
|
|
||||||
'G#',
|
|
||||||
'Ab',
|
|
||||||
'A',
|
|
||||||
'A#',
|
|
||||||
'Bb',
|
|
||||||
'B',
|
|
||||||
'B#',
|
|
||||||
'Cb',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Cached regex patterns for scaleHandler
|
|
||||||
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
|
|
||||||
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
|
|
||||||
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
|
|
||||||
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
|
|
||||||
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
|
|
||||||
|
|
||||||
function scaleHandler(context) {
|
|
||||||
// First check for scale context without quotes - block with empty completions
|
|
||||||
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
|
|
||||||
if (scaleNoQuotesContext) {
|
|
||||||
return {
|
|
||||||
from: scaleNoQuotesContext.to,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for after-colon context first (more specific)
|
|
||||||
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
|
|
||||||
if (scaleAfterColonContext) {
|
|
||||||
const text = scaleAfterColonContext.text;
|
|
||||||
const colonIdx = text.lastIndexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const fragment = text.slice(colonIdx + 1);
|
|
||||||
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
|
||||||
const options = filteredScales.map((s) => ({
|
|
||||||
...s,
|
|
||||||
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
|
|
||||||
}));
|
|
||||||
const from = scaleAfterColonContext.from + colonIdx + 1;
|
|
||||||
return {
|
|
||||||
from,
|
|
||||||
options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check for pre-colon context
|
|
||||||
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
|
|
||||||
if (scalePreColonContext) {
|
|
||||||
if (!scalePreColonContext.text.includes(':')) {
|
|
||||||
if (context.explicit) {
|
|
||||||
const text = scalePreColonContext.text;
|
|
||||||
const match = text.match(SCALE_PITCH_MATCH_REGEX);
|
|
||||||
const fragment = match ? match[0] : '';
|
|
||||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
|
||||||
const from = scalePreColonContext.to - fragment.length;
|
|
||||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
|
||||||
return { from, options };
|
|
||||||
} else {
|
|
||||||
return { from: scalePreColonContext.to, options: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached regex patterns for soundHandler
|
|
||||||
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
|
|
||||||
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
|
|
||||||
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
|
|
||||||
|
|
||||||
function soundHandler(context) {
|
|
||||||
// First check for sound context without quotes - block with empty completions
|
|
||||||
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
|
|
||||||
if (soundNoQuotesContext) {
|
|
||||||
return {
|
|
||||||
from: soundNoQuotesContext.to,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check for sound context with quotes - provide completions
|
|
||||||
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
|
|
||||||
if (!soundContext) return null;
|
|
||||||
|
|
||||||
const text = soundContext.text;
|
|
||||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
|
||||||
if (quoteIdx === -1) return null;
|
|
||||||
const inside = text.slice(quoteIdx + 1);
|
|
||||||
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
|
|
||||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
|
||||||
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
|
|
||||||
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
|
||||||
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
|
||||||
const from = soundContext.to - fragment.length;
|
|
||||||
return {
|
|
||||||
from,
|
|
||||||
options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached regex patterns for bankHandler
|
|
||||||
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
|
|
||||||
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
|
|
||||||
|
|
||||||
function bankHandler(context) {
|
|
||||||
// First check for bank context without quotes - block with empty completions
|
|
||||||
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
|
|
||||||
if (bankNoQuotesContext) {
|
|
||||||
return {
|
|
||||||
from: bankNoQuotesContext.to,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check for bank context with quotes - provide completions
|
|
||||||
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
|
|
||||||
if (!bankMatch) return null;
|
|
||||||
|
|
||||||
const text = bankMatch.text;
|
|
||||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
|
||||||
if (quoteIdx === -1) return null;
|
|
||||||
const inside = text.slice(quoteIdx + 1);
|
|
||||||
const fragment = inside;
|
|
||||||
let banks = bankCompletions();
|
|
||||||
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
|
|
||||||
const from = bankMatch.to - fragment.length;
|
|
||||||
return {
|
|
||||||
from,
|
|
||||||
options: filteredBanks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached regex patterns for modeHandler
|
|
||||||
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
|
|
||||||
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
|
|
||||||
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
|
|
||||||
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
|
|
||||||
|
|
||||||
function modeHandler(context) {
|
|
||||||
// First check for mode context without quotes - block with empty completions
|
|
||||||
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
|
|
||||||
if (modeNoQuotesContext) {
|
|
||||||
return {
|
|
||||||
from: modeNoQuotesContext.to,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for after-colon context first (more specific)
|
|
||||||
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
|
|
||||||
if (modeAfterColonContext) {
|
|
||||||
const text = modeAfterColonContext.text;
|
|
||||||
const colonIdx = text.lastIndexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const fragment = text.slice(colonIdx + 1);
|
|
||||||
// For anchor after colon, we can suggest pitch names
|
|
||||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
|
||||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
|
||||||
const from = modeAfterColonContext.from + colonIdx + 1;
|
|
||||||
return {
|
|
||||||
from,
|
|
||||||
options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check for pre-colon context
|
|
||||||
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
|
|
||||||
if (!modeContext) return null;
|
|
||||||
|
|
||||||
const text = modeContext.text;
|
|
||||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
|
||||||
if (quoteIdx === -1) return null;
|
|
||||||
const inside = text.slice(quoteIdx + 1);
|
|
||||||
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
|
|
||||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
|
||||||
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
|
|
||||||
const from = modeContext.to - fragment.length;
|
|
||||||
return {
|
|
||||||
from,
|
|
||||||
options: filteredModes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached regex patterns for chordHandler
|
|
||||||
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
|
|
||||||
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
|
|
||||||
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
|
|
||||||
|
|
||||||
function chordHandler(context) {
|
|
||||||
// First check for chord context without quotes - block with empty completions
|
|
||||||
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
|
|
||||||
if (chordNoQuotesContext) {
|
|
||||||
return {
|
|
||||||
from: chordNoQuotesContext.to,
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check for chord context with quotes - provide completions
|
|
||||||
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
|
|
||||||
if (!chordContext) return null;
|
|
||||||
|
|
||||||
const text = chordContext.text;
|
|
||||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
|
||||||
if (quoteIdx === -1) return null;
|
|
||||||
const inside = text.slice(quoteIdx + 1);
|
|
||||||
|
|
||||||
// Use same fragment matching as sound/mode for expressions like "<G Am>"
|
|
||||||
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
|
|
||||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
|
||||||
|
|
||||||
// Check if fragment contains any pitch name at start (for root + symbol)
|
|
||||||
let rootMatch = null;
|
|
||||||
let symbolFragment = fragment;
|
|
||||||
for (const pitch of pitchNames) {
|
|
||||||
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
|
|
||||||
rootMatch = pitch;
|
|
||||||
symbolFragment = fragment.slice(pitch.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootMatch) {
|
|
||||||
// We have a root, now complete chord symbols
|
|
||||||
const filteredSymbols = chordSymbolCompletions.filter((s) =>
|
|
||||||
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create completions that replace the entire chord, not just the symbol part
|
|
||||||
const options = filteredSymbols;
|
|
||||||
|
|
||||||
const from = chordContext.to - symbolFragment.length;
|
|
||||||
return { from, options };
|
|
||||||
} else {
|
|
||||||
// No root yet, complete with pitch names
|
|
||||||
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
|
||||||
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
|
|
||||||
const from = chordContext.to - fragment.length;
|
|
||||||
return { from, options };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached regex patterns for fallbackHandler
|
|
||||||
const FALLBACK_WORD_REGEX = /\w*/;
|
|
||||||
|
|
||||||
function fallbackHandler(context) {
|
|
||||||
const word = context.matchBefore(FALLBACK_WORD_REGEX);
|
|
||||||
if (word && word.from === word.to && !context.explicit) return null;
|
|
||||||
if (word) {
|
|
||||||
return {
|
return {
|
||||||
from: word.from,
|
from: word.from,
|
||||||
options: jsdocCompletions,
|
options: jsdocCompletions,
|
||||||
|
/* options: [
|
||||||
|
{ label: 'match', type: 'keyword' },
|
||||||
|
{ label: 'hello', type: 'variable', info: '(World)' },
|
||||||
|
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
||||||
|
], */
|
||||||
};
|
};
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = [
|
|
||||||
soundHandler,
|
|
||||||
bankHandler,
|
|
||||||
chordHandler,
|
|
||||||
scaleHandler,
|
|
||||||
modeHandler,
|
|
||||||
// this handler *must* be last
|
|
||||||
fallbackHandler,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const strudelAutocomplete = (context) => {
|
|
||||||
for (const handler of handlers) {
|
|
||||||
const result = handler(context);
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAutoCompletionEnabled = (enabled) =>
|
export const isAutoCompletionEnabled = (enabled) =>
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
import { indentWithTab, toggleLineComment } from '@codemirror/commands';
|
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||||
|
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { indentWithTab } from '@codemirror/commands';
|
||||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
drawSelection,
|
|
||||||
EditorView,
|
EditorView,
|
||||||
highlightActiveLine,
|
|
||||||
highlightActiveLineGutter,
|
highlightActiveLineGutter,
|
||||||
|
highlightActiveLine,
|
||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
|
drawSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { repl, registerControl } from '@strudel/core';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||||
import { cleanupDraw, Drawer } from '@strudel/draw';
|
|
||||||
|
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { basicSetup } from './basicSetup.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
|
import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
import { basicSetup } from './basicSetup.mjs';
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
@@ -95,8 +94,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
}),
|
}),
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
widgetPlugin,
|
widgetPlugin,
|
||||||
// indentOnInput(), // works without. already brought with javascript
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
// extension? bracketMatching(), // does not do anything
|
// bracketMatching(), // does not do anything
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
@@ -207,8 +206,7 @@ export class StrudelMirror {
|
|||||||
updateWidgets(this.editor, widgets);
|
updateWidgets(this.editor, widgets);
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
// if no painters are set (.onPaint was not called), then we only need
|
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
|
||||||
// the present moment (for highlighting)
|
|
||||||
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
|
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
|
||||||
this.drawer.setDrawTime(drawTime);
|
this.drawer.setDrawTime(drawTime);
|
||||||
// invalidate drawer after we've set the appropriate drawTime
|
// invalidate drawer after we've set the appropriate drawTime
|
||||||
@@ -247,33 +245,6 @@ export class StrudelMirror {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('start-repl', this.onStartRepl);
|
document.addEventListener('start-repl', this.onStartRepl);
|
||||||
|
|
||||||
// Handle global evaluation requests (e.g., from Vim :w)
|
|
||||||
this.onEvaluateRequest = (e) => {
|
|
||||||
try {
|
|
||||||
// Evaluate current editor on repl-evaluate
|
|
||||||
logger('[repl] evaluate via event');
|
|
||||||
this.evaluate();
|
|
||||||
e?.cancelable && e.preventDefault?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling repl-evaluate event', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('repl-evaluate', this.onEvaluateRequest);
|
|
||||||
document.addEventListener('repl-stop', this.onStopRequest);
|
|
||||||
|
|
||||||
// Toggle comments requested from Vim (gc)
|
|
||||||
this.onToggleComment = (e) => {
|
|
||||||
try {
|
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
|
||||||
// single line
|
|
||||||
toggleLineComment(this.editor);
|
|
||||||
e?.cancelable && e.preventDefault?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling repl-toggle-comment event', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('repl-toggle-comment', this.onToggleComment);
|
|
||||||
}
|
}
|
||||||
draw(haps, time, painters) {
|
draw(haps, time, painters) {
|
||||||
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
||||||
@@ -300,16 +271,6 @@ export class StrudelMirror {
|
|||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.scheduler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for global stop requests (e.g., from Vim :q)
|
|
||||||
onStopRequest = (e) => {
|
|
||||||
try {
|
|
||||||
this.stop();
|
|
||||||
e?.cancelable && e.preventDefault?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling repl-stop event', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
async toggle() {
|
async toggle() {
|
||||||
if (this.repl.scheduler.started) {
|
if (this.repl.scheduler.started) {
|
||||||
this.repl.stop();
|
this.repl.stop();
|
||||||
@@ -385,18 +346,11 @@ export class StrudelMirror {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCode(code) {
|
setCode(code) {
|
||||||
const changes = {
|
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
|
||||||
from: 0,
|
|
||||||
to: this.editor.state.doc.length,
|
|
||||||
insert: code,
|
|
||||||
};
|
|
||||||
this.editor.dispatch({ changes });
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
|
||||||
this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
|
|
||||||
this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
|
|
||||||
}
|
}
|
||||||
getCursorLocation() {
|
getCursorLocation() {
|
||||||
return this.editor.state.selection.main.head;
|
return this.editor.state.selection.main.head;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
|
||||||
import { Prec } from '@codemirror/state';
|
import { Prec } from '@codemirror/state';
|
||||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||||
// import { searchKeymap } from '@codemirror/search';
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
import { vim, Vim } from '@replit/codemirror-vim';
|
import { vim } from '@replit/codemirror-vim';
|
||||||
// import { vim } from './vim_test.mjs';
|
// import { vim } from './vim_test.mjs';
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
import { logger } from '@strudel/core';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
@@ -20,110 +19,6 @@ const vscodePlugin = ViewPlugin.fromClass(
|
|||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
|
|
||||||
// event 'repl-evaluate' that the editor listens for, and also simulate
|
|
||||||
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
|
||||||
// appears in the Console panel.
|
|
||||||
try {
|
|
||||||
if (Vim && typeof Vim.defineEx === 'function') {
|
|
||||||
// Map gc to toggle line comments by dispatching a custom event that our
|
|
||||||
// CodeMirror integration listens to. This avoids depending on Vim's
|
|
||||||
// internal actions and works with current selections/visual mode.
|
|
||||||
try {
|
|
||||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
|
||||||
const view = cm?.view || cm;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
document.dispatchEvent(ev);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('strudelToggleComment dispatch failed', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
|
|
||||||
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Vim gc mapping failed', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// :q to pause/stop
|
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
|
||||||
try {
|
|
||||||
const view = cm?.view || cm;
|
|
||||||
// First try dispatching our custom stop event, then fallback to Alt+.
|
|
||||||
let handled = false;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
handled = document.dispatchEvent(ev) === false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching repl-stop event', e);
|
|
||||||
}
|
|
||||||
if (!handled) {
|
|
||||||
const altDot = new KeyboardEvent('keydown', {
|
|
||||||
key: '.',
|
|
||||||
code: 'Period',
|
|
||||||
altKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(altDot);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching :q stop event', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// :w to evaluate
|
|
||||||
Vim.defineEx('write', 'w', (cm) => {
|
|
||||||
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
|
||||||
try {
|
|
||||||
view?.focus?.();
|
|
||||||
// Let the app know this came from Vim :w
|
|
||||||
try {
|
|
||||||
logger('[vim] :w — evaluating code');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error logging Vim :w evaluation', e);
|
|
||||||
}
|
|
||||||
// Dispatch a dedicated evaluate event first
|
|
||||||
let handled = false;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching repl-evaluate event', e);
|
|
||||||
}
|
|
||||||
if (handled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Try Ctrl+Enter first if not handled by custom event
|
|
||||||
const ctrlEnter = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
ctrlKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(ctrlEnter);
|
|
||||||
// If not handled (no handler called preventDefault), try Alt+Enter as
|
|
||||||
// fallback
|
|
||||||
if (!ctrlEnter.defaultPrevented) {
|
|
||||||
const altEnter = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
altKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(altEnter);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching :w evaluation event', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keymaps = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
emacs,
|
emacs,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -46,16 +46,10 @@
|
|||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@tonaljs/tonal": "^4.10.0",
|
"nanostores": "^0.11.3"
|
||||||
"nanostores": "^0.11.3",
|
|
||||||
"superdough": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-216
@@ -1288,151 +1288,6 @@ export const { fanchor } = registerControl('fanchor');
|
|||||||
*/
|
*/
|
||||||
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
|
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
|
||||||
// ['hpf'],
|
// ['hpf'],
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lprate
|
|
||||||
* @param {number | Pattern} rate rate in hertz
|
|
||||||
*/
|
|
||||||
export const { lprate } = registerControl('lprate');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cycle-synced rate of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpsync
|
|
||||||
* @param {number | Pattern} rate rate in cycles
|
|
||||||
*/
|
|
||||||
export const { lpsync } = registerControl('lpsync');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpdepth
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
*/
|
|
||||||
export const { lpdepth } = registerControl('lpdepth');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shape of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpshape
|
|
||||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
|
||||||
*/
|
|
||||||
export const { lpshape } = registerControl('lpshape');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DC offset of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpdc
|
|
||||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
|
||||||
*/
|
|
||||||
export const { lpdc } = registerControl('lpdc');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skew of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpskew
|
|
||||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
|
||||||
*/
|
|
||||||
export const { lpskew } = registerControl('lpskew');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bprate
|
|
||||||
* @param {number | Pattern} rate rate in hertz
|
|
||||||
*/
|
|
||||||
export const { bprate } = registerControl('bprate');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cycle-synced rate of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bpsync
|
|
||||||
* @param {number | Pattern} rate rate in cycles
|
|
||||||
*/
|
|
||||||
export const { bpsync } = registerControl('bpsync');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bpdepth
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
*/
|
|
||||||
export const { bpdepth } = registerControl('bpdepth');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shape of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bpshape
|
|
||||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
|
||||||
*/
|
|
||||||
export const { bpshape } = registerControl('bpshape');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DC offset of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bpdc
|
|
||||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
|
||||||
*/
|
|
||||||
export const { bpdc } = registerControl('bpdc');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skew of the LFO for the bandpass filter
|
|
||||||
*
|
|
||||||
* @name bpskew
|
|
||||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
|
||||||
*/
|
|
||||||
export const { bpskew } = registerControl('bpskew');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hprate
|
|
||||||
* @param {number | Pattern} rate rate in hertz
|
|
||||||
*/
|
|
||||||
export const { hprate } = registerControl('hprate');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cycle-synced rate of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hpsync
|
|
||||||
* @param {number | Pattern} rate rate in cycles
|
|
||||||
*/
|
|
||||||
export const { hpsync } = registerControl('hpsync');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hpdepth
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
*/
|
|
||||||
export const { hpdepth } = registerControl('hpdepth');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shape of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hpshape
|
|
||||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
|
||||||
*/
|
|
||||||
export const { hpshape } = registerControl('hpshape');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DC offset of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hpdc
|
|
||||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
|
||||||
*/
|
|
||||||
export const { hpdc } = registerControl('hpdc');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skew of the LFO for the highpass filter
|
|
||||||
*
|
|
||||||
* @name hpskew
|
|
||||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
|
||||||
*/
|
|
||||||
export const { hpskew } = registerControl('hpskew');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a vibrato to the frequency of the oscillator.
|
* Applies a vibrato to the frequency of the oscillator.
|
||||||
*
|
*
|
||||||
@@ -1895,64 +1750,17 @@ export const { semitone } = registerControl('semitone');
|
|||||||
// TODO: synth param
|
// TODO: synth param
|
||||||
export const { voice } = registerControl('voice');
|
export const { voice } = registerControl('voice');
|
||||||
// voicings // https://codeberg.org/uzu/strudel/issues/506
|
// voicings // https://codeberg.org/uzu/strudel/issues/506
|
||||||
/**
|
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
|
||||||
* The chord to voice
|
|
||||||
* @name chord
|
|
||||||
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
|
|
||||||
* @example
|
|
||||||
* chord("<Am C D F Am E Am E>").voicing()
|
|
||||||
**/
|
|
||||||
export const { chord } = registerControl('chord');
|
export const { chord } = registerControl('chord');
|
||||||
/**
|
// which dictionary to use for the voicings
|
||||||
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
|
|
||||||
*
|
|
||||||
* @name dictionary
|
|
||||||
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
|
|
||||||
* @example
|
|
||||||
* addVoicings('house', {
|
|
||||||
'': ['7 12 16', '0 7 16', '4 7 12'],
|
|
||||||
'm': ['0 3 7']
|
|
||||||
})
|
|
||||||
chord("<Am C D F Am E Am E>")
|
|
||||||
.dict('house').anchor(66)
|
|
||||||
.voicing().room(.5)
|
|
||||||
**/
|
|
||||||
export const { dictionary, dict } = registerControl('dictionary', 'dict');
|
export const { dictionary, dict } = registerControl('dictionary', 'dict');
|
||||||
/** The top note to align the voicing to. Defaults to c5
|
// the top note to align the voicing to, defaults to c5
|
||||||
*
|
|
||||||
* @name anchor
|
|
||||||
* @param {string | Pattern} anchorNote the note to align the voicings to
|
|
||||||
* @example
|
|
||||||
* anchor("<c4 g4 c5 g5>").chord("C").voicing()
|
|
||||||
**/
|
|
||||||
export const { anchor } = registerControl('anchor');
|
export const { anchor } = registerControl('anchor');
|
||||||
/**
|
// how the voicing is offset from the anchored position
|
||||||
* Sets how the voicing is offset from the anchored position
|
|
||||||
*
|
|
||||||
* @name offset
|
|
||||||
* @param {number | Pattern} shift the amount to shift the voicing up or down
|
|
||||||
* @example
|
|
||||||
* chord("<Am C D F Am E Am E>").offset("<0 1 2 3 4 5>") // alter the voicing each time
|
|
||||||
**/
|
|
||||||
export const { offset } = registerControl('offset');
|
export const { offset } = registerControl('offset');
|
||||||
/**
|
// how many octaves are voicing steps spread apart, defaults to 1
|
||||||
* How many octaves are voicing steps spread apart, defaults to 1
|
|
||||||
*
|
|
||||||
* @name octaves
|
|
||||||
* @param {number | Pattern} count the number of octaves
|
|
||||||
* @example
|
|
||||||
* chord("<Am C D F Am E Am E>").octaves("<2 4>").voicing()
|
|
||||||
**/
|
|
||||||
export const { octaves } = registerControl('octaves');
|
export const { octaves } = registerControl('octaves');
|
||||||
/**
|
// below = anchor note will be removed from the voicing, useful for melody harmonization
|
||||||
* Remove anchor note from the voicing. Useful for melody harmonization
|
|
||||||
*
|
|
||||||
* @name mode
|
|
||||||
* @param {string | Pattern} modeName one of {below | above | duck | root}
|
|
||||||
* @example
|
|
||||||
* mode("<below above duck root>").chord("C").voicing()
|
|
||||||
*
|
|
||||||
**/
|
|
||||||
export const { mode } = registerControl(['mode', 'anchor']);
|
export const { mode } = registerControl(['mode', 'anchor']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2512,24 +2320,6 @@ export const { miditouch } = registerControl('miditouch');
|
|||||||
// TODO: what is this?
|
// TODO: what is this?
|
||||||
export const { polyTouch } = registerControl('polyTouch');
|
export const { polyTouch } = registerControl('polyTouch');
|
||||||
|
|
||||||
/**
|
|
||||||
* The host to send open sound control messages to. Requires running the OSC bridge.
|
|
||||||
* @name oschost
|
|
||||||
* @param {string | Pattern} oschost e.g. 'localhost'
|
|
||||||
* @example
|
|
||||||
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
|
||||||
*/
|
|
||||||
export const { oschost } = registerControl('oschost');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The port to send open sound control messages to. Requires running the OSC bridge.
|
|
||||||
* @name oscport
|
|
||||||
* @param {number | Pattern} oscport e.g. 57120
|
|
||||||
* @example
|
|
||||||
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
|
||||||
*/
|
|
||||||
export const { oscport } = registerControl('oscport');
|
|
||||||
|
|
||||||
export const getControlName = (alias) => {
|
export const getControlName = (alias) => {
|
||||||
if (controlAlias.has(alias)) {
|
if (controlAlias.has(alias)) {
|
||||||
return controlAlias.get(alias);
|
return controlAlias.get(alias);
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ const right = function (n, x) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _bjorklund = function (n, x) {
|
const _bjork = function (n, x) {
|
||||||
const [ons, offs] = n;
|
const [ons, offs] = n;
|
||||||
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
|
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bjorklund = function (ons, steps) {
|
export const bjork = function (ons, steps) {
|
||||||
const inverted = ons < 0;
|
const inverted = ons < 0;
|
||||||
const absOns = Math.abs(ons);
|
const absOns = Math.abs(ons);
|
||||||
const offs = steps - absOns;
|
const offs = steps - absOns;
|
||||||
const ones = Array(absOns).fill([1]);
|
const ones = Array(absOns).fill([1]);
|
||||||
const zeros = Array(offs).fill([0]);
|
const zeros = Array(offs).fill([0]);
|
||||||
const result = _bjorklund([absOns, offs], [ones, zeros]);
|
const result = _bjork([absOns, offs], [ones, zeros]);
|
||||||
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
||||||
};
|
};
|
||||||
@@ -128,7 +128,7 @@ export const bjorklund = function (ons, steps) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const _euclidRot = function (pulses, steps, rotation) {
|
const _euclidRot = function (pulses, steps, rotation) {
|
||||||
const b = bjorklund(pulses, steps);
|
const b = bjork(pulses, steps);
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
return rotate(b, -rotation);
|
return rotate(b, -rotation);
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
|||||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bjork = register('bjork', function (euc, pat) {
|
export const e = register('e', function (euc, pat) {
|
||||||
if (!Array.isArray(euc)) {
|
if (!Array.isArray(euc)) {
|
||||||
euc = [euc];
|
euc = [euc];
|
||||||
}
|
}
|
||||||
@@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
|
|||||||
* .pan(sine.slow(8))
|
* .pan(sine.slow(8))
|
||||||
*/
|
*/
|
||||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||||
const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
|
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
||||||
return pat.struct(morphed).setSteps(steps);
|
return pat.struct(morphed).setSteps(steps);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/core",
|
"name": "@strudel/core",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -37,8 +37,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2574,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "<0 8> 1 2 3 4 5 6 7"
|
* "<0 8> 1 2 3 4 5 6 7"
|
||||||
* .scale("C2:major").note()
|
|
||||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||||
|
* .scale("C2:major").note()
|
||||||
*/
|
*/
|
||||||
export const { fastchunk, fastChunk } = register(
|
export const { fastchunk, fastChunk } = register(
|
||||||
['fastchunk', 'fastChunk'],
|
['fastchunk', 'fastChunk'],
|
||||||
@@ -2998,24 +2998,6 @@ export const extend = stepRegister('extend', function (factor, pat) {
|
|||||||
return pat.fast(factor).expand(factor);
|
return pat.fast(factor).expand(factor);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* *Experimental*
|
|
||||||
*
|
|
||||||
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
|
|
||||||
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
|
|
||||||
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
|
||||||
*
|
|
||||||
* TODO: find out how this function differs from extend
|
|
||||||
* @example
|
|
||||||
* stepcat(
|
|
||||||
* sound("bd bd - cp").replicate(2),
|
|
||||||
* sound("bd - sd -")
|
|
||||||
* ).pace(8)
|
|
||||||
*/
|
|
||||||
export const replicate = stepRegister('replicate', function (factor, pat) {
|
|
||||||
return pat.repeatCycles(factor).fast(factor).expand(factor);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { bjorklund } from '../euclid.mjs';
|
import { bjork } from '../euclid.mjs';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { fastcat } from '../pattern.mjs';
|
import { fastcat } from '../pattern.mjs';
|
||||||
|
|
||||||
describe('bjorklund', () => {
|
describe('bjork', () => {
|
||||||
it('should apply bjorklundlund to ons and steps', () => {
|
it('should apply bjorklund to ons and steps', () => {
|
||||||
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||||
expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||||
expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||||
expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||||
expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
@@ -23,10 +23,6 @@ export const tokenizeNote = (note) => {
|
|||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||||
|
|
||||||
export const getAccidentalsOffset = (accidentals) => {
|
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
@@ -34,7 +30,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = getAccidentalsOffset(acc);
|
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -38,8 +38,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,5 @@
|
|||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@tauri-apps/api": "^2.2.0"
|
"@tauri-apps/api": "^2.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme"
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# @strudel/draw
|
# @strudel/canvas
|
||||||
|
|
||||||
Helpers for drawing with the Canvas API and Strudel
|
Helpers for drawing with the Canvas API and Strudel
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm i @strudel/draw --save
|
npm i @strudel/canvas --save
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -84,18 +84,9 @@ Pattern.prototype.onPaint = function (painter) {
|
|||||||
state.controls.painters = [];
|
state.controls.painters = [];
|
||||||
}
|
}
|
||||||
state.controls.painters.push(painter);
|
state.controls.painters.push(painter);
|
||||||
return state;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO - Why isn't this pure deep copy not working?
|
|
||||||
// Pattern.prototype.onPaint = function (painter) {
|
|
||||||
// return this.withState((state) => {
|
|
||||||
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
|
|
||||||
// return new State(state.span, { ...state.controls, painters });
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
Pattern.prototype.getPainters = function () {
|
Pattern.prototype.getPainters = function () {
|
||||||
let painters = [];
|
let painters = [];
|
||||||
this.queryArc(0, 0, { painters });
|
this.queryArc(0, 0, { painters });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/draw",
|
"name": "@strudel/draw",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Helpers for drawing with Strudel",
|
"description": "Helpers for drawing with Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,8 +33,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,5 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://codeberg.org/uzu/strudel/issues"
|
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme"
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ const pattern = sequence([
|
|||||||
- D-Pad
|
- D-Pad
|
||||||
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
|
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
|
||||||
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
|
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
|
||||||
- Stick Buttons
|
|
||||||
- `l3`, `r3` (or `ls`, `rs`)
|
|
||||||
- Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`)
|
|
||||||
- System Buttons
|
|
||||||
- `start`, `back` (or uppercase `START`, `BACK`)
|
|
||||||
- Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`)
|
|
||||||
|
|
||||||
### Analog Sticks
|
### Analog Sticks
|
||||||
- Left Stick
|
- Left Stick
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ The gamepad module provides access to buttons and analog sticks as normalized si
|
|||||||
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
|
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
|
||||||
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
|
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
|
||||||
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
|
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
|
||||||
| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) |
|
|
||||||
| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) |
|
|
||||||
| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) |
|
|
||||||
| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) |
|
|
||||||
|
|
||||||
### Analog Sticks
|
### Analog Sticks
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ export const buttonMap = {
|
|||||||
rt: 7,
|
rt: 7,
|
||||||
back: 8,
|
back: 8,
|
||||||
start: 9,
|
start: 9,
|
||||||
l3: 10,
|
|
||||||
ls: 10,
|
|
||||||
r3: 11,
|
|
||||||
rs: 11,
|
|
||||||
u: 12,
|
u: 12,
|
||||||
up: 12,
|
up: 12,
|
||||||
d: 13,
|
d: 13,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/gamepad",
|
"name": "@strudel/gamepad",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Gamepad Inputs for strudel",
|
"description": "Gamepad Inputs for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,8 +33,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tree-sitter-haskell": "^0.23.1",
|
"tree-sitter-haskell": "^0.23.1",
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/hydra",
|
"name": "@strudel/hydra",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Hydra integration for strudel",
|
"description": "Hydra integration for strudel",
|
||||||
"main": "hydra.mjs",
|
"main": "hydra.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -40,8 +40,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-59
@@ -5,10 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, isPattern, logger, ref } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||||
import { noteToMidi, getControlName } from '@strudel/core';
|
import { noteToMidi, getControlName } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@@ -191,7 +190,7 @@ function mapCC(mapping, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sends a cc message to the given device on the given channel
|
// sends a cc message to the given device on the given channel
|
||||||
function sendCC(ccn, ccv, device, midichan, targetTime) {
|
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
throw new Error('expected ccv to be a number between 0 and 1');
|
||||||
}
|
}
|
||||||
@@ -199,23 +198,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
|
|||||||
throw new Error('expected ccn to be a number or a string');
|
throw new Error('expected ccn to be a number or a string');
|
||||||
}
|
}
|
||||||
const scaled = Math.round(ccv * 127);
|
const scaled = Math.round(ccv * 127);
|
||||||
scheduleAtTime(() => {
|
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||||
device.sendControlChange(ccn, scaled, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a program change message to the given device on the given channel
|
// sends a program change message to the given device on the given channel
|
||||||
function sendProgramChange(progNum, device, midichan, targetTime) {
|
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
|
||||||
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
||||||
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
|
||||||
device.sendProgramChange(progNum, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a sysex message to the given device on the given channel
|
// sends a sysex message to the given device on the given channel
|
||||||
function sendSysex(sysexid, sysexdata, device, targetTime) {
|
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
|
||||||
if (Array.isArray(sysexid)) {
|
if (Array.isArray(sysexid)) {
|
||||||
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
||||||
@@ -230,13 +225,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
|
|||||||
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all sysex bytes must be integers between 0 and 255');
|
throw new Error('all sysex bytes must be integers between 0 and 255');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
|
||||||
device.sendSysex(sysexid, sysexdata);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a NRPN message to the given device on the given channel
|
// sends a NRPN message to the given device on the given channel
|
||||||
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
|
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
|
||||||
if (Array.isArray(nrpnn)) {
|
if (Array.isArray(nrpnn)) {
|
||||||
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all nrpnn bytes must be integers between 0 and 255');
|
throw new Error('all nrpnn bytes must be integers between 0 and 255');
|
||||||
@@ -244,34 +237,28 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
|
|||||||
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
|
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
|
||||||
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
|
||||||
device.sendNRPN(nrpnn, nrpv, midichan);
|
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a pitch bend message to the given device on the given channel
|
// sends a pitch bend message to the given device on the given channel
|
||||||
function sendPitchBend(midibend, device, midichan, targetTime) {
|
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
|
||||||
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
||||||
throw new Error('expected midibend to be a number between -1 and 1');
|
throw new Error('expected midibend to be a number between -1 and 1');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
|
||||||
device.sendPitchBend(midibend, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a channel aftertouch message to the given device on the given channel
|
// sends a channel aftertouch message to the given device on the given channel
|
||||||
function sendAftertouch(miditouch, device, midichan, targetTime) {
|
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
|
||||||
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
||||||
throw new Error('expected miditouch to be a number between 0 and 1');
|
throw new Error('expected miditouch to be a number between 0 and 1');
|
||||||
}
|
}
|
||||||
|
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
|
||||||
scheduleAtTime(() => {
|
|
||||||
device.sendChannelAftertouch(miditouch, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a note message to the given device on the given channel
|
// sends a note message to the given device on the given channel
|
||||||
function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
|
||||||
if (note == null || note === '') {
|
if (note == null || note === '') {
|
||||||
throw new Error('note cannot be null or empty');
|
throw new Error('note cannot be null or empty');
|
||||||
}
|
}
|
||||||
@@ -281,12 +268,12 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
|||||||
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
|
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
|
||||||
throw new Error('duration must be a positive number');
|
throw new Error('duration must be a positive number');
|
||||||
}
|
}
|
||||||
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||||
|
device.playNote(midiNote, midichan, {
|
||||||
scheduleAtTime(() => {
|
time: timeOffsetString,
|
||||||
device.playNote(midiNote, midichan);
|
});
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -322,6 +309,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
let midiConfig = {
|
let midiConfig = {
|
||||||
// Default configuration values
|
// Default configuration values
|
||||||
isController: false, // Disable sending notes for midi controllers
|
isController: false, // Disable sending notes for midi controllers
|
||||||
|
latencyMs: 34, // Default latency to get audio engine to line up in ms
|
||||||
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
|
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
|
||||||
midichannel: 1, // Default MIDI channel
|
midichannel: 1, // Default MIDI channel
|
||||||
velocity: 0.9, // Default velocity
|
velocity: 0.9, // Default velocity
|
||||||
@@ -345,13 +333,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.onTrigger((hap, _currentTime, cps, targetTime) => {
|
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
|
const latencyMs = midiConfig.latencyMs;
|
||||||
|
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||||
|
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
||||||
|
|
||||||
// midi event values from hap with configurable defaults
|
// midi event values from hap with configurable defaults
|
||||||
let {
|
let {
|
||||||
note,
|
note,
|
||||||
@@ -387,7 +380,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
// if midimap is set, send a cc messages from defined controls
|
// if midimap is set, send a cc messages from defined controls
|
||||||
if (midicontrolMap.has(midimap)) {
|
if (midicontrolMap.has(midimap)) {
|
||||||
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
||||||
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime));
|
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
||||||
} else if (midimap !== 'default') {
|
} else if (midimap !== 'default') {
|
||||||
// Add warning when a non-existent midimap is specified
|
// Add warning when a non-existent midimap is specified
|
||||||
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
||||||
@@ -399,12 +392,12 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
// try to prevent glitching by subtracting noteOffsetMs from the duration length
|
// try to prevent glitching by subtracting noteOffsetMs from the duration length
|
||||||
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
|
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
|
||||||
|
|
||||||
sendNote(note, velocity, duration, device, midichan, targetTime);
|
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle program change
|
// Handle program change
|
||||||
if (progNum !== undefined) {
|
if (progNum !== undefined) {
|
||||||
sendProgramChange(progNum, device, midichan, targetTime);
|
sendProgramChange(progNum, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sysex
|
// Handle sysex
|
||||||
@@ -414,63 +407,53 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
// if sysexid is an array the first byte is 0x00
|
// if sysexid is an array the first byte is 0x00
|
||||||
|
|
||||||
if (sysexid !== undefined && sysexdata !== undefined) {
|
if (sysexid !== undefined && sysexdata !== undefined) {
|
||||||
sendSysex(sysexid, sysexdata, device, targetTime);
|
sendSysex(sysexid, sysexdata, device, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle control change
|
// Handle control change
|
||||||
if (ccv !== undefined && ccn !== undefined) {
|
if (ccv !== undefined && ccn !== undefined) {
|
||||||
sendCC(ccn, ccv, device, midichan, targetTime);
|
sendCC(ccn, ccv, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle NRPN non-registered parameter number
|
// Handle NRPN non-registered parameter number
|
||||||
if (nrpnn !== undefined && nrpv !== undefined) {
|
if (nrpnn !== undefined && nrpv !== undefined) {
|
||||||
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
|
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle midibend
|
// Handle midibend
|
||||||
if (midibend !== undefined) {
|
if (midibend !== undefined) {
|
||||||
sendPitchBend(midibend, device, midichan, targetTime);
|
sendPitchBend(midibend, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle miditouch
|
// Handle miditouch
|
||||||
if (miditouch !== undefined) {
|
if (miditouch !== undefined) {
|
||||||
sendAftertouch(miditouch, device, midichan, targetTime);
|
sendAftertouch(miditouch, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle midicmd
|
// Handle midicmd
|
||||||
if (hap.whole.begin + 0 === 0) {
|
if (hap.whole.begin + 0 === 0) {
|
||||||
// we need to start here because we have the timing info
|
// we need to start here because we have the timing info
|
||||||
scheduleAtTime(() => {
|
device.sendStart({ time: timeOffsetString });
|
||||||
device.sendStart();
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
if (['clock', 'midiClock'].includes(midicmd)) {
|
if (['clock', 'midiClock'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
device.sendClock({ time: timeOffsetString });
|
||||||
device.sendClock();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['start'].includes(midicmd)) {
|
} else if (['start'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
device.sendStart({ time: timeOffsetString });
|
||||||
device.sendStart();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['stop'].includes(midicmd)) {
|
} else if (['stop'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
device.sendStop({ time: timeOffsetString });
|
||||||
device.sendStop();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['continue'].includes(midicmd)) {
|
} else if (['continue'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
device.sendContinue({ time: timeOffsetString });
|
||||||
device.sendContinue();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (Array.isArray(midicmd)) {
|
} else if (Array.isArray(midicmd)) {
|
||||||
if (midicmd[0] === 'progNum') {
|
if (midicmd[0] === 'progNum') {
|
||||||
sendProgramChange(midicmd[1], device, midichan, targetTime);
|
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
|
||||||
} else if (midicmd[0] === 'cc') {
|
} else if (midicmd[0] === 'cc') {
|
||||||
if (midicmd.length === 2) {
|
if (midicmd.length === 2) {
|
||||||
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
|
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
} else if (midicmd[0] === 'sysex') {
|
} else if (midicmd[0] === 'sysex') {
|
||||||
if (midicmd.length === 3) {
|
if (midicmd.length === 3) {
|
||||||
const [_, id, data] = midicmd;
|
const [_, id, data] = midicmd;
|
||||||
sendSysex(id, data, device, targetTime);
|
sendSysex(id, data, device, timeOffsetString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/midi",
|
"name": "@strudel/midi",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mini",
|
"name": "@strudel/mini",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -38,8 +38,5 @@
|
|||||||
"peggy": "^4.2.0",
|
"peggy": "^4.2.0",
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ export class MondoParser {
|
|||||||
close_curly: /^\}/,
|
close_curly: /^\}/,
|
||||||
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
||||||
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
||||||
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
|
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
|
||||||
// dollar: /^\$/,
|
// dollar: /^\$/,
|
||||||
pipe: /^#/,
|
pipe: /^#/,
|
||||||
stack: /^[,$]/,
|
stack: /^[,$]/,
|
||||||
or: /^[|]/,
|
or: /^[|]/,
|
||||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||||
};
|
};
|
||||||
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
|
|
||||||
// matches next token
|
// matches next token
|
||||||
next_token(code, offset = 0) {
|
next_token(code, offset = 0) {
|
||||||
for (let type in this.token_types) {
|
for (let type in this.token_types) {
|
||||||
@@ -151,9 +150,9 @@ export class MondoParser {
|
|||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
desugar_ops(children, types) {
|
desugar_ops(children) {
|
||||||
while (true) {
|
while (true) {
|
||||||
let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value));
|
let opIndex = children.findIndex((child) => child.type === 'op');
|
||||||
if (opIndex === -1) break;
|
if (opIndex === -1) break;
|
||||||
const op = { type: 'plain', value: children[opIndex].value };
|
const op = { type: 'plain', value: children[opIndex].value };
|
||||||
if (opIndex === children.length - 1) {
|
if (opIndex === children.length - 1) {
|
||||||
@@ -264,10 +263,8 @@ export class MondoParser {
|
|||||||
// the type we've removed before splitting needs to be added back
|
// the type we've removed before splitting needs to be added back
|
||||||
children = [{ type: 'plain', value: type }, ...children];
|
children = [{ type: 'plain', value: type }, ...children];
|
||||||
}
|
}
|
||||||
// for each precendence group, call desugar_ops once
|
children = this.desugar_ops(children);
|
||||||
this.op_precedence.forEach((ops) => {
|
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
|
||||||
children = this.desugar_ops(children, ops);
|
|
||||||
});
|
|
||||||
children = this.desugar_pipes(children);
|
children = this.desugar_pipes(children);
|
||||||
return children;
|
return children;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -33,8 +33,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ describe('mondo sugar', () => {
|
|||||||
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
||||||
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
|
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
|
||||||
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
||||||
it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) bd)'));
|
|
||||||
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
||||||
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
|
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
|
||||||
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
|
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import {
|
|||||||
slow,
|
slow,
|
||||||
seq,
|
seq,
|
||||||
stepcat,
|
stepcat,
|
||||||
replicate,
|
extend,
|
||||||
expand,
|
expand,
|
||||||
pace,
|
pace,
|
||||||
chooseIn,
|
chooseIn,
|
||||||
degradeBy,
|
degradeBy,
|
||||||
silence,
|
silence,
|
||||||
bjork,
|
|
||||||
} from '@strudel/core';
|
} from '@strudel/core';
|
||||||
import { registerLanguage } from '@strudel/transpiler';
|
import { registerLanguage } from '@strudel/transpiler';
|
||||||
import { MondoRunner } from 'mondolang';
|
import { MondoRunner } from 'mondolang';
|
||||||
@@ -37,11 +36,10 @@ lib.square = (...args) => stepcat(...args).setSteps(1);
|
|||||||
lib.angle = (...args) => stepcat(...args).pace(1);
|
lib.angle = (...args) => stepcat(...args).pace(1);
|
||||||
lib['*'] = fast;
|
lib['*'] = fast;
|
||||||
lib['/'] = slow;
|
lib['/'] = slow;
|
||||||
lib['!'] = replicate;
|
lib['!'] = extend;
|
||||||
lib['@'] = expand;
|
lib['@'] = expand;
|
||||||
lib['%'] = pace;
|
lib['%'] = pace;
|
||||||
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
||||||
lib['&'] = bjork;
|
|
||||||
lib[':'] = tail;
|
lib[':'] = tail;
|
||||||
lib['..'] = range;
|
lib['..'] = range;
|
||||||
lib['def'] = () => silence;
|
lib['def'] = () => silence;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mondo",
|
"name": "@strudel/mondo",
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"description": "mondo notation for strudel",
|
"description": "mondo notation for strudel",
|
||||||
"main": "mondough.mjs",
|
"main": "mondough.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -40,8 +40,5 @@
|
|||||||
"mondo": "*",
|
"mondo": "*",
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/motion",
|
"name": "@strudel/motion",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "DeviceMotion API for strudel",
|
"description": "DeviceMotion API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,8 +33,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mqtt",
|
"name": "@strudel/mqtt",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "MQTT API for strudel",
|
"description": "MQTT API for strudel",
|
||||||
"main": "mqtt.mjs",
|
"main": "mqtt.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -34,8 +34,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-17
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
@@ -11,18 +13,19 @@ function connect() {
|
|||||||
if (!connection) {
|
if (!connection) {
|
||||||
// make sure this runs only once
|
// make sure this runs only once
|
||||||
connection = new Promise((resolve, reject) => {
|
connection = new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket('ws://localhost:8080');
|
const osc = new OSC();
|
||||||
ws.addEventListener('open', (event) => {
|
osc.open();
|
||||||
logger(`[osc] websocket connected`);
|
osc.on('open', () => {
|
||||||
resolve(ws);
|
const url = osc.options?.plugin?.socket?.url;
|
||||||
|
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
|
||||||
|
resolve(osc);
|
||||||
});
|
});
|
||||||
ws.addEventListener('close', (event) => {
|
osc.on('close', () => {
|
||||||
logger(`[osc] websocket closed`);
|
|
||||||
connection = undefined; // allows new connection afterwards
|
connection = undefined; // allows new connection afterwards
|
||||||
console.log('[osc] disconnected');
|
console.log('[osc] disconnected');
|
||||||
reject('OSC connection closed');
|
reject('OSC connection closed');
|
||||||
});
|
});
|
||||||
ws.addEventListener('error', (err) => reject(err));
|
osc.on('error', (err) => reject(err));
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
connection = undefined;
|
connection = undefined;
|
||||||
throw new Error('Could not connect to OSC server. Is it running?');
|
throw new Error('Could not connect to OSC server. Is it running?');
|
||||||
@@ -58,19 +61,15 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||||
const ws = await connect();
|
const osc = await connect();
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
|
||||||
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
|
||||||
|
|
||||||
if ('oschost' in hap.value) {
|
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
|
||||||
msg['host'] = hap.value['oschost'];
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
}
|
const bundle = new OSC.Bundle([message], ts);
|
||||||
if ('oscport' in hap.value) {
|
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||||
msg['port'] = hap.value['oscport'];
|
osc.send(bundle);
|
||||||
}
|
|
||||||
ws.send(JSON.stringify(msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.3.0",
|
"version": "1.2.10",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"bin": "./server.js",
|
"bin": "./server.js",
|
||||||
@@ -38,14 +38,10 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"osc": "^2.4.5",
|
"osc-js": "^2.4.1"
|
||||||
"ws": "^8.18.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+58
-47
@@ -6,59 +6,70 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
import { WebSocketServer } from 'ws';
|
const args = process.argv.slice(2);
|
||||||
import osc from 'osc';
|
function getArgValue(flag) {
|
||||||
|
const i = args.indexOf(flag);
|
||||||
|
if (i !== -1) {
|
||||||
|
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||||
|
if (nextIsFlag) return true;
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WS_PORT = 8080; // WebSocket server port
|
let udpClientPort = Number(getArgValue('--port')) || 57120;
|
||||||
const OSC_REMOTE_IP = '127.0.0.1';
|
let debug = Number(getArgValue('--debug')) || 0;
|
||||||
const OSC_REMOTE_PORT = 57120;
|
|
||||||
|
|
||||||
const udpPort = new osc.UDPPort({
|
const config = {
|
||||||
localAddress: '0.0.0.0',
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
localPort: 0,
|
udpServer: {
|
||||||
remoteAddress: OSC_REMOTE_IP,
|
host: 'localhost', // @param {string} Hostname of udp server to bind to
|
||||||
remotePort: OSC_REMOTE_PORT,
|
port: 57121, // @param {number} Port of udp client for messaging
|
||||||
|
// enabling the following line will receive tidal messages:
|
||||||
|
// port: 57120, // @param {number} Port of udp client for messaging
|
||||||
|
exclusive: false, // @param {boolean} Exclusive flag
|
||||||
|
},
|
||||||
|
udpClient: {
|
||||||
|
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
||||||
|
port: udpClientPort, // @param {number} Port of udp client for messaging
|
||||||
|
},
|
||||||
|
wsServer: {
|
||||||
|
host: 'localhost', // @param {string} Hostname of WebSocket server
|
||||||
|
port: 8080, // @param {number} Port of WebSocket server
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
osc.on('*', (message) => {
|
||||||
|
const { address, args } = message;
|
||||||
|
let str = '';
|
||||||
|
for (let i = 0; i < args.length; i += 2) {
|
||||||
|
str += `${args[i]}: ${args[i + 1]} `;
|
||||||
|
}
|
||||||
|
console.log(`${address} ${str}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
udpPort.open();
|
|
||||||
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
|
|
||||||
|
|
||||||
udpPort.on('error', (e) => {
|
|
||||||
console.log('Error: ', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({ port: WS_PORT });
|
|
||||||
console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
|
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
|
||||||
console.log('New WebSocket connection');
|
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
|
||||||
let osc_host = '127.0.0.1';
|
|
||||||
let osc_port = 57120;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
if ('host' in data) {
|
|
||||||
osc_host = data['host'];
|
|
||||||
}
|
|
||||||
if ('port' in data) {
|
|
||||||
osc_port = data['port'];
|
|
||||||
}
|
|
||||||
let msg = { address: data['address'], args: data['args'] };
|
|
||||||
if ('timestamp' in data) {
|
|
||||||
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
udpPort.send(msg, osc_host, osc_port);
|
osc.on('error', (message) => {
|
||||||
} catch (err) {
|
if (message.toString().includes('EADDRINUSE')) {
|
||||||
console.error('Error parsing message:', err);
|
console.log(`------ ERROR -------
|
||||||
|
a server is already running on port 57121! to stop it:
|
||||||
|
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
|
||||||
|
2. re-run the osc server
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
osc.open();
|
||||||
console.log('WebSocket connection closed');
|
|
||||||
});
|
console.log('osc client running on port', config.udpClient.port);
|
||||||
});
|
console.log('osc server running on port', config.udpServer.port);
|
||||||
|
console.log('websocket server running on port', config.wsServer.port);
|
||||||
|
if (debug) {
|
||||||
|
console.log('debug logs enabled. incoming messages will appear below');
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,8 +33,5 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.7",
|
"version": "1.2.6",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -48,8 +48,5 @@
|
|||||||
"@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:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,5 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cowsay": "^1.6.0"
|
"cowsay": "^1.6.0"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/serial",
|
"name": "@strudel/serial",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,8 +33,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/soundfonts",
|
"name": "@strudel/soundfonts",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -37,8 +37,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,24 +154,6 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
|||||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getParamLfo(audioContext, param, start, end, lfoValues) {
|
|
||||||
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
|
|
||||||
if (depth == null) {
|
|
||||||
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
|
|
||||||
depth = hasLFOParams ? defaultDepth : 0;
|
|
||||||
}
|
|
||||||
let lfo;
|
|
||||||
if (depth) {
|
|
||||||
lfo = getLfo(audioContext, start, end, {
|
|
||||||
depth,
|
|
||||||
dcoffset,
|
|
||||||
...getLfoInputs,
|
|
||||||
});
|
|
||||||
lfo.connect(param);
|
|
||||||
}
|
|
||||||
return lfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper utility for applying standard modulators to a parameter
|
// helper utility for applying standard modulators to a parameter
|
||||||
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
|
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
|
||||||
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
|
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
|
||||||
@@ -188,58 +170,55 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
|
|||||||
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
|
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
|
||||||
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
|
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
|
||||||
}
|
}
|
||||||
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
|
let lfo;
|
||||||
|
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
|
||||||
|
|
||||||
|
if (depth == null) {
|
||||||
|
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
|
||||||
|
depth = hasLFOParams ? defaultDepth : 0;
|
||||||
|
}
|
||||||
|
if (depth) {
|
||||||
|
lfo = getLfo(audioContext, start, end, {
|
||||||
|
depth,
|
||||||
|
dcoffset,
|
||||||
|
...getLfoInputs,
|
||||||
|
});
|
||||||
|
lfo.connect(param);
|
||||||
|
}
|
||||||
|
|
||||||
return { lfo, disconnect: () => lfo?.disconnect() };
|
return { lfo, disconnect: () => lfo?.disconnect() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFilter(context, start, end, params, cps) {
|
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
||||||
let {
|
const curve = 'exponential';
|
||||||
frequency,
|
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||||
anchor,
|
let filter;
|
||||||
env,
|
let frequencyParam;
|
||||||
type,
|
|
||||||
model,
|
|
||||||
q = 1,
|
|
||||||
drive = 0.69,
|
|
||||||
depth,
|
|
||||||
dcoffset = -0.5,
|
|
||||||
skew,
|
|
||||||
shape,
|
|
||||||
rate,
|
|
||||||
sync,
|
|
||||||
} = params;
|
|
||||||
let frequencyParam, filter;
|
|
||||||
if (model === 'ladder') {
|
if (model === 'ladder') {
|
||||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
|
||||||
frequencyParam = filter.parameters.get('frequency');
|
frequencyParam = filter.parameters.get('frequency');
|
||||||
} else {
|
} else {
|
||||||
filter = context.createBiquadFilter();
|
filter = context.createBiquadFilter();
|
||||||
filter.type = type;
|
filter.type = type;
|
||||||
filter.Q.value = q;
|
filter.Q.value = Q;
|
||||||
filter.frequency.value = frequency;
|
filter.frequency.value = frequency;
|
||||||
frequencyParam = filter.frequency;
|
frequencyParam = filter.frequency;
|
||||||
}
|
}
|
||||||
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
|
||||||
const [attack, decay, sustain, release] = getADSRValues(envelopeValues, 'exponential', [0.005, 0.14, 0, 0.1]);
|
|
||||||
// envelope is active when any of these values is set
|
|
||||||
const hasEnvelope = [...envelopeValues, env].some((v) => v !== undefined);
|
|
||||||
// Apply ADSR to filter frequency
|
|
||||||
if (hasEnvelope) {
|
|
||||||
env = nanFallback(env, 1, true);
|
|
||||||
anchor = nanFallback(anchor, 0, true);
|
|
||||||
const envAbs = Math.abs(env);
|
|
||||||
const offset = envAbs * anchor;
|
|
||||||
let min = clamp(2 ** -offset * frequency, 0, 20000);
|
|
||||||
let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000);
|
|
||||||
if (env < 0) [min, max] = [max, min];
|
|
||||||
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sync != null) {
|
// envelope is active when any of these values is set
|
||||||
rate = cps * sync;
|
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
|
||||||
|
// Apply ADSR to filter frequency
|
||||||
|
if (hasEnvelope !== undefined) {
|
||||||
|
fenv = nanFallback(fenv, 1, true);
|
||||||
|
fanchor = nanFallback(fanchor, 0, true);
|
||||||
|
const fenvAbs = Math.abs(fenv);
|
||||||
|
const offset = fenvAbs * fanchor;
|
||||||
|
let min = clamp(2 ** -offset * frequency, 0, 20000);
|
||||||
|
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
|
||||||
|
if (fenv < 0) [min, max] = [max, min];
|
||||||
|
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
|
||||||
|
return filter;
|
||||||
}
|
}
|
||||||
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
|
|
||||||
getParamLfo(context, frequencyParam, start, end, lfoValues);
|
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,11 +280,6 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
return vibratoOscillator;
|
return vibratoOscillator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) {
|
|
||||||
const currentTime = audioContext.currentTime;
|
|
||||||
webAudioTimeout(audioContext, callback, currentTime, targetTime);
|
|
||||||
}
|
|
||||||
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
||||||
// a bit of a hack, but it works very well :)
|
// a bit of a hack, but it works very well :)
|
||||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"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",
|
||||||
@@ -37,8 +37,5 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanostores": "^0.11.3"
|
"nanostores": "^0.11.3"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfoFromBank } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
@@ -23,35 +23,34 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSampleInfo(hapValue, bank) {
|
|
||||||
const { speed = 1.0 } = hapValue;
|
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
|
||||||
}
|
|
||||||
|
|
||||||
// takes hapValue and returns buffer + playbackRate.
|
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
|
||||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
|
||||||
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
|
||||||
if (resolveUrl) {
|
if (resolveUrl) {
|
||||||
sampleUrl = await resolveUrl(sampleUrl);
|
sampleUrl = await resolveUrl(sampleUrl);
|
||||||
}
|
}
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
const buffer = await loadBuffer(sampleUrl, ac, label);
|
||||||
|
return buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBufferPlaybackRate(hapValue, buffer, transpose) {
|
||||||
|
const { speed = 1.0 } = hapValue;
|
||||||
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
if (hapValue.unit === 'c') {
|
if (hapValue.unit === 'c') {
|
||||||
playbackRate = playbackRate * buffer.duration;
|
playbackRate = playbackRate * buffer.duration;
|
||||||
}
|
}
|
||||||
return { buffer, playbackRate };
|
return playbackRate;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// creates playback ready AudioBufferSourceNode from hapValue
|
// creates playback ready AudioBufferSourceNode from hapValue
|
||||||
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
export function getSampleBufferSource(hapValue, buffer,transpose) {
|
||||||
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
|
||||||
if (hapValue.speed < 0) {
|
if (hapValue.speed < 0) {
|
||||||
// should this be cached?
|
// should this be cached?
|
||||||
buffer = reverseBuffer(buffer);
|
buffer = reverseBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const bufferSource = ac.createBufferSource();
|
const bufferSource = ac.createBufferSource();
|
||||||
bufferSource.buffer = buffer;
|
bufferSource.buffer = buffer;
|
||||||
@@ -129,7 +128,9 @@ function githubPath(base, subpath = '') {
|
|||||||
let repo = components.length >= 2 ? components[1] : 'samples';
|
let repo = components.length >= 2 ? components[1] : 'samples';
|
||||||
let branch = components.length >= 3 ? components[2] : 'main';
|
let branch = components.length >= 3 ? components[2] : 'main';
|
||||||
let other = components.slice(3);
|
let other = components.slice(3);
|
||||||
other.push(subpath ? subpath : '');
|
if (subpath) {
|
||||||
|
other.push(subpath);
|
||||||
|
}
|
||||||
other = other.join('/');
|
other = other.join('/');
|
||||||
|
|
||||||
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
||||||
@@ -262,7 +263,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
export async function onTriggerSample(t, value, onended, bufferSrc) {
|
||||||
let {
|
let {
|
||||||
s,
|
s,
|
||||||
nudge = 0, // TODO: is this in seconds?
|
nudge = 0, // TODO: is this in seconds?
|
||||||
@@ -284,7 +285,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
// destructure adsr here, because the default should be different for synths and samples
|
// destructure adsr here, because the default should be different for synths and samples
|
||||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
|
|
||||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
const { bufferSource, sliceDuration, offset } = bufferSrc
|
||||||
|
|
||||||
// asny stuff above took too long?
|
// asny stuff above took too long?
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
@@ -346,14 +347,21 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBufferSrcFromBank(hapValue, bank, resolveUrl = undefined) {
|
||||||
|
const { transpose, url, label } = getCommonSampleInfoFromBank(hapValue, bank)
|
||||||
|
let buffer = await getSampleBuffer(label,url, resolveUrl);
|
||||||
|
return getSampleBufferSource(hapValue, buffer, transpose)
|
||||||
|
}
|
||||||
|
|
||||||
function registerSample(key, bank, params) {
|
function registerSample(key, bank, params) {
|
||||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
|
||||||
type: 'sample',
|
type: 'sample',
|
||||||
samples: bank,
|
samples: bank,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function registerSampleSource(key, bank, params) {
|
export function registerSampleSource(key, bank, params) {
|
||||||
const isWavetable = key.startsWith('wt_');
|
const isWavetable = key.startsWith('wt_');
|
||||||
if (isWavetable) {
|
if (isWavetable) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
@@ -146,6 +146,11 @@ let defaultDefaultValues = {
|
|||||||
gain: 0.8,
|
gain: 0.8,
|
||||||
postgain: 1,
|
postgain: 1,
|
||||||
density: '.03',
|
density: '.03',
|
||||||
|
ftype: '12db',
|
||||||
|
fanchor: 0,
|
||||||
|
resonance: 1,
|
||||||
|
hresonance: 1,
|
||||||
|
bandq: 1,
|
||||||
channels: [1, 2],
|
channels: [1, 2],
|
||||||
phaserdepth: 0.75,
|
phaserdepth: 0.75,
|
||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
@@ -410,7 +415,32 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
djf,
|
djf,
|
||||||
// filters
|
// filters
|
||||||
fanchor = getDefaultValue('fanchor'),
|
fanchor = getDefaultValue('fanchor'),
|
||||||
|
drive = 0.69,
|
||||||
release = 0,
|
release = 0,
|
||||||
|
// low pass
|
||||||
|
cutoff,
|
||||||
|
lpenv,
|
||||||
|
lpattack,
|
||||||
|
lpdecay,
|
||||||
|
lpsustain,
|
||||||
|
lprelease,
|
||||||
|
resonance = getDefaultValue('resonance'),
|
||||||
|
// high pass
|
||||||
|
hpenv,
|
||||||
|
hcutoff,
|
||||||
|
hpattack,
|
||||||
|
hpdecay,
|
||||||
|
hpsustain,
|
||||||
|
hprelease,
|
||||||
|
hresonance = getDefaultValue('hresonance'),
|
||||||
|
// band pass
|
||||||
|
bpenv,
|
||||||
|
bandf,
|
||||||
|
bpattack,
|
||||||
|
bpdecay,
|
||||||
|
bpsustain,
|
||||||
|
bprelease,
|
||||||
|
bandq = getDefaultValue('bandq'),
|
||||||
|
|
||||||
//phaser
|
//phaser
|
||||||
phaserrate: phaser,
|
phaserrate: phaser,
|
||||||
@@ -537,85 +567,55 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
|
|
||||||
//filter
|
//filter
|
||||||
const ftype = getFilterType(value.ftype);
|
const ftype = getFilterType(value.ftype);
|
||||||
|
if (cutoff !== undefined) {
|
||||||
if (value.cutoff !== undefined) {
|
let lp = () =>
|
||||||
const lpMap = {
|
createFilter(
|
||||||
frequency: 'cutoff',
|
ac,
|
||||||
q: 'resonance',
|
'lowpass',
|
||||||
attack: 'lpattack',
|
cutoff,
|
||||||
decay: 'lpdecay',
|
resonance,
|
||||||
sustain: 'lpsustain',
|
lpattack,
|
||||||
release: 'lprelease',
|
lpdecay,
|
||||||
env: 'lpenv',
|
lpsustain,
|
||||||
anchor: 'fanchor',
|
lprelease,
|
||||||
model: 'ftype',
|
lpenv,
|
||||||
drive: 'drive',
|
t,
|
||||||
rate: 'lprate',
|
end,
|
||||||
sync: 'lpsync',
|
fanchor,
|
||||||
depth: 'lpdepth',
|
ftype,
|
||||||
shape: 'lpshape',
|
drive,
|
||||||
dcoffset: 'lpdc',
|
);
|
||||||
skew: 'lpskew',
|
|
||||||
};
|
|
||||||
const lpParams = pickAndRename(value, lpMap);
|
|
||||||
lpParams.type = 'lowpass';
|
|
||||||
let lp = () => createFilter(ac, t, end, lpParams, cps);
|
|
||||||
chain.push(lp());
|
chain.push(lp());
|
||||||
if (ftype === '24db') {
|
if (ftype === '24db') {
|
||||||
chain.push(lp());
|
chain.push(lp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.hcutoff !== undefined) {
|
if (hcutoff !== undefined) {
|
||||||
const hpMap = {
|
let hp = () =>
|
||||||
frequency: 'hcutoff',
|
createFilter(
|
||||||
q: 'hresonance',
|
ac,
|
||||||
attack: 'hpattack',
|
'highpass',
|
||||||
decay: 'hpdecay',
|
hcutoff,
|
||||||
sustain: 'hpsustain',
|
hresonance,
|
||||||
release: 'hprelease',
|
hpattack,
|
||||||
env: 'hpenv',
|
hpdecay,
|
||||||
anchor: 'fanchor',
|
hpsustain,
|
||||||
model: 'ftype',
|
hprelease,
|
||||||
drive: 'drive',
|
hpenv,
|
||||||
rate: 'hprate',
|
t,
|
||||||
sync: 'hpsync',
|
end,
|
||||||
depth: 'hpdepth',
|
fanchor,
|
||||||
shape: 'hpshape',
|
);
|
||||||
dcoffset: 'hpdc',
|
|
||||||
skew: 'hpskew',
|
|
||||||
};
|
|
||||||
const hpParams = pickAndRename(value, hpMap);
|
|
||||||
hpParams.type = 'highpass';
|
|
||||||
let hp = () => createFilter(ac, t, end, hpParams, cps);
|
|
||||||
chain.push(hp());
|
chain.push(hp());
|
||||||
if (ftype === '24db') {
|
if (ftype === '24db') {
|
||||||
chain.push(hp());
|
chain.push(hp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.bandf !== undefined) {
|
if (bandf !== undefined) {
|
||||||
const bpMap = {
|
let bp = () =>
|
||||||
frequency: 'bandf',
|
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
|
||||||
q: 'bandq',
|
|
||||||
attack: 'bpattack',
|
|
||||||
decay: 'bpdecay',
|
|
||||||
sustain: 'bpsustain',
|
|
||||||
release: 'bprelease',
|
|
||||||
env: 'bpenv',
|
|
||||||
anchor: 'fanchor',
|
|
||||||
model: 'ftype',
|
|
||||||
drive: 'drive',
|
|
||||||
rate: 'bprate',
|
|
||||||
sync: 'bpsync',
|
|
||||||
depth: 'bpdepth',
|
|
||||||
shape: 'bpshape',
|
|
||||||
dcoffset: 'bpdc',
|
|
||||||
skew: 'bpskew',
|
|
||||||
};
|
|
||||||
const bpParams = pickAndRename(value, bpMap);
|
|
||||||
bpParams.type = 'bandpass';
|
|
||||||
let bp = () => createFilter(ac, t, end, bpParams, cps);
|
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
if (ftype === '24db') {
|
if (ftype === '24db') {
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
@@ -630,7 +630,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// effects
|
// effects
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
|
||||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||||
|
|
||||||
if (tremolosync != null) {
|
if (tremolosync != null) {
|
||||||
|
|||||||
@@ -16,17 +16,13 @@ export const tokenizeNote = (note) => {
|
|||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||||
|
|
||||||
export const getAccidentalsOffset = (accidentals) => {
|
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = getAccidentalsOffset(acc);
|
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
@@ -84,12 +80,13 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
export function getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfoFromBank(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
let midi = valueToMidi(hapValue, 36);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
|
||||||
let url;
|
let url;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let {transpose, label} = getCommonSampleInfo(hapValue)
|
||||||
|
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
url = bank[index];
|
||||||
@@ -106,11 +103,20 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
url = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
label = `${s}:${index}`;
|
||||||
return { transpose, url, index, midi, label };
|
return { transpose, url, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects entries from `source` and renames them via `map` */
|
|
||||||
export const pickAndRename = (source, map) => {
|
export function getCommonSampleInfo(hapValue) {
|
||||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
const { s, n = 0 } = hapValue;
|
||||||
};
|
let midi = valueToMidi(hapValue, 36);
|
||||||
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
|
const label = `${s}:${n}`;
|
||||||
|
return { transpose, label };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfoFromBank } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
@@ -216,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
||||||
}
|
}
|
||||||
const frequency = getFrequencyFromValue(value);
|
const frequency = getFrequencyFromValue(value);
|
||||||
const { url, label } = getCommonSampleInfo(value, tables);
|
const { url, label } = getCommonSampleInfoFromBank(value, tables);
|
||||||
const payload = await getPayload(url, label, frameLen);
|
const payload = await getPayload(url, label, frameLen);
|
||||||
let holdEnd = t + duration;
|
let holdEnd = t + duration;
|
||||||
if (clip !== undefined) {
|
if (clip !== undefined) {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
// this is dough, the superdough without dependencies
|
// this is dough, the superdough without dependencies
|
||||||
// @ts-nocheck
|
// @ts-check
|
||||||
// @ts-ignore ignore next line because sampleRate is unknown
|
// @ts-ignore ignore next line because sampleRate is unknown
|
||||||
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||||
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
||||||
const ISR = 1 / SAMPLE_RATE;
|
const ISR = 1 / SAMPLE_RATE;
|
||||||
|
|
||||||
let gainCurveFunc = (val) => Math.pow(val, 2);
|
let gainCurveFunc = (val) => Math.pow(val, 2);
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
||||||
|
|
||||||
function applyGainCurve(val) {
|
function applyGainCurve(val) {
|
||||||
return gainCurveFunc(val);
|
return gainCurveFunc(val);
|
||||||
@@ -152,8 +151,7 @@ export class TwoPoleFilter {
|
|||||||
resonance = Math.max(resonance, 0);
|
resonance = Math.max(resonance, 0);
|
||||||
|
|
||||||
cutoff = Math.min(cutoff, 20000);
|
cutoff = Math.min(cutoff, 20000);
|
||||||
let c = 2 * Math.sin(cutoff * PI_DIV_SR);
|
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
|
||||||
c = clamp(c, 0, 1.14); // this line prevents instability TODO: test
|
|
||||||
|
|
||||||
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||||
const mrc = 1 - r * c;
|
const mrc = 1 - r * c;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "supradough",
|
"name": "supradough",
|
||||||
"version": "1.2.4",
|
"version": "1.2.3",
|
||||||
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://codeberg.org/uzu/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tidalcycles",
|
"tidalcycles",
|
||||||
@@ -25,15 +25,13 @@
|
|||||||
"author": "Felix Roos <flix91@gmail.com>",
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://codeberg.org/uzu/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vite-plugin-bundle-audioworklet": "workspace:*",
|
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||||
"wav-encoder": "^1.3.0"
|
"wav-encoder": "^1.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"dependencies": {}
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [bundleAudioWorkletPlugin()],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -25,8 +25,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import './tonal.mjs';
|
import './tonal.mjs';
|
||||||
import './voicings.mjs';
|
import './voicings.mjs';
|
||||||
import './ireal.mjs';
|
|
||||||
|
|
||||||
export * from './tonal.mjs';
|
export * from './tonal.mjs';
|
||||||
export * from './voicings.mjs';
|
export * from './voicings.mjs';
|
||||||
export * from './ireal.mjs';
|
|
||||||
|
import './ireal.mjs';
|
||||||
|
|
||||||
export const packageName = '@strudel/tonal';
|
export const packageName = '@strudel/tonal';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/tonal",
|
"name": "@strudel/tonal",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -38,8 +38,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ describe('tonal', () => {
|
|||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
||||||
});
|
});
|
||||||
|
it('produces silence for mixed sharps and flats', () => {
|
||||||
|
expect(
|
||||||
|
n(seq('0b#', '1#b', '2#b#'))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
it('snaps notes (upwards) to scale', () => {
|
it('snaps notes (upwards) to scale', () => {
|
||||||
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
||||||
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
||||||
|
|||||||
+40
-44
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||||
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core';
|
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
||||||
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||||
|
import { noteToMidi } from '../core/util.mjs';
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||||
|
|
||||||
@@ -184,15 +185,17 @@ function _convertStepToNumberAndOffset(step) {
|
|||||||
step = String(step);
|
step = String(step);
|
||||||
// Check to see if the step matches the expected format:
|
// Check to see if the step matches the expected format:
|
||||||
// - A number (possibly negative)
|
// - A number (possibly negative)
|
||||||
// - Some number of sharps or flats
|
// - Some number of sharps or flats (but not both)
|
||||||
const match = /^(-?\d+)([#bsf]*)$/.exec(step);
|
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
||||||
}
|
}
|
||||||
asNumber = Number(match[1]);
|
asNumber = Number(match[1]);
|
||||||
const accidentals = match[2] || '';
|
// These decorations will determine the semitone offset based on the number of
|
||||||
offset = getAccidentalsOffset(accidentals);
|
// sharps or flats
|
||||||
|
const decorations = match[2] || '';
|
||||||
|
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
||||||
}
|
}
|
||||||
return [asNumber, offset];
|
return [asNumber, offset];
|
||||||
}
|
}
|
||||||
@@ -223,15 +226,12 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
|||||||
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
||||||
*
|
*
|
||||||
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||||
* in the scale as well as sharps or flats to produce notes outside of the scale.
|
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
|
||||||
*
|
*
|
||||||
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
*
|
||||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||||
*
|
*
|
||||||
* The scale name must be written without spaces (because it would be interpreted as a multi-step pattern otherwise).
|
|
||||||
* If your scale name includes spaces, replace them with colons.
|
|
||||||
*
|
|
||||||
* The root note defaults to octave 3, if no octave number is given.
|
* The root note defaults to octave 3, if no octave number is given.
|
||||||
*
|
*
|
||||||
* @name scale
|
* @name scale
|
||||||
@@ -253,9 +253,8 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
|||||||
* .s("piano")
|
* .s("piano")
|
||||||
* @example
|
* @example
|
||||||
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
||||||
* @example
|
|
||||||
* n("[0 0] [1 2] [3 4] [5 6]").scale("C:major:blues")
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register(
|
export const scale = register(
|
||||||
'scale',
|
'scale',
|
||||||
function (scale, pat) {
|
function (scale, pat) {
|
||||||
@@ -263,47 +262,44 @@ export const scale = register(
|
|||||||
if (Array.isArray(scale)) {
|
if (Array.isArray(scale)) {
|
||||||
scale = scale.flat().join(' ');
|
scale = scale.flat().join(' ');
|
||||||
}
|
}
|
||||||
return pat.withHaps((haps) => {
|
return (
|
||||||
haps = haps.map((hap) => {
|
pat
|
||||||
let hVal = hap.value;
|
.fmap((value) => {
|
||||||
const isObject = typeof hVal === 'object';
|
const isObject = typeof value === 'object';
|
||||||
// If hVal is a pure value, place it on `n` so that we interpret it as a scale degree
|
// The case where the note has been defined via `n` or `pure`
|
||||||
hVal = isObject ? hVal : { n: hVal };
|
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
||||||
const { note, n, value, ...otherValues } = hVal;
|
const step = isObject ? (value.n ?? value.value) : value;
|
||||||
const noteOrStep = note ?? n ?? value;
|
delete value.n; // remove n so it won't cause trouble
|
||||||
if (noteOrStep === undefined) {
|
if (isNote(step)) {
|
||||||
logger(
|
// legacy..
|
||||||
`[tonal] Invalid value format for 'scale'. Value must contain n, note, or value but received keys [${Object.keys(hVal).join(', ')}]`,
|
return pure(step);
|
||||||
'error',
|
|
||||||
);
|
|
||||||
return hap; // pass the value through unchanged
|
|
||||||
}
|
}
|
||||||
let scaleNote;
|
|
||||||
if (isNote(noteOrStep)) {
|
|
||||||
// Note case (quantize to scale)
|
|
||||||
scaleNote = _getNearestScaleNote(scale, noteOrStep);
|
|
||||||
hap.value = { ...otherValues, note: scaleNote };
|
|
||||||
} else {
|
|
||||||
// Step case (convert to note in scale)
|
|
||||||
try {
|
try {
|
||||||
const [number, offset] = _convertStepToNumberAndOffset(noteOrStep);
|
const [number, offset] = _convertStepToNumberAndOffset(step);
|
||||||
if (otherValues.anchor) {
|
let note;
|
||||||
scaleNote = stepInNamedScale(number, scale, otherValues.anchor);
|
if (isObject && value.anchor) {
|
||||||
|
note = stepInNamedScale(number, scale, value.anchor);
|
||||||
} else {
|
} else {
|
||||||
scaleNote = scaleStep(number, scale);
|
note = scaleStep(number, scale);
|
||||||
}
|
}
|
||||||
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
|
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
||||||
|
value = pure(isObject ? { ...value, note } : note);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
logger(`[tonal] ${err.message}`, 'error');
|
||||||
return; // will be removed
|
return silence;
|
||||||
}
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote;
|
// The case where the note has been defined via `note`
|
||||||
// Tag with scale for downsteam scale-aware operations
|
else {
|
||||||
return hap.setContext({ ...hap.context, scale });
|
const note = _getNearestScaleNote(scale, value.note);
|
||||||
});
|
return pure(isObject ? { ...value, note } : note);
|
||||||
return removeUndefineds(haps);
|
}
|
||||||
});
|
})
|
||||||
|
.outerJoin()
|
||||||
|
// legacy:
|
||||||
|
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
||||||
|
);
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
true, // preserve step count
|
true, // preserve step count
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/transpiler",
|
"name": "@strudel/transpiler",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"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",
|
||||||
@@ -39,8 +39,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,5 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ initStrudel();
|
|||||||
document.getElementById('play').addEventListener('click',
|
document.getElementById('play').addEventListener('click',
|
||||||
() => evaluate('note("c a f e").jux(rev)')
|
() => evaluate('note("c a f e").jux(rev)')
|
||||||
);
|
);
|
||||||
document.getElementById('stop').addEventListener('click',
|
document.getElementById('play').addEventListener('stop',
|
||||||
() => hush()
|
() => hush()
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"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": {
|
||||||
@@ -43,8 +43,5 @@
|
|||||||
"@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:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/webaudio",
|
"name": "@strudel/webaudio",
|
||||||
"version": "1.2.6",
|
"version": "1.2.5",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -40,8 +40,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/xen",
|
"name": "@strudel/xen",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "Xenharmonic API for strudel",
|
"description": "Xenharmonic API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^3.0.4"
|
"vitest": "^3.0.4"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Tune.prototype.tonicize = function(newTonic) {
|
|||||||
this.tonic = newTonic
|
this.tonic = newTonic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Return data in the mode you are in (freq, ratio, or midi) */
|
/* Return data in the mode you are in (freq, ratio, or midi) */
|
||||||
|
|
||||||
Tune.prototype.note = function(input,octave){
|
Tune.prototype.note = function(input,octave){
|
||||||
|
|||||||
Generated
+24
-374
@@ -85,29 +85,29 @@ importers:
|
|||||||
examples/codemirror-repl:
|
examples/codemirror-repl:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/codemirror':
|
'@strudel/codemirror':
|
||||||
specifier: 1.2.6
|
specifier: workspace:*
|
||||||
version: 1.2.6(@codemirror/lint@6.8.4)
|
version: link:../../packages/codemirror
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: 1.2.5
|
specifier: workspace:*
|
||||||
version: 1.2.5
|
version: link:../../packages/core
|
||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: 1.2.5
|
specifier: workspace:*
|
||||||
version: 1.2.5
|
version: link:../../packages/draw
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: 1.2.5
|
specifier: workspace:*
|
||||||
version: 1.2.5
|
version: link:../../packages/mini
|
||||||
'@strudel/soundfonts':
|
'@strudel/soundfonts':
|
||||||
specifier: 1.2.6
|
specifier: workspace:*
|
||||||
version: 1.2.6
|
version: link:../../packages/soundfonts
|
||||||
'@strudel/tonal':
|
'@strudel/tonal':
|
||||||
specifier: 1.2.5
|
specifier: workspace:*
|
||||||
version: 1.2.5
|
version: link:../../packages/tonal
|
||||||
'@strudel/transpiler':
|
'@strudel/transpiler':
|
||||||
specifier: 1.2.5
|
specifier: workspace:*
|
||||||
version: 1.2.5
|
version: link:../../packages/transpiler
|
||||||
'@strudel/webaudio':
|
'@strudel/webaudio':
|
||||||
specifier: 1.2.6
|
specifier: workspace:*
|
||||||
version: 1.2.6
|
version: link:../../packages/webaudio
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -116,8 +116,8 @@ importers:
|
|||||||
examples/headless-repl:
|
examples/headless-repl:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/web':
|
'@strudel/web':
|
||||||
specifier: 1.2.6
|
specifier: workspace:*
|
||||||
version: 1.2.6
|
version: link:../../packages/web
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -212,21 +212,12 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
'@strudel/tonal':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../tonal
|
|
||||||
'@strudel/transpiler':
|
'@strudel/transpiler':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../transpiler
|
version: link:../transpiler
|
||||||
'@tonaljs/tonal':
|
|
||||||
specifier: ^4.10.0
|
|
||||||
version: 4.10.0
|
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
superdough:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../superdough
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -415,12 +406,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
osc:
|
osc-js:
|
||||||
specifier: ^2.4.5
|
specifier: ^2.4.1
|
||||||
version: 2.4.5
|
version: 2.4.1
|
||||||
ws:
|
|
||||||
specifier: ^8.18.3
|
|
||||||
version: 8.18.3
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
pkg:
|
pkg:
|
||||||
specifier: ^5.8.1
|
specifier: ^5.8.1
|
||||||
@@ -2432,70 +2420,6 @@ packages:
|
|||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
'@serialport/binding-mock@10.2.2':
|
|
||||||
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/bindings-cpp@12.0.1':
|
|
||||||
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
|
|
||||||
engines: {node: '>=16.0.0'}
|
|
||||||
|
|
||||||
'@serialport/bindings-interface@1.2.2':
|
|
||||||
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
|
|
||||||
engines: {node: ^12.22 || ^14.13 || >=16}
|
|
||||||
|
|
||||||
'@serialport/parser-byte-length@12.0.0':
|
|
||||||
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-cctalk@12.0.0':
|
|
||||||
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-delimiter@11.0.0':
|
|
||||||
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-delimiter@12.0.0':
|
|
||||||
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-inter-byte-timeout@12.0.0':
|
|
||||||
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-packet-length@12.0.0':
|
|
||||||
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
|
|
||||||
engines: {node: '>=8.6.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-readline@11.0.0':
|
|
||||||
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-readline@12.0.0':
|
|
||||||
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-ready@12.0.0':
|
|
||||||
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-regex@12.0.0':
|
|
||||||
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-slip-encoder@12.0.0':
|
|
||||||
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/parser-spacepacket@12.0.0':
|
|
||||||
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@serialport/stream@12.0.0':
|
|
||||||
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
'@shikijs/core@1.29.1':
|
'@shikijs/core@1.29.1':
|
||||||
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
|
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
|
||||||
|
|
||||||
@@ -2544,33 +2468,6 @@ packages:
|
|||||||
'@sinclair/typebox@0.27.8':
|
'@sinclair/typebox@0.27.8':
|
||||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||||
|
|
||||||
'@strudel/codemirror@1.2.6':
|
|
||||||
resolution: {integrity: sha512-zjKbnbnOY40dlIAQu1poq28rcngYliLsuDwXXVnJsaXzCZSVuXEOMq91LqY4IGz/2QUOIDGm663ziz6Nb4IXeg==}
|
|
||||||
|
|
||||||
'@strudel/core@1.2.5':
|
|
||||||
resolution: {integrity: sha512-XO6ICbfuGfvaQ4sZmfIFsPDcjFez/+AVpNgJnmks5gtMMQNXpr0reuee61tvMiu2k5QYKzxWgzDl2FF9pJeIgA==}
|
|
||||||
|
|
||||||
'@strudel/draw@1.2.5':
|
|
||||||
resolution: {integrity: sha512-ZI4WPxUsVsxgDoeZMHo6VfFf+S1tsj8ubKBeSLcf6lH29JqPp7Nz6tPmbomOV1fzazZ7+xpkC7z772MdtgWy2g==}
|
|
||||||
|
|
||||||
'@strudel/mini@1.2.5':
|
|
||||||
resolution: {integrity: sha512-GmTWSBt49C3ogBy0hjM3bmf6K12PaJ1JJLsEvQu7j5J+lUd2n7GmLYR1UL6fntY4yVqT8ZwMhfUf6vIgUCTNbw==}
|
|
||||||
|
|
||||||
'@strudel/soundfonts@1.2.6':
|
|
||||||
resolution: {integrity: sha512-F7eBhohkCXCMo8WiufSAhD3JCfL5GTgCh9RlOJVT+1swiFeIss1vUAWJR3WjonB6moL8BiuNpcYOqO6i6cFy4A==}
|
|
||||||
|
|
||||||
'@strudel/tonal@1.2.5':
|
|
||||||
resolution: {integrity: sha512-mX3uW0nWH8Sa52HTLwnxlR5BrejFX77abeaiMbp+kQCMrX9I+XRdf8rEr7gdeInB6f/y2TzTVFpnsNsB5jLdoA==}
|
|
||||||
|
|
||||||
'@strudel/transpiler@1.2.5':
|
|
||||||
resolution: {integrity: sha512-mBZWfQBzknvnIYsjjzR8D67A+uIZ1iZ60U2WoXT21q5luXKz2c95RcTpqdx3oOGpLK9K00lDzcKjhQDJtj4OKw==}
|
|
||||||
|
|
||||||
'@strudel/web@1.2.6':
|
|
||||||
resolution: {integrity: sha512-e6VltuX90zZz8JVe1FtN23DCaXA9p8zGDakp5OuUb5kZ5hC6cySFkkOsHdGC6dJ0EAsZSCKXMihRxgk9GhBwKA==}
|
|
||||||
|
|
||||||
'@strudel/webaudio@1.2.6':
|
|
||||||
resolution: {integrity: sha512-4LnyNvyeAXYNmXQGGw2f0chIkDr9Rbwo+SK6Y0p8C3W8juDiOfKq2ihSVph95yaBBPRYFJ5Kttj1kLknRHYsog==}
|
|
||||||
|
|
||||||
'@supabase/auth-js@2.67.3':
|
'@supabase/auth-js@2.67.3':
|
||||||
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
|
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
|
||||||
|
|
||||||
@@ -3775,15 +3672,6 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
|
||||||
engines: {node: '>=6.0'}
|
|
||||||
peerDependencies:
|
|
||||||
supports-color: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
supports-color:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.0:
|
||||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -5393,9 +5281,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
long@4.0.0:
|
|
||||||
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
|
||||||
|
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@@ -5779,9 +5664,6 @@ packages:
|
|||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.2:
|
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -5840,9 +5722,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
|
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
node-addon-api@7.0.0:
|
|
||||||
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
|
|
||||||
|
|
||||||
node-addon-api@8.3.0:
|
node-addon-api@8.3.0:
|
||||||
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
|
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
@@ -5881,10 +5760,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
|
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
node-gyp-build@4.6.0:
|
|
||||||
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -6043,8 +5918,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
osc@2.4.5:
|
osc-js@2.4.1:
|
||||||
resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}
|
resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
@@ -6805,10 +6680,6 @@ packages:
|
|||||||
serialize-javascript@4.0.0:
|
serialize-javascript@4.0.0:
|
||||||
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
|
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
|
||||||
|
|
||||||
serialport@12.0.0:
|
|
||||||
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
|
|
||||||
engines: {node: '>=16.0.0'}
|
|
||||||
|
|
||||||
set-blocking@2.0.0:
|
set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
@@ -6896,9 +6767,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
slip@1.0.2:
|
|
||||||
resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==}
|
|
||||||
|
|
||||||
smart-buffer@4.2.0:
|
smart-buffer@4.2.0:
|
||||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||||
@@ -7121,9 +6989,6 @@ packages:
|
|||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
superdough@1.2.6:
|
|
||||||
resolution: {integrity: sha512-zHEFfG+N4zhxJ9lUDf8p4Oj4sdGGQ5SrOPksdc0cKv5Yn0hzlNlMehEa5TkeIfDCiR27Y4H81ks7S8/CGSByhQ==}
|
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -7132,9 +6997,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
supradough@1.2.4:
|
|
||||||
resolution: {integrity: sha512-/v3cL9fq05QCQVwbttLHNZcjyyFFz5wdKvg3j6V6moRXpDlQG89l8dQtUIpQvHsxYI5sXNod9rh5bf+Y4NMGeA==}
|
|
||||||
|
|
||||||
tabbable@6.2.0:
|
tabbable@6.2.0:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
|
|
||||||
@@ -7780,9 +7642,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
wolfy87-eventemitter@5.2.9:
|
|
||||||
resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==}
|
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7905,18 +7764,6 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
ws@8.18.3:
|
|
||||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
bufferutil: ^4.0.1
|
|
||||||
utf-8-validate: '>=5.0.2'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bufferutil:
|
|
||||||
optional: true
|
|
||||||
utf-8-validate:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
xmlcreate@2.0.4:
|
xmlcreate@2.0.4:
|
||||||
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
|
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
|
||||||
|
|
||||||
@@ -9913,76 +9760,6 @@ snapshots:
|
|||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@serialport/binding-mock@10.2.2':
|
|
||||||
dependencies:
|
|
||||||
'@serialport/bindings-interface': 1.2.2
|
|
||||||
debug: 4.4.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/bindings-cpp@12.0.1':
|
|
||||||
dependencies:
|
|
||||||
'@serialport/bindings-interface': 1.2.2
|
|
||||||
'@serialport/parser-readline': 11.0.0
|
|
||||||
debug: 4.3.4
|
|
||||||
node-addon-api: 7.0.0
|
|
||||||
node-gyp-build: 4.6.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/bindings-interface@1.2.2':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-byte-length@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-cctalk@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-delimiter@11.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-delimiter@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-inter-byte-timeout@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-packet-length@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-readline@11.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@serialport/parser-delimiter': 11.0.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-readline@12.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@serialport/parser-delimiter': 12.0.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-ready@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-regex@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-slip-encoder@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/parser-spacepacket@12.0.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@serialport/stream@12.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@serialport/bindings-interface': 1.2.2
|
|
||||||
debug: 4.3.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@shikijs/core@1.29.1':
|
'@shikijs/core@1.29.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/engine-javascript': 1.29.1
|
'@shikijs/engine-javascript': 1.29.1
|
||||||
@@ -10052,79 +9829,6 @@ snapshots:
|
|||||||
|
|
||||||
'@sinclair/typebox@0.27.8': {}
|
'@sinclair/typebox@0.27.8': {}
|
||||||
|
|
||||||
'@strudel/codemirror@1.2.6(@codemirror/lint@6.8.4)':
|
|
||||||
dependencies:
|
|
||||||
'@codemirror/autocomplete': 6.18.4
|
|
||||||
'@codemirror/commands': 6.8.0
|
|
||||||
'@codemirror/lang-javascript': 6.2.2
|
|
||||||
'@codemirror/language': 6.10.8
|
|
||||||
'@codemirror/search': 6.5.8
|
|
||||||
'@codemirror/state': 6.5.1
|
|
||||||
'@codemirror/view': 6.36.2
|
|
||||||
'@lezer/highlight': 1.2.1
|
|
||||||
'@nanostores/persistent': 0.10.2(nanostores@0.11.3)
|
|
||||||
'@replit/codemirror-emacs': 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
|
||||||
'@replit/codemirror-vim': 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
|
||||||
'@replit/codemirror-vscode-keymap': 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@strudel/draw': 1.2.5
|
|
||||||
'@strudel/tonal': 1.2.5
|
|
||||||
'@strudel/transpiler': 1.2.5
|
|
||||||
'@tonaljs/tonal': 4.10.0
|
|
||||||
nanostores: 0.11.3
|
|
||||||
superdough: 1.2.6
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@codemirror/lint'
|
|
||||||
|
|
||||||
'@strudel/core@1.2.5':
|
|
||||||
dependencies:
|
|
||||||
fraction.js: 5.2.1
|
|
||||||
|
|
||||||
'@strudel/draw@1.2.5':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
|
|
||||||
'@strudel/mini@1.2.5':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
|
|
||||||
'@strudel/soundfonts@1.2.6':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@strudel/webaudio': 1.2.6
|
|
||||||
sfumato: 0.1.2
|
|
||||||
soundfont2: 0.5.0
|
|
||||||
|
|
||||||
'@strudel/tonal@1.2.5':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@tonaljs/tonal': 4.10.0
|
|
||||||
chord-voicings: 0.0.1
|
|
||||||
webmidi: 3.1.12
|
|
||||||
|
|
||||||
'@strudel/transpiler@1.2.5':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@strudel/mini': 1.2.5
|
|
||||||
acorn: 8.14.0
|
|
||||||
escodegen: 2.1.0
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
|
|
||||||
'@strudel/web@1.2.6':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@strudel/mini': 1.2.5
|
|
||||||
'@strudel/tonal': 1.2.5
|
|
||||||
'@strudel/transpiler': 1.2.5
|
|
||||||
'@strudel/webaudio': 1.2.6
|
|
||||||
|
|
||||||
'@strudel/webaudio@1.2.6':
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core': 1.2.5
|
|
||||||
'@strudel/draw': 1.2.5
|
|
||||||
superdough: 1.2.6
|
|
||||||
supradough: 1.2.4
|
|
||||||
|
|
||||||
'@supabase/auth-js@2.67.3':
|
'@supabase/auth-js@2.67.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@supabase/node-fetch': 2.6.15
|
'@supabase/node-fetch': 2.6.15
|
||||||
@@ -11568,11 +11272,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.2
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -13442,8 +13141,6 @@ snapshots:
|
|||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
is-unicode-supported: 0.1.0
|
is-unicode-supported: 0.1.0
|
||||||
|
|
||||||
long@4.0.0: {}
|
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
loupe@3.1.2: {}
|
loupe@3.1.2: {}
|
||||||
@@ -14121,9 +13818,6 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.2:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multimatch@5.0.0:
|
multimatch@5.0.0:
|
||||||
@@ -14173,9 +13867,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
|
|
||||||
node-addon-api@7.0.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
node-addon-api@8.3.0: {}
|
node-addon-api@8.3.0: {}
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
@@ -14202,9 +13893,6 @@ snapshots:
|
|||||||
|
|
||||||
node-getopt@0.3.2: {}
|
node-getopt@0.3.2: {}
|
||||||
|
|
||||||
node-gyp-build@4.6.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-gyp@10.3.1:
|
node-gyp@10.3.1:
|
||||||
@@ -14456,17 +14144,11 @@ snapshots:
|
|||||||
|
|
||||||
os-tmpdir@1.0.2: {}
|
os-tmpdir@1.0.2: {}
|
||||||
|
|
||||||
osc@2.4.5:
|
osc-js@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
long: 4.0.0
|
|
||||||
slip: 1.0.2
|
|
||||||
wolfy87-eventemitter: 5.2.9
|
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
optionalDependencies:
|
|
||||||
serialport: 12.0.0
|
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
@@ -15361,26 +15043,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|
||||||
serialport@12.0.0:
|
|
||||||
dependencies:
|
|
||||||
'@serialport/binding-mock': 10.2.2
|
|
||||||
'@serialport/bindings-cpp': 12.0.1
|
|
||||||
'@serialport/parser-byte-length': 12.0.0
|
|
||||||
'@serialport/parser-cctalk': 12.0.0
|
|
||||||
'@serialport/parser-delimiter': 12.0.0
|
|
||||||
'@serialport/parser-inter-byte-timeout': 12.0.0
|
|
||||||
'@serialport/parser-packet-length': 12.0.0
|
|
||||||
'@serialport/parser-readline': 12.0.0
|
|
||||||
'@serialport/parser-ready': 12.0.0
|
|
||||||
'@serialport/parser-regex': 12.0.0
|
|
||||||
'@serialport/parser-slip-encoder': 12.0.0
|
|
||||||
'@serialport/parser-spacepacket': 12.0.0
|
|
||||||
'@serialport/stream': 12.0.0
|
|
||||||
debug: 4.3.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
set-blocking@2.0.0: {}
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
@@ -15523,8 +15185,6 @@ snapshots:
|
|||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
slip@1.0.2: {}
|
|
||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0: {}
|
||||||
|
|
||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
@@ -15771,18 +15431,12 @@ snapshots:
|
|||||||
pirates: 4.0.6
|
pirates: 4.0.6
|
||||||
ts-interface-checker: 0.1.13
|
ts-interface-checker: 0.1.13
|
||||||
|
|
||||||
superdough@1.2.6:
|
|
||||||
dependencies:
|
|
||||||
nanostores: 0.11.3
|
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
supradough@1.2.4: {}
|
|
||||||
|
|
||||||
tabbable@6.2.0: {}
|
tabbable@6.2.0: {}
|
||||||
|
|
||||||
tailwindcss@3.4.17:
|
tailwindcss@3.4.17:
|
||||||
@@ -16440,8 +16094,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width: 7.2.0
|
string-width: 7.2.0
|
||||||
|
|
||||||
wolfy87-eventemitter@5.2.9: {}
|
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wordwrap@0.0.3: {}
|
wordwrap@0.0.3: {}
|
||||||
@@ -16641,8 +16293,6 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.18.0: {}
|
ws@8.18.0: {}
|
||||||
|
|
||||||
ws@8.18.3: {}
|
|
||||||
|
|
||||||
xmlcreate@2.0.4: {}
|
xmlcreate@2.0.4: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|||||||
@@ -840,31 +840,6 @@ exports[`runs examples > example "amp" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "anchor" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:E2 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:E3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:G3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C3 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G3 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:E3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:C4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:E4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:G4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:C5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:G4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:E5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:G5 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "apply" example index 0 1`] = `
|
exports[`runs examples > example "apply" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:C3 ]",
|
"[ 0/1 → 1/1 | note:C3 ]",
|
||||||
@@ -1872,31 +1847,6 @@ exports[`runs examples > example "chop" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "chord" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:A3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:E4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:A4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E3 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C5 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:D3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:A3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:D4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:Gb4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:A4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:F3 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:F4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:A4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C5 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "chorus" example index 0 1`] = `
|
exports[`runs examples > example "chorus" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
|
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
@@ -2789,23 +2739,6 @@ exports[`runs examples > example "detune" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "dictionary" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:A3 room:0.5 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C4 room:0.5 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:E4 room:0.5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G3 room:0.5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C4 room:0.5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E4 room:0.5 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:A3 room:0.5 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:D4 room:0.5 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:Gb4 room:0.5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:A3 room:0.5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C4 room:0.5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:F4 room:0.5 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "distort" example index 0 1`] = `
|
exports[`runs examples > example "distort" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh distort:0 ]",
|
"[ 0/1 → 1/8 | s:hh distort:0 ]",
|
||||||
@@ -3905,8 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:C2 color:red ]",
|
"[ 0/1 → 1/4 | color:red note:0 ]",
|
||||||
"[ 1/4 → 1/2 | note:D2 color:red ]",
|
"[ 1/4 → 1/2 | color:red note:1 ]",
|
||||||
"[ 1/2 → 3/4 | note:E2 ]",
|
"[ 1/2 → 3/4 | note:E2 ]",
|
||||||
"[ 3/4 → 1/1 | note:F2 ]",
|
"[ 3/4 → 1/1 | note:F2 ]",
|
||||||
"[ 1/1 → 5/4 | note:G2 ]",
|
"[ 1/1 → 5/4 | note:G2 ]",
|
||||||
@@ -3915,8 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
|||||||
"[ 7/4 → 2/1 | note:C3 ]",
|
"[ 7/4 → 2/1 | note:C3 ]",
|
||||||
"[ 2/1 → 9/4 | note:D3 ]",
|
"[ 2/1 → 9/4 | note:D3 ]",
|
||||||
"[ 9/4 → 5/2 | note:D2 ]",
|
"[ 9/4 → 5/2 | note:D2 ]",
|
||||||
"[ 5/2 → 11/4 | note:E2 color:red ]",
|
"[ 5/2 → 11/4 | color:red note:2 ]",
|
||||||
"[ 11/4 → 3/1 | note:F2 color:red ]",
|
"[ 11/4 → 3/1 | color:red note:3 ]",
|
||||||
"[ 3/1 → 13/4 | note:G2 ]",
|
"[ 3/1 → 13/4 | note:G2 ]",
|
||||||
"[ 13/4 → 7/2 | note:A2 ]",
|
"[ 13/4 → 7/2 | note:A2 ]",
|
||||||
"[ 7/2 → 15/4 | note:B2 ]",
|
"[ 7/2 → 15/4 | note:B2 ]",
|
||||||
@@ -6563,28 +6496,6 @@ exports[`runs examples > example "miditouch" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "mode" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:E3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:E4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:G4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C6 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E6 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:E3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:C4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:E4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:G4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:G5 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C6 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:E6 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "morph" example index 0 1`] = `
|
exports[`runs examples > example "morph" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh ]",
|
"[ 0/1 → 1/8 | s:hh ]",
|
||||||
@@ -6903,31 +6814,6 @@ exports[`runs examples > example "octave" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "octaves" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:A3 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:E4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:A4 ]",
|
|
||||||
"[ 0/1 → 1/1 | note:C5 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E3 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:E4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:G4 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:C5 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:D3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:A3 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:D4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:Gb4 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:A4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:F3 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:F4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:A4 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:C5 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "off" example index 0 1`] = `
|
exports[`runs examples > example "off" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]",
|
"[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]",
|
||||||
@@ -6961,15 +6847,6 @@ exports[`runs examples > example "off" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "offset" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | chord:Am offset:0 ]",
|
|
||||||
"[ 1/1 → 2/1 | chord:C offset:1 ]",
|
|
||||||
"[ 2/1 → 3/1 | chord:D offset:2 ]",
|
|
||||||
"[ 3/1 → 4/1 | chord:F offset:3 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "often" example index 0 1`] = `
|
exports[`runs examples > example "often" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh speed:0.5 ]",
|
"[ 0/1 → 1/8 | s:hh speed:0.5 ]",
|
||||||
@@ -7081,24 +6958,6 @@ exports[`runs examples > example "orbit" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "oschost" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "oscport" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "outside" example index 0 1`] = `
|
exports[`runs examples > example "outside" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:A3 ]",
|
"[ 0/1 → 1/1 | note:A3 ]",
|
||||||
@@ -8448,33 +8307,6 @@ exports[`runs examples > example "repeatCycles" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "replicate" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
|
||||||
"[ 1/8 → 1/4 | s:bd ]",
|
|
||||||
"[ 3/8 → 1/2 | s:cp ]",
|
|
||||||
"[ 1/2 → 5/8 | s:bd ]",
|
|
||||||
"[ 5/8 → 3/4 | s:bd ]",
|
|
||||||
"[ 7/8 → 1/1 | s:cp ]",
|
|
||||||
"[ 1/1 → 9/8 | s:bd ]",
|
|
||||||
"[ 5/4 → 11/8 | s:sd ]",
|
|
||||||
"[ 3/2 → 13/8 | s:bd ]",
|
|
||||||
"[ 13/8 → 7/4 | s:bd ]",
|
|
||||||
"[ 15/8 → 2/1 | s:cp ]",
|
|
||||||
"[ 2/1 → 17/8 | s:bd ]",
|
|
||||||
"[ 17/8 → 9/4 | s:bd ]",
|
|
||||||
"[ 19/8 → 5/2 | s:cp ]",
|
|
||||||
"[ 5/2 → 21/8 | s:bd ]",
|
|
||||||
"[ 11/4 → 23/8 | s:sd ]",
|
|
||||||
"[ 3/1 → 25/8 | s:bd ]",
|
|
||||||
"[ 25/8 → 13/4 | s:bd ]",
|
|
||||||
"[ 27/8 → 7/2 | s:cp ]",
|
|
||||||
"[ 7/2 → 29/8 | s:bd ]",
|
|
||||||
"[ 29/8 → 15/4 | s:bd ]",
|
|
||||||
"[ 31/8 → 4/1 | s:cp ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "reset" example index 0 1`] = `
|
exports[`runs examples > example "reset" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh ]",
|
"[ 0/1 → 1/8 | s:hh ]",
|
||||||
@@ -9337,43 +9169,6 @@ exports[`runs examples > example "scale" example index 4 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "scale" example index 5 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/8 | note:C3 ]",
|
|
||||||
"[ 1/8 → 1/4 | note:C3 ]",
|
|
||||||
"[ 1/4 → 3/8 | note:D3 ]",
|
|
||||||
"[ 3/8 → 1/2 | note:Eb3 ]",
|
|
||||||
"[ 1/2 → 5/8 | note:E3 ]",
|
|
||||||
"[ 5/8 → 3/4 | note:G3 ]",
|
|
||||||
"[ 3/4 → 7/8 | note:A3 ]",
|
|
||||||
"[ 7/8 → 1/1 | note:C4 ]",
|
|
||||||
"[ 1/1 → 9/8 | note:C3 ]",
|
|
||||||
"[ 9/8 → 5/4 | note:C3 ]",
|
|
||||||
"[ 5/4 → 11/8 | note:D3 ]",
|
|
||||||
"[ 11/8 → 3/2 | note:Eb3 ]",
|
|
||||||
"[ 3/2 → 13/8 | note:E3 ]",
|
|
||||||
"[ 13/8 → 7/4 | note:G3 ]",
|
|
||||||
"[ 7/4 → 15/8 | note:A3 ]",
|
|
||||||
"[ 15/8 → 2/1 | note:C4 ]",
|
|
||||||
"[ 2/1 → 17/8 | note:C3 ]",
|
|
||||||
"[ 17/8 → 9/4 | note:C3 ]",
|
|
||||||
"[ 9/4 → 19/8 | note:D3 ]",
|
|
||||||
"[ 19/8 → 5/2 | note:Eb3 ]",
|
|
||||||
"[ 5/2 → 21/8 | note:E3 ]",
|
|
||||||
"[ 21/8 → 11/4 | note:G3 ]",
|
|
||||||
"[ 11/4 → 23/8 | note:A3 ]",
|
|
||||||
"[ 23/8 → 3/1 | note:C4 ]",
|
|
||||||
"[ 3/1 → 25/8 | note:C3 ]",
|
|
||||||
"[ 25/8 → 13/4 | note:C3 ]",
|
|
||||||
"[ 13/4 → 27/8 | note:D3 ]",
|
|
||||||
"[ 27/8 → 7/2 | note:Eb3 ]",
|
|
||||||
"[ 7/2 → 29/8 | note:E3 ]",
|
|
||||||
"[ 29/8 → 15/4 | note:G3 ]",
|
|
||||||
"[ 15/4 → 31/8 | note:A3 ]",
|
|
||||||
"[ 31/8 → 4/1 | note:C4 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | note:C3 ]",
|
"[ 0/1 → 1/2 | note:C3 ]",
|
||||||
|
|||||||
+6336
-6336
File diff suppressed because it is too large
Load Diff
+1
-15
@@ -4,25 +4,11 @@ import { describe, it } from 'vitest';
|
|||||||
|
|
||||||
const tuneKeys = Object.keys(tunes);
|
const tuneKeys = Object.keys(tunes);
|
||||||
|
|
||||||
// Node 24 tightened Number→string rounding; clamp decimals so snapshots stay stable across engines.
|
|
||||||
const roundFloatStrings = (input, precision = 12) => {
|
|
||||||
// if matches a decimal number ex: 12.34, -0.5, 0.123, 99.0, 1.932093850293
|
|
||||||
const regex = /-?\d+\.\d+/g;
|
|
||||||
return input.replace(regex, (match) => {
|
|
||||||
// converts the literal to a number, performs round to nearest (ties to even)
|
|
||||||
// at the requested precision, and returns the rounded decimal string
|
|
||||||
const rounded = Number(match).toFixed(precision);
|
|
||||||
// trims trailing zeros (and a dangling dot) after rounding, so the displayed string looks tidy
|
|
||||||
return rounded.replace(/\.?0+$/, '').replace(/\.$/, '');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('renders tunes', () => {
|
describe('renders tunes', () => {
|
||||||
tuneKeys.forEach((key) => {
|
tuneKeys.forEach((key) => {
|
||||||
it(`tune: ${key}`, async ({ expect }) => {
|
it(`tune: ${key}`, async ({ expect }) => {
|
||||||
const haps = await queryCode(tunes[key], testCycles[key] || 1);
|
const haps = await queryCode(tunes[key], testCycles[key] || 1);
|
||||||
const normalized = haps.map((hap) => roundFloatStrings(hap));
|
expect(haps).toMatchSnapshot();
|
||||||
expect(normalized).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv": "^6.3.11"
|
"csv": "^6.3.11"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,5 @@
|
|||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"workbox-window": "^7.3.0",
|
"workbox-window": "^7.3.0",
|
||||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ You can also create custom aliases for existing sounds using the `soundAlias` fu
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`soundAlias('RolandTR808_bd', 'kick')
|
tune={`soundAlias("RolandTR808_bd", "kick")
|
||||||
s("kick")`}
|
s("kick")`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all:
|
|||||||
.pitchJump(0) // +/- pitch change after pitchJumpTime
|
.pitchJump(0) // +/- pitch change after pitchJumpTime
|
||||||
.pitchJumpTime(0) // >0 time after pitchJump is applied
|
.pitchJumpTime(0) // >0 time after pitchJump is applied
|
||||||
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
|
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
|
||||||
.tremolo(0.5) // 0-1 lfo volume modulation amount
|
.tremolo(0) // 0-1 lfo volume modulation amount
|
||||||
//.duration(.2) // overwrite strudel event duration
|
//.duration(.2) // overwrite strudel event duration
|
||||||
//.gain(1) // change volume
|
//.gain(1) // change volume
|
||||||
._scope() // vizualise waveform (not zzfx related)
|
._scope() // vizualise waveform (not zzfx related)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ There are 3 quick ways to embed strudel in your website:
|
|||||||
|
|
||||||
### Inside an iframe
|
### Inside an iframe
|
||||||
|
|
||||||
Using an iframe is the most easy way to embed a strudel tune.
|
Using an iframe is the most easy way to embed a studel tune.
|
||||||
You can embed any pattern of your choice via an iframe and the URL of the pattern of your choice:
|
You can embed any pattern of your choice via an iframe and the URL of the pattern of your choice:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -133,7 +133,7 @@ If you'd rather use your own UI, you can use the `@strudel/web` package:
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
For more info on this package, see the [@strudel/web README](https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web).
|
For more info on this package, see the [@strudel/web README]https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web).
|
||||||
|
|
||||||
## Via npm
|
## Via npm
|
||||||
|
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ Here's an example AST for `c3 [e3 g3]`
|
|||||||
|
|
||||||
which translates to `seq(c3, seq(e3, g3))`
|
which translates to `seq(c3, seq(e3, g3))`
|
||||||
|
|
||||||
## Vim Keybindings
|
|
||||||
|
|
||||||
See the separate page on Vim shortcuts for a quick reference: [/technical-manual/vim](/technical-manual/vim)
|
|
||||||
|
|
||||||
## Scheduling Events
|
## Scheduling Events
|
||||||
|
|
||||||
After an instance of `Pattern` is obtained from the user code,
|
After an instance of `Pattern` is obtained from the user code,
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
title: Vim Shortcuts
|
|
||||||
layout: ../../layouts/MainLayout.astro
|
|
||||||
---
|
|
||||||
|
|
||||||
# Vim Shortcuts in the REPL
|
|
||||||
|
|
||||||
When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available:
|
|
||||||
|
|
||||||
- :w — Evaluate the current code
|
|
||||||
|
|
||||||
- Triggers the same evaluation as Ctrl+Enter / Alt+Enter
|
|
||||||
- You'll see messages in the Console panel such as:
|
|
||||||
- [vim] :w — evaluating code
|
|
||||||
- [repl] evaluate via event
|
|
||||||
- [eval] code updated
|
|
||||||
|
|
||||||
- :q — Stop/pause playback
|
|
||||||
|
|
||||||
- Triggers the same stop action as Alt+.
|
|
||||||
- Useful to quickly stop scheduling without leaving Vim mode
|
|
||||||
|
|
||||||
- gc — Toggle line comments for the current selection(s)
|
|
||||||
|
|
||||||
- Works in normal and visual mode
|
|
||||||
- If there's a selection, all selected lines are toggled
|
|
||||||
|
|
||||||
Notes
|
|
||||||
|
|
||||||
- Behavior respects the current language mode in the editor for comment syntax.
|
|
||||||
- If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor.
|
|
||||||
- If you don't see the Console panel, open the right panel in the REPL UI.
|
|
||||||
|
|
||||||
Troubleshooting
|
|
||||||
|
|
||||||
- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
|
|
||||||
- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
|
|
||||||
@@ -79,11 +79,13 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
|||||||
context.handleUpdate(patternData, reset);
|
context.handleUpdate(patternData, reset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const autoResetPatternOnChange = !isUdels();
|
||||||
|
|
||||||
function UserPatterns({ context }) {
|
function UserPatterns({ context }) {
|
||||||
const activePattern = useActivePattern();
|
const activePattern = useActivePattern();
|
||||||
const viewingPatternStore = useViewingPatternData();
|
const viewingPatternStore = useViewingPatternData();
|
||||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||||
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
|
const { userPatterns, patternFilter } = useSettings();
|
||||||
const viewingPatternID = viewingPatternData?.id;
|
const viewingPatternID = viewingPatternData?.id;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||||
@@ -133,13 +135,13 @@ function UserPatterns({ context }) {
|
|||||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||||
{/* {patternFilter === patternFilterName.user && ( */}
|
{/* {patternFilter === patternFilterName.user && ( */}
|
||||||
<PatternButtons
|
<PatternButtons
|
||||||
onClick={(id) => {
|
onClick={(id) =>
|
||||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
updateCodeWindow(
|
||||||
|
context,
|
||||||
if (context.started && activePattern === id) {
|
{ ...userPatterns[id], collection: userPattern.collection },
|
||||||
context.handleEvaluate();
|
autoResetPatternOnChange,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
patterns={userPatterns}
|
patterns={userPatterns}
|
||||||
started={context.started}
|
started={context.started}
|
||||||
activePattern={activePattern}
|
activePattern={activePattern}
|
||||||
@@ -186,14 +188,17 @@ function FeaturedPatterns({ context }) {
|
|||||||
const examplePatterns = useExamplePatterns();
|
const examplePatterns = useExamplePatterns();
|
||||||
const collections = examplePatterns.collections;
|
const collections = examplePatterns.collections;
|
||||||
const patterns = collections.get(patternFilterName.featured);
|
const patterns = collections.get(patternFilterName.featured);
|
||||||
const { patternAutoStart } = useSettings();
|
|
||||||
return (
|
return (
|
||||||
<PatternPageWithPagination
|
<PatternPageWithPagination
|
||||||
patterns={patterns}
|
patterns={patterns}
|
||||||
context={context}
|
context={context}
|
||||||
initialPage={featuredPageNum}
|
initialPage={featuredPageNum}
|
||||||
patternOnClick={(id) => {
|
patternOnClick={(id) => {
|
||||||
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.featured }, patternAutoStart);
|
updateCodeWindow(
|
||||||
|
context,
|
||||||
|
{ ...patterns[id], collection: patternFilterName.featured },
|
||||||
|
autoResetPatternOnChange,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
paginationOnChange={async (pageNum) => {
|
paginationOnChange={async (pageNum) => {
|
||||||
await loadAndSetFeaturedPatterns(pageNum - 1);
|
await loadAndSetFeaturedPatterns(pageNum - 1);
|
||||||
@@ -208,14 +213,13 @@ function LatestPatterns({ context }) {
|
|||||||
const examplePatterns = useExamplePatterns();
|
const examplePatterns = useExamplePatterns();
|
||||||
const collections = examplePatterns.collections;
|
const collections = examplePatterns.collections;
|
||||||
const patterns = collections.get(patternFilterName.public);
|
const patterns = collections.get(patternFilterName.public);
|
||||||
const { patternAutoStart } = useSettings();
|
|
||||||
return (
|
return (
|
||||||
<PatternPageWithPagination
|
<PatternPageWithPagination
|
||||||
patterns={patterns}
|
patterns={patterns}
|
||||||
context={context}
|
context={context}
|
||||||
initialPage={latestPageNum}
|
initialPage={latestPageNum}
|
||||||
patternOnClick={(id) => {
|
patternOnClick={(id) => {
|
||||||
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, patternAutoStart);
|
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
|
||||||
}}
|
}}
|
||||||
paginationOnChange={async (pageNum) => {
|
paginationOnChange={async (pageNum) => {
|
||||||
await loadAndSetPublicPatterns(pageNum - 1);
|
await loadAndSetPublicPatterns(pageNum - 1);
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export function SettingsTab({ started }) {
|
|||||||
multiChannelOrbits,
|
multiChannelOrbits,
|
||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
@@ -305,11 +304,6 @@ export function SettingsTab({ started }) {
|
|||||||
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
|
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
|
||||||
value={isCSSAnimationDisabled}
|
value={isCSSAnimationDisabled}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
|
||||||
label="Auto-start pattern on pattern change"
|
|
||||||
onChange={(cbEvent) => settingsMap.setKey('patternAutoStart', cbEvent.target.checked)}
|
|
||||||
value={patternAutoStart}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||||
<FormItem label="Reset Settings">
|
<FormItem label="Reset Settings">
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ export function SoundsTab() {
|
|||||||
|
|
||||||
// stop current sound on mouseup
|
// stop current sound on mouseup
|
||||||
useEvent('mouseup', () => {
|
useEvent('mouseup', () => {
|
||||||
const ref = trigRef.current;
|
const t = trigRef.current;
|
||||||
trigRef.current = undefined;
|
trigRef.current = undefined;
|
||||||
ref?.stop?.(getAudioContext().currentTime + 0.01);
|
t?.then((ref) => {
|
||||||
|
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||||
@@ -122,19 +124,12 @@ export function SoundsTab() {
|
|||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
};
|
};
|
||||||
soundPreviewIdx++;
|
soundPreviewIdx++;
|
||||||
|
const time = ctx.currentTime + 0.05;
|
||||||
const onended = () => trigRef.current?.node?.disconnect();
|
const onended = () => trigRef.current?.node?.disconnect();
|
||||||
try {
|
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||||
// Pre-load the sample by calling onTrigger with a future time
|
trigRef.current.then((ref) => {
|
||||||
// This triggers the loading but schedules playback for later
|
connectToDestination(ref?.node);
|
||||||
const time = ctx.currentTime + 0.5; // Give 500ms for loading
|
});
|
||||||
const ref = await onTrigger(time, params, onended);
|
|
||||||
trigRef.current = ref;
|
|
||||||
if (ref?.node) {
|
|
||||||
connectToDestination(ref.node);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to trigger sound:', err);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { registerSampleSource } from '@strudel/webaudio';
|
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
import { isAudioFile } from './files.mjs';
|
||||||
import { logger } from '@strudel/core';
|
import { getSoundIndex, logger } from '@strudel/core';
|
||||||
|
import { registerSound } from '@strudel/webaudio';
|
||||||
|
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
|
||||||
|
|
||||||
//utilites for writing and reading to the indexdb
|
//utilites for writing and reading to the indexdb
|
||||||
|
|
||||||
@@ -25,6 +27,15 @@ function clearAllIDB() {
|
|||||||
|
|
||||||
export function clearIDB(dbName) {
|
export function clearIDB(dbName) {
|
||||||
return window.indexedDB.deleteDatabase(dbName);
|
return window.indexedDB.deleteDatabase(dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function registerSampleFromIdb(key, bank, params) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// queries the DB, and registers the sounds so they can be played
|
// queries the DB, and registers the sounds so they can be played
|
||||||
@@ -52,31 +63,76 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitRelativePath = soundFile.id.split('/');
|
const splitRelativePath = soundFile.id.split('/');
|
||||||
let parentDirectory =
|
let parentDirectory =
|
||||||
//fallback to file name before period and seperator if no parent directory
|
//fallback to file name before period and seperator if no parent directory
|
||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
const blob = soundFile.blob;
|
|
||||||
|
|
||||||
return blobToDataUrl(blob).then((soundPath) => {
|
// const blob = soundFile.blob;
|
||||||
|
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
|
||||||
titlePathMap.set(title, soundPath);
|
titlePathMap.set(title, soundFile.id);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
sounds.set(parentDirectory, titlePathMap);
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
// return blobToDataUrl(blob).then((soundPath) => {
|
||||||
|
// const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
|
||||||
|
// titlePathMap.set(title, soundPath);
|
||||||
|
|
||||||
|
// sounds.set(parentDirectory, titlePathMap);
|
||||||
|
// return;
|
||||||
|
// });
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((titlePathMap, key) => {
|
||||||
const value = Array.from(titlePathMap.keys())
|
const bank = Array.from(titlePathMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => titlePathMap.get(title));
|
||||||
|
|
||||||
registerSampleSource(key, value, { prebake: false });
|
|
||||||
|
|
||||||
|
|
||||||
|
registerSound(key, async (t, hapValue, onended) => {
|
||||||
|
const { s, n = 0 } = hapValue;
|
||||||
|
const index = getSoundIndex(n, bank.length);
|
||||||
|
let {transpose, label} = getCommonSampleInfo(hapValue)
|
||||||
|
const storeKey = bank[index];
|
||||||
|
|
||||||
|
openDB(config, (objectStore) => {
|
||||||
|
const getRequest = objectStore.get(storeKey);
|
||||||
|
getRequest.onsuccess = async (event) => {
|
||||||
|
const result = event.target.result;
|
||||||
|
let buffer = result?.blob.arrayBuffer ? await result.blob.arrayBuffer() : null;
|
||||||
|
console.info(buffer)
|
||||||
|
if (buffer) {
|
||||||
|
const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
||||||
|
onTriggerSample(t, hapValue, onended, bufferSource)
|
||||||
|
} else {
|
||||||
|
logger(`Could not load sample for ${storeKey}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
// const buffer = objectStore.getKey(key)
|
||||||
|
// const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
||||||
|
|
||||||
|
// onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined))
|
||||||
|
|
||||||
|
}, {
|
||||||
|
type: 'sample',
|
||||||
|
samples: bank,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// registerSampleSource(key, value, { prebake: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
logger('imported sounds registered!', 'success');
|
logger('imported sounds registered!', 'success');
|
||||||
@@ -92,7 +148,11 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
|
|
||||||
async function blobToDataUrl(blob) {
|
async function blobToDataUrl(blob) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
resolve(URL.createObjectURL(blob));
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (event) {
|
||||||
|
resolve(event.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import './files.mjs';
|
|||||||
|
|
||||||
const { BASE_URL } = import.meta.env;
|
const { BASE_URL } = import.meta.env;
|
||||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||||
const baseCDN = 'https://strudel.b-cdn.net';
|
|
||||||
|
|
||||||
export async function prebake() {
|
export async function prebake() {
|
||||||
// https://archive.org/details/SalamanderGrandPianoV3
|
// https://archive.org/details/SalamanderGrandPianoV3
|
||||||
@@ -20,23 +19,23 @@ export async function prebake() {
|
|||||||
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
|
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
|
||||||
// seems to be a problem with soundfont2
|
// seems to be a problem with soundfont2
|
||||||
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
|
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
|
||||||
samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }),
|
samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }),
|
||||||
// https://github.com/sgossner/VCSL/
|
// https://github.com/sgossner/VCSL/
|
||||||
// https://api.github.com/repositories/126427031/contents/
|
// https://api.github.com/repositories/126427031/contents/
|
||||||
// LICENSE: CC0 general-purpose
|
// LICENSE: CC0 general-purpose
|
||||||
samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }),
|
samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
|
||||||
samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, {
|
samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
|
||||||
prebake: true,
|
prebake: true,
|
||||||
tag: 'drum-machines',
|
tag: 'drum-machines',
|
||||||
}),
|
}),
|
||||||
samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, {
|
samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
|
||||||
prebake: true,
|
prebake: true,
|
||||||
tag: 'drum-machines',
|
tag: 'drum-machines',
|
||||||
}),
|
}),
|
||||||
samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, {
|
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
|
||||||
prebake: true,
|
prebake: true,
|
||||||
}),
|
}),
|
||||||
samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }),
|
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
|
||||||
samples(
|
samples(
|
||||||
{
|
{
|
||||||
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
|
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
|
||||||
@@ -146,14 +145,14 @@ export async function prebake() {
|
|||||||
'num/20.wav',
|
'num/20.wav',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
`${baseCDN}/Dirt-Samples/`,
|
'github:tidalcycles/dirt-samples',
|
||||||
{
|
{
|
||||||
prebake: true,
|
prebake: true,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`);
|
aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxPan = noteToMidi('C8');
|
const maxPan = noteToMidi('C8');
|
||||||
|
|||||||
@@ -96,11 +96,6 @@ export function useSettings() {
|
|||||||
isPanelOpen: parseBoolean(state.isPanelOpen),
|
isPanelOpen: parseBoolean(state.isPanelOpen),
|
||||||
userPatterns: userPatterns,
|
userPatterns: userPatterns,
|
||||||
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
|
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
|
||||||
patternAutoStart: isUdels()
|
|
||||||
? false
|
|
||||||
: state.patternAutoStart === undefined
|
|
||||||
? true
|
|
||||||
: parseBoolean(state.patternAutoStart),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user