mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -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
|
||||||
|
|||||||
@@ -20,8 +20,5 @@
|
|||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel/webaudio": "workspace:*"
|
"@strudel/webaudio": "workspace:*"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,5 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/web": "workspace:*"
|
"@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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-266
@@ -1288,196 +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
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
|
|
||||||
*/
|
|
||||||
export const { lprate } = registerControl('lprate');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cycle-synced rate of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpsync
|
|
||||||
* @param {number | Pattern} rate rate in cycles
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
|
|
||||||
*/
|
|
||||||
export const { lpsync } = registerControl('lpsync');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the lowpass filter
|
|
||||||
*
|
|
||||||
* @name lpdepth
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const { lpdepth } = registerControl('lpdepth');
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the lowpass filter, in HZ
|
|
||||||
*
|
|
||||||
* @name lpdepthfrequency
|
|
||||||
* @synonyms
|
|
||||||
* lpdethfreq
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the bandpass filter, in HZ
|
|
||||||
*
|
|
||||||
* @name bpdepthfrequency
|
|
||||||
* @synonyms
|
|
||||||
* bpdethfreq
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, hpdepthfreq } = registerControl('hpdepth');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depth of the LFO for the hipass filter, in hz
|
|
||||||
*
|
|
||||||
* @name hpdepthfrequency
|
|
||||||
* @synonyms
|
|
||||||
* hpdethfreq
|
|
||||||
* @param {number | Pattern} depth depth of modulation
|
|
||||||
* @example
|
|
||||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
@@ -1865,12 +1675,12 @@ export const { nudge } = registerControl('nudge');
|
|||||||
* Sets the default octave of a synth.
|
* Sets the default octave of a synth.
|
||||||
*
|
*
|
||||||
* @name octave
|
* @name octave
|
||||||
* @synonyms oct
|
|
||||||
* @param {number | Pattern} octave octave number
|
* @param {number | Pattern} octave octave number
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
|
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
||||||
|
* @superDirtOnly
|
||||||
*/
|
*/
|
||||||
export const { octave, oct } = registerControl('octave', 'oct');
|
export const { octave } = registerControl('octave');
|
||||||
|
|
||||||
// ['ophatdecay'],
|
// ['ophatdecay'],
|
||||||
// TODO: example
|
// TODO: example
|
||||||
@@ -1878,7 +1688,6 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
|||||||
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
||||||
*
|
*
|
||||||
* @name orbit
|
* @name orbit
|
||||||
* @synonyms o
|
|
||||||
* @param {number | Pattern} number
|
* @param {number | Pattern} number
|
||||||
* @example
|
* @example
|
||||||
* stack(
|
* stack(
|
||||||
@@ -1886,7 +1695,7 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
|||||||
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
|
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export const { orbit } = registerControl('orbit', 'o');
|
export const { orbit } = registerControl('orbit');
|
||||||
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||||
export const { overgain } = registerControl('overgain');
|
export const { overgain } = registerControl('overgain');
|
||||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||||
@@ -1941,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']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2310,6 +2072,7 @@ export const { tsdelay } = registerControl('tsdelay');
|
|||||||
export const { real } = registerControl('real');
|
export const { real } = registerControl('real');
|
||||||
export const { imag } = registerControl('imag');
|
export const { imag } = registerControl('imag');
|
||||||
export const { enhance } = registerControl('enhance');
|
export const { enhance } = registerControl('enhance');
|
||||||
|
export const { partials } = registerControl('partials');
|
||||||
export const { comb } = registerControl('comb');
|
export const { comb } = registerControl('comb');
|
||||||
export const { smear } = registerControl('smear');
|
export const { smear } = registerControl('smear');
|
||||||
export const { scram } = registerControl('scram');
|
export const { scram } = registerControl('scram');
|
||||||
@@ -2557,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*
|
||||||
*
|
*
|
||||||
@@ -3624,66 +3606,3 @@ for (const name of distAlgoNames) {
|
|||||||
return this.distort(argsPat);
|
return this.distort(argsPat);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns a list of patterns into a single pattern which outputs list-values
|
|
||||||
*
|
|
||||||
* @name parray
|
|
||||||
* @returns Pattern
|
|
||||||
*/
|
|
||||||
export const parray = (pats) => {
|
|
||||||
const pack = (...xs) => xs;
|
|
||||||
let acc = pure(curry(pack, null, pats.length));
|
|
||||||
for (const p of pats) acc = acc.appBoth(reify(p));
|
|
||||||
return acc;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _ensureListPattern = (list) => {
|
|
||||||
if (Array.isArray(list)) {
|
|
||||||
return parray(list);
|
|
||||||
}
|
|
||||||
return reify(list);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
|
|
||||||
*
|
|
||||||
* Can also be used to create a new synth via `s('user').partials(...)`
|
|
||||||
*
|
|
||||||
* @name partials
|
|
||||||
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
|
|
||||||
* @example
|
|
||||||
* s("user").seg(16).n(irand(8)).scale("A:major")
|
|
||||||
* .partials([1, 0, 1, 0, 0, 1])
|
|
||||||
* @example
|
|
||||||
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
|
|
||||||
* .partials(binaryL(irand(256).add("1")))
|
|
||||||
*/
|
|
||||||
Pattern.prototype.partials = function (list) {
|
|
||||||
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also create a top-level function
|
|
||||||
export const partials = (list) => {
|
|
||||||
return _ensureListPattern(list).as('partials');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
|
|
||||||
*
|
|
||||||
* @name phases
|
|
||||||
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
|
|
||||||
* @example
|
|
||||||
* // Phase cancellation
|
|
||||||
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
|
|
||||||
* .partials(partials([1, 1, 1]))
|
|
||||||
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
|
|
||||||
*/
|
|
||||||
Pattern.prototype.phases = function (list) {
|
|
||||||
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also create a top-level function
|
|
||||||
export const phases = (list) => {
|
|
||||||
return _ensureListPattern(list).as('phases');
|
|
||||||
};
|
|
||||||
|
|||||||
+5
-16
@@ -152,9 +152,9 @@ export function repl({
|
|||||||
// allows muting a pattern x with x_ or _x
|
// allows muting a pattern x with x_ or _x
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
if (id.includes('$')) {
|
if (id === '$') {
|
||||||
// allows adding anonymous patterns with $:
|
// allows adding anonymous patterns with $:
|
||||||
id = `${id}${anonymousIndex}`;
|
id = `$${anonymousIndex}`;
|
||||||
anonymousIndex++;
|
anonymousIndex++;
|
||||||
}
|
}
|
||||||
pPatterns[id] = this;
|
pPatterns[id] = this;
|
||||||
@@ -215,19 +215,8 @@ export function repl({
|
|||||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
if (Object.keys(pPatterns).length) {
|
if (Object.keys(pPatterns).length) {
|
||||||
let patterns = [];
|
let patterns = [];
|
||||||
let soloActive = false;
|
|
||||||
for (const [key, value] of Object.entries(pPatterns)) {
|
for (const [key, value] of Object.entries(pPatterns)) {
|
||||||
// handle soloed patterns ex: S$: s("bd!4")
|
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||||
const isSolod = key.length > 1 && key.startsWith('S');
|
|
||||||
if (isSolod && soloActive === false) {
|
|
||||||
// first time we see a soloed pattern, clear existing patterns
|
|
||||||
patterns = [];
|
|
||||||
soloActive = true;
|
|
||||||
}
|
|
||||||
if (!soloActive || (soloActive && isSolod)) {
|
|
||||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
|
||||||
patterns.push(valWithState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (eachTransform) {
|
if (eachTransform) {
|
||||||
// Explicit lambda so only element (not index and array) are passed
|
// Explicit lambda so only element (not index and array) are passed
|
||||||
@@ -238,8 +227,8 @@ export function repl({
|
|||||||
pattern = eachTransform(pattern);
|
pattern = eachTransform(pattern);
|
||||||
}
|
}
|
||||||
if (allTransforms.length) {
|
if (allTransforms.length) {
|
||||||
for (const transform of allTransforms) {
|
for (let i in allTransforms) {
|
||||||
pattern = transform(pattern);
|
pattern = allTransforms[i](pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|||||||
export const run = (n) => saw.range(0, n).round().segment(n);
|
export const run = (n) => saw.range(0, n).round().segment(n);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a binary pattern from a number.
|
* Creates a pattern from a binary number.
|
||||||
*
|
*
|
||||||
* @name binary
|
* @name binary
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
@@ -242,7 +242,7 @@ export const binary = (n) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a binary pattern from a number, padded to n bits long.
|
* Creates a pattern from a binary number, padded to n bits long.
|
||||||
*
|
*
|
||||||
* @name binaryN
|
* @name binaryN
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
@@ -258,51 +258,6 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a binary list pattern from a number.
|
|
||||||
*
|
|
||||||
* @name binaryL
|
|
||||||
* @param {number} n - input number to convert to binary
|
|
||||||
* s("saw").seg(8)
|
|
||||||
* .partials(binaryL(irand(4096).add(1)))
|
|
||||||
*/
|
|
||||||
export const binaryL = (n) => {
|
|
||||||
const nBits = reify(n).log2(0).floor().add(1);
|
|
||||||
return binaryNL(n, nBits);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a binary list pattern from a number, padded to n bits long.
|
|
||||||
*
|
|
||||||
* @name binaryNL
|
|
||||||
* @param {number} n - input number to convert to binary
|
|
||||||
* @param {number} nBits - pattern length, defaults to 16
|
|
||||||
*/
|
|
||||||
export const binaryNL = (n, nBits = 16) => {
|
|
||||||
return reify(n)
|
|
||||||
.withValue((v) => (bits) => {
|
|
||||||
const bList = [];
|
|
||||||
for (let i = bits - 1; i >= 0; i--) {
|
|
||||||
bList.push((v >> i) & 1);
|
|
||||||
}
|
|
||||||
return bList;
|
|
||||||
})
|
|
||||||
.appLeft(reify(nBits));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a list of random numbers of the given length
|
|
||||||
*
|
|
||||||
* @name randL
|
|
||||||
* @param {number} n Number of random numbers to sample
|
|
||||||
* @example
|
|
||||||
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
|
||||||
* .partials(randL(8))
|
|
||||||
*/
|
|
||||||
export const randL = (n) => {
|
|
||||||
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t) => {
|
return signal((t) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||||
@@ -524,7 +479,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
* @example
|
* @example
|
||||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||||
* @example
|
* @example
|
||||||
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
|
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||||
* @example
|
* @example
|
||||||
* // The probability can itself be a pattern
|
* // The probability can itself be a pattern
|
||||||
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
+60
-49
@@ -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);
|
||||||
const WS_PORT = 8080; // WebSocket server port
|
if (i !== -1) {
|
||||||
const OSC_REMOTE_IP = '127.0.0.1';
|
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||||
const OSC_REMOTE_PORT = 57120;
|
if (nextIsFlag) return true;
|
||||||
|
return args[i + 1];
|
||||||
const udpPort = new osc.UDPPort({
|
|
||||||
localAddress: '0.0.0.0',
|
|
||||||
localPort: 0,
|
|
||||||
remoteAddress: OSC_REMOTE_IP,
|
|
||||||
remotePort: OSC_REMOTE_PORT,
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
let udpClientPort = Number(getArgValue('--port')) || 57120;
|
||||||
} catch (err) {
|
let debug = Number(getArgValue('--debug')) || 0;
|
||||||
console.error('Error parsing message:', err);
|
|
||||||
|
const config = {
|
||||||
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
|
udpServer: {
|
||||||
|
host: 'localhost', // @param {string} Hostname of udp server to bind to
|
||||||
|
port: 57121, // @param {number} Port of udp client for messaging
|
||||||
|
// enabling the following line will receive tidal messages:
|
||||||
|
// port: 57120, // @param {number} Port of udp client for messaging
|
||||||
|
exclusive: false, // @param {boolean} Exclusive flag
|
||||||
|
},
|
||||||
|
udpClient: {
|
||||||
|
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
||||||
|
port: udpClientPort, // @param {number} Port of udp client for messaging
|
||||||
|
},
|
||||||
|
wsServer: {
|
||||||
|
host: 'localhost', // @param {string} Hostname of WebSocket server
|
||||||
|
port: 8080, // @param {number} Port of WebSocket server
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
osc.on('*', (message) => {
|
||||||
|
const { address, args } = message;
|
||||||
|
let str = '';
|
||||||
|
for (let i = 0; i < args.length; i += 2) {
|
||||||
|
str += `${args[i]}: ${args[i + 1]} `;
|
||||||
}
|
}
|
||||||
|
console.log(`${address} ${str}`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ws.on('close', () => {
|
osc.on('error', (message) => {
|
||||||
console.log('WebSocket connection closed');
|
if (message.toString().includes('EADDRINUSE')) {
|
||||||
});
|
console.log(`------ ERROR -------
|
||||||
|
a server is already running on port 57121! to stop it:
|
||||||
|
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
|
||||||
|
2. re-run the osc server
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
osc.open();
|
||||||
|
|
||||||
|
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import readline from 'readline';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
const LOG = !!process.env.LOG || false;
|
const LOG = !!process.env.LOG || false;
|
||||||
const PORT = process.env.PORT || 5432;
|
|
||||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||||
|
|
||||||
const isAudioFile = (f) => {
|
const isAudioFile = (f) => {
|
||||||
@@ -55,6 +54,7 @@ async function getBanks(directory, flat = false) {
|
|||||||
banks[bank].push(subDir);
|
banks[bank].push(subDir);
|
||||||
return subDir;
|
return subDir;
|
||||||
});
|
});
|
||||||
|
banks._base = `http://localhost:5432`;
|
||||||
return { banks, files };
|
return { banks, files };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +134,8 @@ const server = http.createServer(async (req, res) => {
|
|||||||
readStream.pipe(res);
|
readStream.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const PORT = process.env.PORT || 5432;
|
||||||
const IP_ADDRESS = '0.0.0.0';
|
const IP_ADDRESS = '0.0.0.0';
|
||||||
let IP;
|
let IP;
|
||||||
const networkInterfaces = os.networkInterfaces();
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
|
|||||||
- `decay`: seconds of decay phase
|
- `decay`: seconds of decay phase
|
||||||
- `sustain`: gain of sustain phase
|
- `sustain`: gain of sustain phase
|
||||||
- `release`: seconds of release phase
|
- `release`: seconds of release phase
|
||||||
- `deadline`: seconds from audio context initialization before playing the sound (getAudioContextCurrentTime() = immediate)
|
- `deadline`: seconds until the sound should play (0 = immediate)
|
||||||
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
||||||
|
|
||||||
### registerSynthSounds()
|
### registerSynthSounds()
|
||||||
|
|||||||
+39
-100
@@ -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,76 +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, cycle) {
|
|
||||||
let {
|
|
||||||
frequency,
|
|
||||||
anchor,
|
|
||||||
env,
|
|
||||||
type,
|
|
||||||
model,
|
|
||||||
q = 1,
|
|
||||||
drive = 0.69,
|
|
||||||
depth,
|
|
||||||
depthfrequency,
|
|
||||||
dcoffset = -0.5,
|
|
||||||
skew,
|
|
||||||
shape,
|
|
||||||
rate,
|
|
||||||
sync,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
let frequencyParam, filter;
|
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
||||||
|
const curve = 'exponential';
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||||
|
let filter;
|
||||||
|
let frequencyParam;
|
||||||
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
|
// envelope is active when any of these values is set
|
||||||
const hasEnvelope = [...envelopeValues, env].some((v) => v !== undefined);
|
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
|
||||||
// Apply ADSR to filter frequency
|
// Apply ADSR to filter frequency
|
||||||
if (hasEnvelope) {
|
if (hasEnvelope !== undefined) {
|
||||||
env = nanFallback(env, 1, true);
|
fenv = nanFallback(fenv, 1, true);
|
||||||
anchor = nanFallback(anchor, 0, true);
|
fanchor = nanFallback(fanchor, 0, true);
|
||||||
const envAbs = Math.abs(env);
|
const fenvAbs = Math.abs(fenv);
|
||||||
const offset = envAbs * anchor;
|
const offset = fenvAbs * fanchor;
|
||||||
let min = clamp(2 ** -offset * frequency, 0, 20000);
|
let min = clamp(2 ** -offset * frequency, 0, 20000);
|
||||||
let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000);
|
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
|
||||||
if (env < 0) [min, max] = [max, min];
|
if (fenv < 0) [min, max] = [max, min];
|
||||||
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential');
|
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
|
||||||
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync != null) {
|
|
||||||
rate = cps * sync;
|
|
||||||
}
|
|
||||||
const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined);
|
|
||||||
if (hasLFO) {
|
|
||||||
depth = depth ?? 1;
|
|
||||||
const time = cycle / cps;
|
|
||||||
const modDepth = depthfrequency ?? (depth ?? 1) * frequency;
|
|
||||||
const lfoValues = {
|
|
||||||
depth: modDepth,
|
|
||||||
dcoffset,
|
|
||||||
skew,
|
|
||||||
shape,
|
|
||||||
frequency: rate ?? cps,
|
|
||||||
min: -frequency + 30,
|
|
||||||
max: 20000 - frequency,
|
|
||||||
time,
|
|
||||||
curve: 1,
|
|
||||||
};
|
|
||||||
getParamLfo(context, frequencyParam, start, end, lfoValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,15 +241,7 @@ export function drywet(dry, wet, wetAmount = 0) {
|
|||||||
let mix = ac.createGain();
|
let mix = ac.createGain();
|
||||||
dry_gain.connect(mix);
|
dry_gain.connect(mix);
|
||||||
wet_gain.connect(mix);
|
wet_gain.connect(mix);
|
||||||
return {
|
return mix;
|
||||||
node: mix,
|
|
||||||
onended: () => {
|
|
||||||
dry_gain.disconnect(mix);
|
|
||||||
wet_gain.disconnect(mix);
|
|
||||||
dry.disconnect(dry_gain);
|
|
||||||
wet.disconnect(wet_gain);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let curves = ['linear', 'exponential'];
|
let curves = ['linear', 'exponential'];
|
||||||
@@ -323,19 +276,10 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
gain.gain.value = vibmod * 100;
|
gain.gain.value = vibmod * 100;
|
||||||
vibratoOscillator.connect(gain);
|
vibratoOscillator.connect(gain);
|
||||||
gain.connect(param);
|
gain.connect(param);
|
||||||
vibratoOscillator.onended = () => {
|
|
||||||
gain.disconnect(param);
|
|
||||||
vibratoOscillator.disconnect(gain);
|
|
||||||
};
|
|
||||||
vibratoOscillator.start(t);
|
vibratoOscillator.start(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) {
|
||||||
@@ -380,9 +324,9 @@ const mod = (freq, range = 1, type = 'sine') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
osc.start();
|
osc.start();
|
||||||
const g = gainNode(range);
|
const g = new GainNode(ctx, { gain: range });
|
||||||
osc.connect(g); // -range, range
|
osc.connect(g); // -range, range
|
||||||
return { node: g, stop: (t) => osc.stop(t), osc: osc };
|
return { node: g, stop: (t) => osc.stop(t) };
|
||||||
};
|
};
|
||||||
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
||||||
const carrfreq = frequencyparam.value;
|
const carrfreq = frequencyparam.value;
|
||||||
@@ -434,11 +378,6 @@ export function applyFM(param, value, begin) {
|
|||||||
modulator.connect(envGain);
|
modulator.connect(envGain);
|
||||||
envGain.connect(param);
|
envGain.connect(param);
|
||||||
}
|
}
|
||||||
fmmod.osc.onended = () => {
|
|
||||||
envGain.disconnect();
|
|
||||||
modulator.disconnect();
|
|
||||||
fmmod.osc.disconnect();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { stop };
|
return { stop };
|
||||||
}
|
}
|
||||||
@@ -540,7 +479,7 @@ export const getDistortion = (distort, postgain, algorithm) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
let { note, freq, octave = 0 } = value;
|
let { note, freq } = value;
|
||||||
note = note || defaultNote;
|
note = note || defaultNote;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
note = noteToMidi(note); // e.g. c3 => 48
|
note = noteToMidi(note); // e.g. c3 => 48
|
||||||
@@ -549,7 +488,7 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|||||||
if (!freq && typeof note === 'number') {
|
if (!freq && typeof note === 'number') {
|
||||||
freq = midiToFreq(note); // + 48);
|
freq = midiToFreq(note); // + 48);
|
||||||
}
|
}
|
||||||
freq *= Math.pow(2, octave);
|
|
||||||
return Number(freq);
|
return Number(freq);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,8 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
|||||||
export function getNoiseMix(inputNode, wet, t) {
|
export function getNoiseMix(inputNode, wet, t) {
|
||||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||||
noiseOscillator.node.onended = () => noiseMix.onended();
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix.node,
|
node: noiseMix,
|
||||||
stop: (time) => noiseOscillator?.stop(time),
|
stop: (time) => noiseOscillator?.stop(time),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,8 +104,6 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
|
|||||||
player.start();
|
player.start();
|
||||||
context.oncomplete = function (event) {
|
context.oncomplete = function (event) {
|
||||||
callback(event.renderedBuffer);
|
callback(event.renderedBuffer);
|
||||||
filter.disconnect();
|
|
||||||
player.disconnect();
|
|
||||||
};
|
};
|
||||||
context.startRendering();
|
context.startRendering();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getBaseURL, 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}`;
|
||||||
@@ -211,7 +212,7 @@ export async function fetchSampleMap(url) {
|
|||||||
// not a browser
|
// not a browser
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const base = getBaseURL(url);
|
const base = url.split('/').slice(0, -1).join('/');
|
||||||
if (typeof fetch === 'undefined') {
|
if (typeof fetch === 'undefined') {
|
||||||
// skip fetch when in node / testing
|
// skip fetch when in node / testing
|
||||||
return;
|
return;
|
||||||
@@ -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,
|
||||||
@@ -481,7 +511,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||||
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
|
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
|
||||||
const ch = activeSoundSources.entries().next();
|
const ch = activeSoundSources.entries().next();
|
||||||
const source = ch.value[1].deref();
|
const source = ch.value[1];
|
||||||
const chainID = ch.value[0];
|
const chainID = ch.value[0];
|
||||||
const endTime = t + 0.25;
|
const endTime = t + 0.25;
|
||||||
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
|
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
|
||||||
@@ -513,7 +543,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
|
|
||||||
if (soundHandle) {
|
if (soundHandle) {
|
||||||
sourceNode = soundHandle.node;
|
sourceNode = soundHandle.node;
|
||||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
activeSoundSources.set(chainID, soundHandle);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||||
@@ -535,90 +565,57 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// gain stage
|
// gain stage
|
||||||
chain.push(gainNode(gain));
|
chain.push(gainNode(gain));
|
||||||
|
|
||||||
// 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,
|
||||||
depthfrequency: 'lpdepthfrequency',
|
drive,
|
||||||
shape: 'lpshape',
|
);
|
||||||
dcoffset: 'lpdc',
|
|
||||||
skew: 'lpskew',
|
|
||||||
};
|
|
||||||
const lpParams = pickAndRename(value, lpMap);
|
|
||||||
lpParams.type = 'lowpass';
|
|
||||||
let lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
|
||||||
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,
|
||||||
depthfrequency: 'hpdepthfrequency',
|
);
|
||||||
shape: 'hpshape',
|
|
||||||
dcoffset: 'hpdc',
|
|
||||||
skew: 'hpskew',
|
|
||||||
};
|
|
||||||
const hpParams = pickAndRename(value, hpMap);
|
|
||||||
hpParams.type = 'highpass';
|
|
||||||
let hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
|
||||||
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',
|
|
||||||
depthfrequency: 'bpdepthfrequency',
|
|
||||||
shape: 'bpshape',
|
|
||||||
dcoffset: 'bpdc',
|
|
||||||
skew: 'bpskew',
|
|
||||||
};
|
|
||||||
const bpParams = pickAndRename(value, bpMap);
|
|
||||||
bpParams.type = 'bandpass';
|
|
||||||
let bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
if (ftype === '24db') {
|
if (ftype === '24db') {
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
@@ -633,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) {
|
||||||
@@ -668,7 +664,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
curve: 1.5,
|
curve: 1.5,
|
||||||
});
|
});
|
||||||
lfo.connect(amGain.gain);
|
lfo.connect(amGain.gain);
|
||||||
audioNodes.push(lfo);
|
|
||||||
chain.push(amGain);
|
chain.push(amGain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,8 +691,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// delay
|
// delay
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
const send = orbitBus.sendDelay(post, delay);
|
orbitBus.sendDelay(post, delay);
|
||||||
audioNodes.push(send);
|
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
@@ -713,8 +707,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
const send = orbitBus.sendReverb(post, room);
|
orbitBus.sendReverb(post, room);
|
||||||
audioNodes.push(send);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (djf != null) {
|
if (djf != null) {
|
||||||
|
|||||||
@@ -78,11 +78,11 @@ export class Orbit {
|
|||||||
return this.reverbNode;
|
return this.reverbNode;
|
||||||
}
|
}
|
||||||
sendReverb(node, amount) {
|
sendReverb(node, amount) {
|
||||||
return effectSend(node, this.reverbNode, amount);
|
effectSend(node, this.reverbNode, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDelay(node, amount) {
|
sendDelay(node, amount) {
|
||||||
return effectSend(node, this.delayNode, amount);
|
effectSend(node, this.delayNode, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import {
|
|||||||
noises,
|
noises,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
['sqr', 'square'],
|
['sqr', 'square'],
|
||||||
@@ -48,17 +47,19 @@ export function registerSynthSounds() {
|
|||||||
[0.001, 0.05, 0.6, 0.01],
|
[0.001, 0.05, 0.6, 0.01],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let sound = getOscillator(s, t, value);
|
||||||
|
let { node: o, stop, triggerRelease } = sound;
|
||||||
|
|
||||||
// turn down
|
// turn down
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
|
|
||||||
let sound = getOscillator(s, t, value, () => {
|
const { duration } = value;
|
||||||
|
|
||||||
|
o.onended = () => {
|
||||||
|
o.disconnect();
|
||||||
g.disconnect();
|
g.disconnect();
|
||||||
onended();
|
onended();
|
||||||
});
|
};
|
||||||
|
|
||||||
let { node: o, stop, triggerRelease } = sound;
|
|
||||||
|
|
||||||
const { duration } = value;
|
|
||||||
|
|
||||||
const envGain = gainNode(1);
|
const envGain = gainNode(1);
|
||||||
let node = o.connect(g).connect(envGain);
|
let node = o.connect(g).connect(envGain);
|
||||||
@@ -413,13 +414,9 @@ export function registerSynthSounds() {
|
|||||||
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const PI2 = 2 * Math.PI;
|
export function waveformN(partials, type) {
|
||||||
export function waveformN(partials, phases, type) {
|
const real = new Float32Array(partials + 1);
|
||||||
const isList = typeof partials === 'object';
|
const imag = new Float32Array(partials + 1);
|
||||||
partials = isList ? partials : new Float32Array(partials).fill(1);
|
|
||||||
const len = partials.length;
|
|
||||||
const real = new Float32Array(len + 1);
|
|
||||||
const imag = new Float32Array(len + 1);
|
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const osc = ac.createOscillator();
|
const osc = ac.createOscillator();
|
||||||
|
|
||||||
@@ -427,29 +424,20 @@ export function waveformN(partials, phases, type) {
|
|||||||
sawtooth: (n) => [0, -1 / n],
|
sawtooth: (n) => [0, -1 / n],
|
||||||
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
|
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
|
||||||
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
|
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
|
||||||
user: (_n) => [0, 1],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!terms[type]) {
|
if (!terms[type]) {
|
||||||
throw new Error(`unknown wave type ${type}`);
|
throw new Error(`unknown wave type ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let n = 0; n < len; n++) {
|
real[0] = 0; // dc offset
|
||||||
const mag = partials[n];
|
imag[0] = 0;
|
||||||
const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset
|
let n = 1;
|
||||||
const phase = phases?.[n] ?? 0;
|
while (n <= partials) {
|
||||||
// Scale by `partials`
|
const [r, i] = terms[type](n);
|
||||||
let R = r * mag;
|
real[n] = r;
|
||||||
let I = i * mag;
|
imag[n] = i;
|
||||||
// Apply rotation by the phase
|
n++;
|
||||||
if (phase !== 0) {
|
|
||||||
const c = Math.cos(PI2 * phase);
|
|
||||||
const s = Math.sin(PI2 * phase);
|
|
||||||
R = c * R - s * I;
|
|
||||||
I = s * R + c * I;
|
|
||||||
}
|
|
||||||
real[n + 1] = R;
|
|
||||||
imag[n + 1] = I;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wave = ac.createPeriodicWave(real, imag);
|
const wave = ac.createPeriodicWave(real, imag);
|
||||||
@@ -458,28 +446,21 @@ export function waveformN(partials, phases, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expects one of waveforms as s
|
// expects one of waveforms as s
|
||||||
export function getOscillator(s, t, value, onended) {
|
export function getOscillator(s, t, value) {
|
||||||
const { duration, noise = 0 } = value;
|
let { n: partials, duration, noise = 0 } = value;
|
||||||
const partials = value.partials ?? value.n;
|
|
||||||
let o;
|
let o;
|
||||||
if (s === 'user' && !partials) {
|
|
||||||
logger(
|
|
||||||
`[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`,
|
|
||||||
);
|
|
||||||
s = 'triangle';
|
|
||||||
}
|
|
||||||
s = s === 'user' && !partials ? 'triangle' : s;
|
|
||||||
// If no partials are given, use stock waveforms
|
// If no partials are given, use stock waveforms
|
||||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
if (!partials || s === 'sine') {
|
||||||
o = getAudioContext().createOscillator();
|
o = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
}
|
}
|
||||||
// generate custom waveform if partials are given
|
// generate custom waveform if partials are given
|
||||||
else {
|
else {
|
||||||
o = waveformN(partials, value.phases, s);
|
o = waveformN(partials, s);
|
||||||
}
|
}
|
||||||
// set frequency
|
// set frequency
|
||||||
o.frequency.value = getFrequencyFromValue(value);
|
o.frequency.value = getFrequencyFromValue(value);
|
||||||
|
o.start(t);
|
||||||
|
|
||||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||||
|
|
||||||
@@ -492,13 +473,6 @@ export function getOscillator(s, t, value, onended) {
|
|||||||
noiseMix = getNoiseMix(o, noise, t);
|
noiseMix = getNoiseMix(o, noise, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
o.onended = () => {
|
|
||||||
o.disconnect();
|
|
||||||
noiseMix?.node.disconnect();
|
|
||||||
onended();
|
|
||||||
};
|
|
||||||
o.start(t);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix?.node || o,
|
node: noiseMix?.node || o,
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
|
|||||||
@@ -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,21 +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) => {
|
|
||||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBaseURL = (url) => {
|
export function getCommonSampleInfo(hapValue) {
|
||||||
try {
|
const { s, n = 0 } = hapValue;
|
||||||
// For real URLs
|
let midi = valueToMidi(hapValue, 36);
|
||||||
return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
} catch {
|
const label = `${s}:${n}`;
|
||||||
// For pseudo URLS
|
return { transpose, label };
|
||||||
return url.split('/').slice(0, -1).join('/');
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfoFromBank } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
@@ -190,7 +190,6 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||||||
if (url.startsWith('local:')) {
|
if (url.startsWith('local:')) {
|
||||||
url = `http://localhost:5432`;
|
url = `http://localhost:5432`;
|
||||||
}
|
}
|
||||||
const base = getBaseURL(url);
|
|
||||||
if (typeof fetch !== 'function') {
|
if (typeof fetch !== 'function') {
|
||||||
// not a browser
|
// not a browser
|
||||||
return;
|
return;
|
||||||
@@ -201,7 +200,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||||||
}
|
}
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => _processTables(json, base, frameLen, options))
|
.then((json) => _processTables(json, url, frameLen, options))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new Error(`error loading "${url}"`);
|
throw new Error(`error loading "${url}"`);
|
||||||
@@ -217,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) {
|
||||||
|
|||||||
+171
-235
@@ -6,57 +6,51 @@ import OLAProcessor from './ola-processor';
|
|||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
|
||||||
const blockSize = 128;
|
|
||||||
const PI = Math.PI;
|
|
||||||
const TWO_PI = 2 * PI;
|
|
||||||
const INVSR = 1 / sampleRate;
|
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
const lerp = (a, b, n) => n * (b - a) + a;
|
const lerp = (a, b, n) => n * (b - a) + a;
|
||||||
const pv = (arr, n) => arr[n] ?? arr[0];
|
const pv = (arr, n) => arr[n] ?? arr[0];
|
||||||
const frac = (x) => x - Math.floor(x);
|
const frac = (x) => x - Math.floor(x);
|
||||||
|
const ffloor = (x) => x | 0; // fast floor for non-negative
|
||||||
|
|
||||||
// Fast integer ops for non-negative values
|
const getUnisonDetune = (unison, detune, voiceIndex) => {
|
||||||
const ffloor = (x) => x | 0;
|
|
||||||
const fround = (x) => ffloor(x + 0.5);
|
|
||||||
const fceil = (x) => ffloor(x + 1);
|
|
||||||
const ffrac = (x) => x - ffloor(x);
|
|
||||||
|
|
||||||
const fast_tanh = (x) => {
|
|
||||||
const x2 = x ** 2;
|
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optimized per-voice detuner which precomputes constants
|
|
||||||
const getDetuner = (unison, detune) => {
|
|
||||||
if (unison < 2) {
|
if (unison < 2) {
|
||||||
return (_voiceIdx) => 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const scale = detune / (unison - 1);
|
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||||
const center = detune * 0.5;
|
|
||||||
return (voiceIdx) => voiceIdx * scale - center;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||||
return frequency * Math.pow(2, detune / 12);
|
return frequency * Math.pow(2, detune / 12);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||||
|
function wrapPhase(phase, maxPhase = 1) {
|
||||||
|
if (phase >= maxPhase) {
|
||||||
|
phase -= maxPhase;
|
||||||
|
} else if (phase < 0) {
|
||||||
|
phase += maxPhase;
|
||||||
|
}
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
const blockSize = 128;
|
||||||
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
||||||
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||||
function polyBlep(phase, dt) {
|
function polyBlep(phase, dt) {
|
||||||
dt = Math.min(dt, 1 - dt);
|
dt = Math.min(dt, 1 - dt);
|
||||||
const invdt = 1 / dt;
|
|
||||||
// Start of cycle
|
// Start of cycle
|
||||||
if (phase < dt) {
|
if (phase < dt) {
|
||||||
phase *= invdt;
|
phase /= dt;
|
||||||
return 2 * phase - phase ** 2 - 1;
|
// 2 * (phase - phase^2/2 - 0.5)
|
||||||
|
return phase + phase - phase * phase - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End of cycle
|
// End of cycle
|
||||||
else if (phase > 1 - dt) {
|
else if (phase > 1 - dt) {
|
||||||
phase = (phase - 1) * invdt;
|
phase = (phase - 1) / dt;
|
||||||
return phase ** 2 + 2 * phase + 1;
|
// 2 * (phase^2/2 + phase + 0.5)
|
||||||
|
return phase * phase + phase + phase + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0 otherwise
|
// 0 otherwise
|
||||||
else {
|
else {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -72,7 +66,7 @@ const waveshapes = {
|
|||||||
return phase / skew;
|
return phase / skew;
|
||||||
},
|
},
|
||||||
sine(phase) {
|
sine(phase) {
|
||||||
return Math.sin(TWO_PI * phase) * 0.5 + 0.5;
|
return Math.sin(Math.PI * 2 * phase) * 0.5 + 0.5;
|
||||||
},
|
},
|
||||||
ramp(phase) {
|
ramp(phase) {
|
||||||
return phase;
|
return phase;
|
||||||
@@ -106,6 +100,12 @@ const waveshapes = {
|
|||||||
return v - polyBlep(phase, dt);
|
return v - polyBlep(phase, dt);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
function getParamValue(block, param) {
|
||||||
|
if (param.length > 1) {
|
||||||
|
return param[block];
|
||||||
|
}
|
||||||
|
return param[0];
|
||||||
|
}
|
||||||
|
|
||||||
const waveShapeNames = Object.keys(waveshapes);
|
const waveShapeNames = Object.keys(waveshapes);
|
||||||
class LFOProcessor extends AudioWorkletProcessor {
|
class LFOProcessor extends AudioWorkletProcessor {
|
||||||
@@ -165,9 +165,9 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
const blockSize = output[0].length ?? 0;
|
const blockSize = output[0].length ?? 0;
|
||||||
|
|
||||||
if (this.phase == null) {
|
if (this.phase == null) {
|
||||||
this.phase = ffrac(time * frequency + phaseoffset);
|
this.phase = mod(time * frequency + phaseoffset, 1);
|
||||||
}
|
}
|
||||||
const dt = frequency * INVSR;
|
const dt = frequency / sampleRate;
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||||
@@ -293,8 +293,8 @@ class TwoPoleFilter {
|
|||||||
// Out of bound values can produce NaNs
|
// Out of bound values can produce NaNs
|
||||||
resonance = clamp(resonance, 0, 1);
|
resonance = clamp(resonance, 0, 1);
|
||||||
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
|
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
|
||||||
const c = clamp(2 * Math.sin(cutoff * PI * INVSR), 0, 1.14);
|
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
|
||||||
const r = Math.pow(0.5, 8 * resonance + 1);
|
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||||
const mrc = 1 - r * c;
|
const mrc = 1 - r * c;
|
||||||
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||||
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||||
@@ -353,6 +353,11 @@ class DJFProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('djf-processor', DJFProcessor);
|
registerProcessor('djf-processor', DJFProcessor);
|
||||||
|
|
||||||
|
function fast_tanh(x) {
|
||||||
|
const x2 = x * x;
|
||||||
|
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||||
|
}
|
||||||
|
const _PI = 3.14159265359;
|
||||||
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
|
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
|
||||||
class LadderProcessor extends AudioWorkletProcessor {
|
class LadderProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
@@ -390,7 +395,7 @@ class LadderProcessor extends AudioWorkletProcessor {
|
|||||||
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||||
|
|
||||||
let cutoff = parameters.frequency[0];
|
let cutoff = parameters.frequency[0];
|
||||||
cutoff = cutoff * TWO_PI * INVSR;
|
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||||
|
|
||||||
const k = Math.min(8, resonance * 0.13);
|
const k = Math.min(8, resonance * 0.13);
|
||||||
@@ -503,7 +508,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
name: 'voices',
|
name: 'voices',
|
||||||
defaultValue: 5,
|
defaultValue: 5,
|
||||||
min: 1,
|
min: 1,
|
||||||
automationRate: 'k-rate',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -515,37 +519,40 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
// this.port.postMessage({ type: 'onended' });
|
// this.port.postMessage({ type: 'onended' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const detune = pv(params.detune, i);
|
const detune = pv(params.detune, i);
|
||||||
|
const voices = pv(params.voices, i);
|
||||||
const freqspread = pv(params.freqspread, i);
|
const freqspread = pv(params.freqspread, i);
|
||||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||||
let gainL = Math.sqrt(1 - panspread);
|
const gain1 = Math.sqrt(1 - panspread);
|
||||||
let gainR = Math.sqrt(panspread);
|
const gain2 = Math.sqrt(panspread);
|
||||||
let freq = pv(params.frequency, i);
|
let freq = pv(params.frequency, i);
|
||||||
// Main detuning
|
// Main detuning
|
||||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
const detuner = getDetuner(voices, freqspread);
|
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let n = 0; n < voices; n++) {
|
||||||
|
const isOdd = (n & 1) == 1;
|
||||||
|
let gainL = gain1;
|
||||||
|
let gainR = gain2;
|
||||||
|
// invert right and left gain
|
||||||
|
if (isOdd) {
|
||||||
|
gainL = gain2;
|
||||||
|
gainR = gain1;
|
||||||
|
}
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
// has domain [0, 1]
|
// has domain [0, 1]
|
||||||
const dt = frac(freqVoice * INVSR);
|
const dt = mod(freqVoice / sampleRate, 1);
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
this.phase[n] = this.phase[n] ?? Math.random();
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||||
|
|
||||||
output[0][i] += v * gainL;
|
output[0][i] = output[0][i] + v * gainL;
|
||||||
output[1][i] += v * gainR;
|
output[1][i] = output[1][i] + v * gainR;
|
||||||
|
|
||||||
let pn = this.phase[n] + dt;
|
this.phase[n] = wrapPhase(this.phase[n] + dt);
|
||||||
if (pn >= 1.0) pn -= 1.0;
|
|
||||||
this.phase[n] = pn;
|
|
||||||
// invert right and left gain
|
|
||||||
const tmp = gainL;
|
|
||||||
gainL = gainR;
|
|
||||||
gainR = tmp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -557,16 +564,12 @@ registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
|||||||
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||||
const BUFFERED_BLOCK_SIZE = 2048;
|
const BUFFERED_BLOCK_SIZE = 2048;
|
||||||
|
|
||||||
const hannCache = new Map();
|
|
||||||
function genHannWindow(length) {
|
function genHannWindow(length) {
|
||||||
if (!hannCache.has(length)) {
|
let win = new Float32Array(length);
|
||||||
const win = new Float32Array(length);
|
for (var i = 0; i < length; i++) {
|
||||||
for (let i = 0; i < length; i++) {
|
win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length));
|
||||||
win[i] = 0.5 * (1 - Math.cos((TWO_PI * i) / length));
|
|
||||||
}
|
}
|
||||||
hannCache.set(length, win);
|
return win;
|
||||||
}
|
|
||||||
return hannCache.get(length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PhaseVocoderProcessor extends OLAProcessor {
|
class PhaseVocoderProcessor extends OLAProcessor {
|
||||||
@@ -584,10 +587,11 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
blockSize: BUFFERED_BLOCK_SIZE,
|
blockSize: BUFFERED_BLOCK_SIZE,
|
||||||
};
|
};
|
||||||
super(options);
|
super(options);
|
||||||
this.timeCursor = 0;
|
|
||||||
this.fftSize = this.blockSize;
|
this.fftSize = this.blockSize;
|
||||||
this.invfftSize = 1 / this.fftSize;
|
this.timeCursor = 0;
|
||||||
this.hannWindow = genHannWindow(this.fftSize);
|
|
||||||
|
this.hannWindow = genHannWindow(this.blockSize);
|
||||||
// prepare FFT and pre-allocate buffers
|
// prepare FFT and pre-allocate buffers
|
||||||
this.fft = new FFT(this.fftSize);
|
this.fft = new FFT(this.fftSize);
|
||||||
this.freqComplexBuffer = this.fft.createComplexArray();
|
this.freqComplexBuffer = this.fft.createComplexArray();
|
||||||
@@ -600,43 +604,52 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
|
|
||||||
processOLA(inputs, outputs, parameters) {
|
processOLA(inputs, outputs, parameters) {
|
||||||
// no automation, take last value
|
// no automation, take last value
|
||||||
|
|
||||||
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
|
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
|
||||||
|
|
||||||
if (pitchFactor < 0) {
|
if (pitchFactor < 0) {
|
||||||
pitchFactor = pitchFactor * 0.25;
|
pitchFactor = pitchFactor * 0.25;
|
||||||
}
|
}
|
||||||
pitchFactor = Math.max(0, pitchFactor + 1);
|
pitchFactor = Math.max(0, pitchFactor + 1);
|
||||||
for (let i = 0; i < this.nbInputs; i++) {
|
|
||||||
for (let j = 0; j < inputs[i].length; j++) {
|
for (var i = 0; i < this.nbInputs; i++) {
|
||||||
const input = inputs[i][j];
|
for (var j = 0; j < inputs[i].length; j++) {
|
||||||
const output = outputs[i][j];
|
// big assumption here: output is symetric to input
|
||||||
|
var input = inputs[i][j];
|
||||||
|
var output = outputs[i][j];
|
||||||
|
|
||||||
this.applyHannWindow(input);
|
this.applyHannWindow(input);
|
||||||
|
|
||||||
this.fft.realTransform(this.freqComplexBuffer, input);
|
this.fft.realTransform(this.freqComplexBuffer, input);
|
||||||
|
|
||||||
this.computeMagnitudes();
|
this.computeMagnitudes();
|
||||||
this.findPeaks();
|
this.findPeaks();
|
||||||
this.shiftPeaks(pitchFactor);
|
this.shiftPeaks(pitchFactor);
|
||||||
|
|
||||||
this.fft.completeSpectrum(this.freqComplexBufferShifted);
|
this.fft.completeSpectrum(this.freqComplexBufferShifted);
|
||||||
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
|
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
|
||||||
this.fft.fromComplexArray(this.timeComplexBuffer, output);
|
this.fft.fromComplexArray(this.timeComplexBuffer, output);
|
||||||
this.applyHannWindow(output);
|
this.applyHannWindow(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeCursor += this.hopSize;
|
this.timeCursor += this.hopSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply Hann window in-place */
|
/** Apply Hann window in-place */
|
||||||
applyHannWindow(input) {
|
applyHannWindow(input) {
|
||||||
for (let i = 0; i < this.blockSize; i++) {
|
for (var i = 0; i < this.blockSize; i++) {
|
||||||
input[i] *= this.hannWindow[i] * 1.62;
|
input[i] = input[i] * this.hannWindow[i] * 1.62;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compute squared magnitudes for peak finding **/
|
/** Compute squared magnitudes for peak finding **/
|
||||||
computeMagnitudes() {
|
computeMagnitudes() {
|
||||||
let i = 0,
|
var i = 0,
|
||||||
j = 0;
|
j = 0;
|
||||||
while (i < this.magnitudes.length) {
|
while (i < this.magnitudes.length) {
|
||||||
const real = this.freqComplexBuffer[j];
|
let real = this.freqComplexBuffer[j];
|
||||||
const imag = this.freqComplexBuffer[j + 1];
|
let imag = this.freqComplexBuffer[j + 1];
|
||||||
// no need to sqrt for peak finding
|
// no need to sqrt for peak finding
|
||||||
this.magnitudes[i] = real ** 2 + imag ** 2;
|
this.magnitudes[i] = real ** 2 + imag ** 2;
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -647,10 +660,12 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
/** Find peaks in spectrum magnitudes **/
|
/** Find peaks in spectrum magnitudes **/
|
||||||
findPeaks() {
|
findPeaks() {
|
||||||
this.nbPeaks = 0;
|
this.nbPeaks = 0;
|
||||||
let i = 2;
|
var i = 2;
|
||||||
const end = this.magnitudes.length - 2;
|
let end = this.magnitudes.length - 2;
|
||||||
|
|
||||||
while (i < end) {
|
while (i < end) {
|
||||||
const mag = this.magnitudes[i];
|
let mag = this.magnitudes[i];
|
||||||
|
|
||||||
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
@@ -659,6 +674,7 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.peakIndexes[this.nbPeaks] = i;
|
this.peakIndexes[this.nbPeaks] = i;
|
||||||
this.nbPeaks++;
|
this.nbPeaks++;
|
||||||
i += 2;
|
i += 2;
|
||||||
@@ -669,44 +685,53 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
shiftPeaks(pitchFactor) {
|
shiftPeaks(pitchFactor) {
|
||||||
// zero-fill new spectrum
|
// zero-fill new spectrum
|
||||||
this.freqComplexBufferShifted.fill(0);
|
this.freqComplexBufferShifted.fill(0);
|
||||||
for (let i = 0; i < this.nbPeaks; i++) {
|
|
||||||
const peakIndex = this.peakIndexes[i];
|
for (var i = 0; i < this.nbPeaks; i++) {
|
||||||
const peakIndexShifted = fround(peakIndex * pitchFactor);
|
let peakIndex = this.peakIndexes[i];
|
||||||
|
let peakIndexShifted = Math.round(peakIndex * pitchFactor);
|
||||||
|
|
||||||
if (peakIndexShifted > this.magnitudes.length) {
|
if (peakIndexShifted > this.magnitudes.length) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find region of influence
|
// find region of influence
|
||||||
let startIndex = 0;
|
var startIndex = 0;
|
||||||
let endIndex = this.fftSize;
|
var endIndex = this.fftSize;
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
startIndex = peakIndex - fround((peakIndex - this.peakIndexes[i - 1]) / 2);
|
let peakIndexBefore = this.peakIndexes[i - 1];
|
||||||
|
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2);
|
||||||
}
|
}
|
||||||
if (i < this.nbPeaks - 1) {
|
if (i < this.nbPeaks - 1) {
|
||||||
endIndex = peakIndex + fceil((this.peakIndexes[i + 1] - peakIndex) / 2);
|
let peakIndexAfter = this.peakIndexes[i + 1];
|
||||||
|
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// shift whole region of influence around peak to shifted peak
|
// shift whole region of influence around peak to shifted peak
|
||||||
const startOffset = startIndex - peakIndex;
|
let startOffset = startIndex - peakIndex;
|
||||||
const endOffset = endIndex - peakIndex;
|
let endOffset = endIndex - peakIndex;
|
||||||
const omegaDelta = TWO_PI * this.invfftSize * (peakIndexShifted - peakIndex);
|
for (var j = startOffset; j < endOffset; j++) {
|
||||||
const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
|
let binIndex = peakIndex + j;
|
||||||
const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
|
let binIndexShifted = peakIndexShifted + j;
|
||||||
for (let j = startOffset; j < endOffset; j++) {
|
|
||||||
const binIndex = peakIndex + j;
|
|
||||||
const binIndexShifted = peakIndexShifted + j;
|
|
||||||
if (binIndexShifted >= this.magnitudes.length) {
|
if (binIndexShifted >= this.magnitudes.length) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply phase correction
|
// apply phase correction
|
||||||
const indexReal = 2 * binIndex;
|
let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize;
|
||||||
const indexImag = indexReal + 1;
|
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
|
||||||
const valueReal = this.freqComplexBuffer[indexReal];
|
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
|
||||||
const valueImag = this.freqComplexBuffer[indexImag];
|
|
||||||
|
|
||||||
const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
|
let indexReal = binIndex * 2;
|
||||||
const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
|
let indexImag = indexReal + 1;
|
||||||
|
let valueReal = this.freqComplexBuffer[indexReal];
|
||||||
|
let valueImag = this.freqComplexBuffer[indexImag];
|
||||||
|
|
||||||
const indexShiftedReal = 2 * binIndexShifted;
|
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
|
||||||
const indexShiftedImag = indexShiftedReal + 1;
|
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
|
||||||
|
|
||||||
|
let indexShiftedReal = binIndexShifted * 2;
|
||||||
|
let indexShiftedImag = indexShiftedReal + 1;
|
||||||
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
|
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
|
||||||
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
|
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
|
||||||
}
|
}
|
||||||
@@ -720,10 +745,11 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
|||||||
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.phi = -PI; // phase
|
this.pi = _PI;
|
||||||
|
this.phi = -this.pi; // phase
|
||||||
this.Y0 = 0; // feedback memories
|
this.Y0 = 0; // feedback memories
|
||||||
this.Y1 = 0;
|
this.Y1 = 0;
|
||||||
this.PW = PI; // pulse width
|
this.PW = this.pi; // pulse width
|
||||||
this.B = 2.3; // feedback coefficient
|
this.B = 2.3; // feedback coefficient
|
||||||
this.dphif = 0; // filtered phase increment
|
this.dphif = 0; // filtered phase increment
|
||||||
this.envf = 0; // filtered envelope
|
this.envf = 0; // filtered envelope
|
||||||
@@ -780,11 +806,11 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
dphi;
|
dphi;
|
||||||
|
|
||||||
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
||||||
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
|
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi;
|
||||||
const detune = pv(params.detune, i);
|
const detune = getParamValue(i, params.detune);
|
||||||
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
|
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||||
|
|
||||||
dphi = freq * TWO_PI * INVSR; // phase increment
|
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
|
||||||
this.dphif += 0.1 * (dphi - this.dphif);
|
this.dphif += 0.1 * (dphi - this.dphif);
|
||||||
|
|
||||||
env *= 0.9998; // exponential decay envelope
|
env *= 0.9998; // exponential decay envelope
|
||||||
@@ -796,7 +822,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
// Waveform generation (half-Tomisawa oscillators)
|
// Waveform generation (half-Tomisawa oscillators)
|
||||||
this.phi += this.dphif; // phase increment
|
this.phi += this.dphif; // phase increment
|
||||||
if (this.phi >= PI) this.phi -= TWO_PI; // phase wrapping
|
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping
|
||||||
|
|
||||||
// First half-Tomisawa generator
|
// First half-Tomisawa generator
|
||||||
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
|
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
|
||||||
@@ -826,23 +852,24 @@ const chyx = {
|
|||||||
/*bit reverse*/ br: function (x, size = 8) {
|
/*bit reverse*/ br: function (x, size = 8) {
|
||||||
if (size > 32) {
|
if (size > 32) {
|
||||||
throw new Error('br() Size cannot be greater than 32');
|
throw new Error('br() Size cannot be greater than 32');
|
||||||
}
|
} else {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
for (let idx = 0; idx < size; idx++) {
|
for (let idx = 0; idx < size - 0; idx++) {
|
||||||
result |= chyx.bitC(x, 1 << idx, 1 << (size - (idx + 1)));
|
result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
|
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
|
||||||
return Math.sin((x * PI) / 128);
|
return Math.sin(x / (128 / Math.PI));
|
||||||
},
|
},
|
||||||
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
|
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
|
||||||
return Math.cos((x * PI) / 128);
|
return Math.cos(x / (128 / Math.PI));
|
||||||
},
|
},
|
||||||
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
|
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
|
||||||
return Math.tan((x * PI) / 128);
|
return Math.tan(x / (128 / Math.PI));
|
||||||
},
|
},
|
||||||
/*converts t into a string composed of its bits; regexes that*/ regG: function (t, X) {
|
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
|
||||||
return X.test(t.toString(2));
|
return X.test(t.toString(2));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -850,7 +877,7 @@ const chyx = {
|
|||||||
// Create shortened Math functions
|
// Create shortened Math functions
|
||||||
let mathParams, byteBeatHelperFuncs;
|
let mathParams, byteBeatHelperFuncs;
|
||||||
function getByteBeatFunc(codetext) {
|
function getByteBeatFunc(codetext) {
|
||||||
if (mathParams == null) {
|
if ((mathParams || byteBeatHelperFuncs) == null) {
|
||||||
mathParams = Object.getOwnPropertyNames(Math);
|
mathParams = Object.getOwnPropertyNames(Math);
|
||||||
byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
|
byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
|
||||||
const chyxNames = Object.getOwnPropertyNames(chyx);
|
const chyxNames = Object.getOwnPropertyNames(chyx);
|
||||||
@@ -883,7 +910,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
this.func = getByteBeatFunc(codeText);
|
this.func = getByteBeatFunc(codeText);
|
||||||
};
|
};
|
||||||
this.initialOffset = 0;
|
this.initialOffset = null;
|
||||||
this.t = null;
|
this.t = null;
|
||||||
this.func = null;
|
this.func = null;
|
||||||
}
|
}
|
||||||
@@ -930,19 +957,18 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
this.t = params.begin[0] * sampleRate;
|
this.t = params.begin[0] * sampleRate;
|
||||||
}
|
}
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const scale = 256 * INVSR;
|
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const detune = pv(params.detune, i);
|
const detune = getParamValue(i, params.detune);
|
||||||
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
|
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||||
const local_t = scale * freq * this.t + this.initialOffset;
|
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset;
|
||||||
const funcValue = this.func(local_t);
|
const funcValue = this.func(local_t);
|
||||||
const signal = (funcValue & 255) / 127.5 - 1;
|
let signal = (funcValue & 255) / 127.5 - 1;
|
||||||
//prevent speaker blowout via clipping if threshold exceeds
|
const out = signal * 0.2;
|
||||||
const out = clamp(signal * 0.2, -0.4, 0.4);
|
|
||||||
for (let c = 0; c < output.length; c++) {
|
for (let c = 0; c < output.length; c++) {
|
||||||
output[c][i] = out;
|
//prevent speaker blowout via clipping if threshold exceeds
|
||||||
|
output[c][i] = clamp(out, -0.4, 0.4);
|
||||||
}
|
}
|
||||||
this.t++;
|
this.t = this.t + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // keep the audio processing going
|
return true; // keep the audio processing going
|
||||||
@@ -951,102 +977,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
||||||
class EnvelopeProcessor extends AudioWorkletProcessor {
|
|
||||||
static get parameterDescriptors() {
|
|
||||||
return [
|
|
||||||
{ name: 'begin', defaultValue: 0 },
|
|
||||||
{ name: 'end', defaultValue: 0 },
|
|
||||||
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
|
|
||||||
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
|
|
||||||
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
|
|
||||||
{ name: 'release', defaultValue: 0.1, minValue: 0 },
|
|
||||||
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
|
||||||
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
|
||||||
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
|
||||||
{ name: 'peak', defaultValue: 1 },
|
|
||||||
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.val = 0;
|
|
||||||
this.segIdx = 0;
|
|
||||||
this.state = 0;
|
|
||||||
this.beginTime = 0;
|
|
||||||
this.endTime = 0;
|
|
||||||
this.attackStart = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_warp(phase, curvature, strength = 8) {
|
|
||||||
if (phase === 0 || phase === 1) return phase; // fast exit
|
|
||||||
if (curvature > 0) {
|
|
||||||
// snappier
|
|
||||||
const exp = 1 + strength * curvature;
|
|
||||||
return 1 - Math.pow(1 - phase, exp);
|
|
||||||
} else {
|
|
||||||
// more calm
|
|
||||||
const exp = 1 - strength * curvature;
|
|
||||||
return Math.pow(phase, exp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_advance(start, target, time, curvature) {
|
|
||||||
if (time === 0 || start === target) {
|
|
||||||
this.val = target;
|
|
||||||
} else {
|
|
||||||
// We compute our progress through this section of the envelope in time
|
|
||||||
// as a `phase` value, which is warped by the curvature, and then used
|
|
||||||
// to compute the value of the envelope at that time
|
|
||||||
const phase = Math.min(1, (currentTime - this.beginTime) / time);
|
|
||||||
const phaseWarped = this._warp(phase, curvature);
|
|
||||||
this.val = start + (target - start) * phaseWarped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process(_inputs, outputs, params) {
|
|
||||||
const out = outputs[0][0];
|
|
||||||
if (!out) return true;
|
|
||||||
const begin = pv(params.begin, 0);
|
|
||||||
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
|
||||||
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
|
||||||
// triggered
|
|
||||||
this.beginTime = begin;
|
|
||||||
this.state = 1;
|
|
||||||
this.endTime = pv(params.end, 0);
|
|
||||||
this.attackStart = this.val;
|
|
||||||
}
|
|
||||||
const susTime = this.endTime - this.beginTime;
|
|
||||||
for (let i = 0; i < out.length; i++) {
|
|
||||||
const attack = pv(params.attack, i);
|
|
||||||
const decay = pv(params.decay, i);
|
|
||||||
const sustain = pv(params.sustain, i);
|
|
||||||
const release = pv(params.release, i);
|
|
||||||
const aCurve = pv(params.attackCurve, i);
|
|
||||||
const dCurve = pv(params.decayCurve, i);
|
|
||||||
const rCurve = pv(params.releaseCurve, i);
|
|
||||||
const peak = pv(params.peak, i);
|
|
||||||
const states = [
|
|
||||||
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
|
||||||
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
|
||||||
{ time: attack + decay, start: 1, target: sustain, curve: dCurve },
|
|
||||||
{ time: susTime, start: sustain, target: sustain },
|
|
||||||
{ time: susTime + release, start: sustain, target: 0, curve: rCurve },
|
|
||||||
];
|
|
||||||
let { time, start, target, curve } = states[this.state];
|
|
||||||
this._advance(start, target, time, curve);
|
|
||||||
while (currentTime - this.beginTime >= time) {
|
|
||||||
this.state = (this.state + 1) % states.length;
|
|
||||||
time = states[this.state].time;
|
|
||||||
}
|
|
||||||
out[i] = this.val * peak;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerProcessor('envelope-processor', EnvelopeProcessor);
|
|
||||||
|
|
||||||
export const WarpMode = Object.freeze({
|
export const WarpMode = Object.freeze({
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
ASYM: 1,
|
ASYM: 1,
|
||||||
@@ -1126,7 +1056,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
|
{ name: 'voices', defaultValue: 1, min: 1 },
|
||||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||||
];
|
];
|
||||||
@@ -1137,6 +1067,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
this.frameLen = 0;
|
this.frameLen = 0;
|
||||||
this.numFrames = 0;
|
this.numFrames = 0;
|
||||||
this.phase = [];
|
this.phase = [];
|
||||||
|
this.invSR = 1 / sampleRate;
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
@@ -1173,7 +1104,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
_toBits(amt, min = 2, max = 12) {
|
_toBits(amt, min = 2, max = 12) {
|
||||||
const b = max + (min - max) * amt;
|
const b = max + (min - max) * amt;
|
||||||
return { b, n: fround(Math.pow(2, b)) };
|
return { b, n: Math.round(Math.pow(2, b)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
_warpPhase(phase, amt, mode) {
|
_warpPhase(phase, amt, mode) {
|
||||||
@@ -1199,7 +1130,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
|
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
|
||||||
}
|
}
|
||||||
case WarpMode.SYNC: {
|
case WarpMode.SYNC: {
|
||||||
const syncRatio = Math.pow(16, amt ** 2);
|
const syncRatio = Math.pow(16, amt * amt);
|
||||||
return (phase * syncRatio) % 1;
|
return (phase * syncRatio) % 1;
|
||||||
}
|
}
|
||||||
case WarpMode.QUANT: {
|
case WarpMode.QUANT: {
|
||||||
@@ -1208,8 +1139,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
case WarpMode.FOLD: {
|
case WarpMode.FOLD: {
|
||||||
const K = 7;
|
const K = 7;
|
||||||
const k = 1 + Math.max(1, fround(K * amt));
|
const k = 1 + Math.max(1, Math.round(K * amt));
|
||||||
return Math.abs(ffrac(k * phase) - 0.5) * 2;
|
return Math.abs(frac(k * phase) - 0.5) * 2;
|
||||||
}
|
}
|
||||||
case WarpMode.PWM: {
|
case WarpMode.PWM: {
|
||||||
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
|
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
|
||||||
@@ -1219,12 +1150,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
case WarpMode.ORBIT: {
|
case WarpMode.ORBIT: {
|
||||||
const depth = 0.5 * amt;
|
const depth = 0.5 * amt;
|
||||||
const n = 3;
|
const n = 3;
|
||||||
return frac(phase + depth * Math.sin(TWO_PI * n * phase));
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
}
|
}
|
||||||
case WarpMode.SPIN: {
|
case WarpMode.SPIN: {
|
||||||
const depth = 0.5 * amt;
|
const depth = 0.5 * amt;
|
||||||
const { n } = this._toBits(amt, 1, 6);
|
const { n } = this._toBits(amt, 1, 6);
|
||||||
return frac(phase + depth * Math.sin(TWO_PI * n * phase));
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
}
|
}
|
||||||
case WarpMode.CHAOS: {
|
case WarpMode.CHAOS: {
|
||||||
const r = 3.7 + 0.3 * amt;
|
const r = 3.7 + 0.3 * amt;
|
||||||
@@ -1235,7 +1166,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const isPrime = (n) => {
|
const isPrime = (n) => {
|
||||||
if (n < 2) return false;
|
if (n < 2) return false;
|
||||||
if (n % 2 === 0) return n === 2;
|
if (n % 2 === 0) return n === 2;
|
||||||
for (let d = 3; d ** 2 <= n; d += 2) if (n % d === 0) return false;
|
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
let { n } = this._toBits(amt, 3);
|
let { n } = this._toBits(amt, 3);
|
||||||
@@ -1244,12 +1175,18 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
case WarpMode.BINARY: {
|
case WarpMode.BINARY: {
|
||||||
let { b } = this._toBits(amt, 3);
|
let { b } = this._toBits(amt, 3);
|
||||||
b = fround(b);
|
b = Math.round(b);
|
||||||
const n = 1 << b;
|
const n = 1 << b;
|
||||||
const idx = ffloor(phase * n);
|
const idx = ffloor(phase * n);
|
||||||
const ridx = bitReverse(idx, b);
|
const ridx = bitReverse(idx, b);
|
||||||
return ridx / n;
|
return ridx / n;
|
||||||
}
|
}
|
||||||
|
case WarpMode.MODULAR: {
|
||||||
|
const { n } = this._toBits(amt);
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const jump = frac(phase * n) / n;
|
||||||
|
return frac(phase + depth * jump);
|
||||||
|
}
|
||||||
case WarpMode.BROWNIAN: {
|
case WarpMode.BROWNIAN: {
|
||||||
const disp = 0.25 * amt * brownian(64 * phase, 4);
|
const disp = 0.25 * amt * brownian(64 * phase, 4);
|
||||||
return frac(phase + disp);
|
return frac(phase + disp);
|
||||||
@@ -1272,7 +1209,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
case WarpMode.LOGISTIC: {
|
case WarpMode.LOGISTIC: {
|
||||||
let x = phase;
|
let x = phase;
|
||||||
const r = 3.6 + 0.4 * amt;
|
const r = 3.6 + 0.4 * amt;
|
||||||
const iters = 1 + fround(2 * amt);
|
const iters = 1 + Math.round(2 * amt);
|
||||||
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
|
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
|
||||||
return clamp(x, 0, 1);
|
return clamp(x, 0, 1);
|
||||||
}
|
}
|
||||||
@@ -1285,7 +1222,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return (y - y0) / (y1 - y0);
|
return (y - y0) / (y1 - y0);
|
||||||
}
|
}
|
||||||
case WarpMode.FRACTAL: {
|
case WarpMode.FRACTAL: {
|
||||||
const d = 0.5 * Math.sin(TWO_PI * phase) * amt;
|
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
|
||||||
return frac(phase + d);
|
return frac(phase + d);
|
||||||
}
|
}
|
||||||
case WarpMode.FLIP: {
|
case WarpMode.FLIP: {
|
||||||
@@ -1332,16 +1269,16 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
if (outR !== outL) outR.set(outL);
|
if (outR !== outL) outR.set(outL);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const voices = parameters.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const detune = pv(parameters.detune, i);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
const freqspread = pv(parameters.freqspread, i);
|
||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
const interpT = idx - fIdx;
|
const frac = idx - fIdx;
|
||||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
|
const voices = pv(parameters.voices, i);
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
@@ -1349,7 +1286,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
let f = pv(parameters.frequency, i);
|
let f = pv(parameters.frequency, i);
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
const normalizer = 1 / Math.sqrt(voices);
|
||||||
const detuner = getDetuner(voices, freqspread);
|
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let n = 0; n < voices; n++) {
|
||||||
const isOdd = (n & 1) == 1;
|
const isOdd = (n & 1) == 1;
|
||||||
let gainL = gain1;
|
let gainL = gain1;
|
||||||
@@ -1359,8 +1295,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
gainL = gain2;
|
gainL = gain2;
|
||||||
gainR = gain1;
|
gainR = gain1;
|
||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
||||||
const dPhase = fVoice * INVSR;
|
const dPhase = fVoice * this.invSR;
|
||||||
const level = this._chooseMip(dPhase);
|
const level = this._chooseMip(dPhase);
|
||||||
const table = this.tables[level];
|
const table = this.tables[level];
|
||||||
|
|
||||||
@@ -1369,13 +1305,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
let s = lerp(s0, s1, interpT);
|
let s = s0 + (s1 - s0) * frac;
|
||||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
s = -s;
|
s = -s;
|
||||||
}
|
}
|
||||||
outL[i] += s * gainL * normalizer;
|
outL[i] += s * gainL * normalizer;
|
||||||
outR[i] += s * gainR * normalizer;
|
outR[i] += s * gainR * normalizer;
|
||||||
this.phase[n] = frac(this.phase[n] + dPhase);
|
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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
+8
-246
@@ -41,7 +41,7 @@ importers:
|
|||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 3.0.4
|
specifier: 3.0.4
|
||||||
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
|
version: 3.0.4(vitest@3.0.4)
|
||||||
'@vitest/ui':
|
'@vitest/ui':
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(vitest@3.0.4)
|
version: 3.0.4(vitest@3.0.4)
|
||||||
@@ -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==}
|
||||||
|
|
||||||
@@ -3748,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'}
|
||||||
@@ -5366,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==}
|
||||||
|
|
||||||
@@ -5752,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==}
|
||||||
|
|
||||||
@@ -5813,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}
|
||||||
@@ -5854,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
|
||||||
@@ -6016,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==}
|
||||||
@@ -6778,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==}
|
||||||
|
|
||||||
@@ -6869,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'}
|
||||||
@@ -7747,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'}
|
||||||
@@ -7872,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==}
|
||||||
|
|
||||||
@@ -9880,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
|
||||||
@@ -10554,7 +10364,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
|
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
@@ -11462,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
|
||||||
@@ -13336,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: {}
|
||||||
@@ -14015,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:
|
||||||
@@ -14067,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: {}
|
||||||
@@ -14096,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:
|
||||||
@@ -14350,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:
|
||||||
@@ -15255,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:
|
||||||
@@ -15417,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:
|
||||||
@@ -16328,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: {}
|
||||||
@@ -16529,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: {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+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")`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -48,94 +48,28 @@ You can also use the `crackle` type to play some subtle noise crackles. You can
|
|||||||
|
|
||||||
### Additive Synthesis
|
### Additive Synthesis
|
||||||
|
|
||||||
Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality.
|
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
|
||||||
|
|
||||||
For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions.
|
|
||||||
|
|
||||||
#### Partials
|
|
||||||
|
|
||||||
`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness:
|
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||||
.sound("sawtooth")
|
.sound("sawtooth")
|
||||||
.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"])
|
.n("<32 16 8 4>")
|
||||||
._scope()`}
|
._scope()`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source:
|
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
|
||||||
|
You can also set `n` directly in mini notation with `sound`:
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||||
.sound("user")
|
.sound("sawtooth:<32 16 8 4>")
|
||||||
.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3])
|
|
||||||
._scope()`}
|
._scope()`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
We may algorithmically construct lists of magnitudes with Javascript code like:
|
Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
|
||||||
|
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`const numHarmonics = 22;
|
|
||||||
note("c2 <eb2 <g2 g1>>".fast(2))
|
|
||||||
.sound("saw")
|
|
||||||
.partials(new Array(numHarmonics).fill(1))
|
|
||||||
._scope()`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
which acts as a spectral filter. Or:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
|
|
||||||
.sound("user")
|
|
||||||
.partials(new Array(50).fill(0)
|
|
||||||
.map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1))
|
|
||||||
)
|
|
||||||
._scope()`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
which recovers a familiar waveform.
|
|
||||||
|
|
||||||
`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
|
|
||||||
.sound("user")
|
|
||||||
.partials(randL(10))
|
|
||||||
._scope()`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
and with lists _of_ patterns:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`note("c2 <eb2 <g2 g1>>".fast(4))
|
|
||||||
.sound("user")
|
|
||||||
.partials([1, 0, "0 1", "0 1 0.3", rand])
|
|
||||||
._scope()`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0.
|
|
||||||
|
|
||||||
#### Phases
|
|
||||||
|
|
||||||
Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built.
|
|
||||||
|
|
||||||
These phases too can be declared in Strudel and can give your sounds interesting depth.
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
|
||||||
.penv(48).panchor(0).pdec(0.05)
|
|
||||||
.delay(0.25).room(0.25)
|
|
||||||
.compressor(-20).vib(0.3)
|
|
||||||
.partials(randL(200))
|
|
||||||
.phases(randL(200))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Vibrato
|
## Vibrato
|
||||||
|
|
||||||
@@ -223,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.
|
|
||||||
@@ -8,34 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialActionButton(props) {
|
|
||||||
const { className, ...buttonProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionButton
|
|
||||||
{...buttonProps}
|
|
||||||
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionInput({ label, className, ...props }) {
|
|
||||||
return (
|
|
||||||
<label className={cx('inline-flex items-center cursor-pointer', className)}>
|
|
||||||
<input {...props} className="sr-only peer" />
|
|
||||||
|
|
||||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpecialActionInput({ className, ...props }) {
|
|
||||||
return (
|
|
||||||
<ActionInput
|
|
||||||
{...props}
|
|
||||||
className={className}
|
|
||||||
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { errorLogger } from '@strudel/core';
|
|
||||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
|
||||||
import { SpecialActionInput } from '../button/action-button';
|
|
||||||
|
|
||||||
async function importScript(script) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsText(script);
|
|
||||||
|
|
||||||
reader.onload = () => {
|
|
||||||
const text = reader.result;
|
|
||||||
storePrebakeScript(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.onerror = () => {
|
|
||||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export function ImportPrebakeScriptButton() {
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpecialActionInput
|
|
||||||
type="file"
|
|
||||||
label="import prebake script"
|
|
||||||
accept=".strudel"
|
|
||||||
onChange={(e) => importScript(e.target.files[0])}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
|||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||||
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
|
|
||||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
|
||||||
|
|
||||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||||
return (
|
return (
|
||||||
@@ -114,8 +112,6 @@ export function SettingsTab({ started }) {
|
|||||||
multiChannelOrbits,
|
multiChannelOrbits,
|
||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
|
||||||
includePrebakeScriptInShare,
|
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
@@ -207,15 +203,6 @@ export function SettingsTab({ started }) {
|
|||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<FormItem label="Prebake">
|
|
||||||
<ImportPrebakeScriptButton />
|
|
||||||
<Checkbox
|
|
||||||
label="Include prebake script in share"
|
|
||||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
|
||||||
value={includePrebakeScriptInShare}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label="Keybindings">
|
<FormItem label="Keybindings">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={keybindings}
|
value={keybindings}
|
||||||
@@ -317,15 +304,11 @@ 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">
|
||||||
<SpecialActionButton
|
<button
|
||||||
|
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
@@ -336,7 +319,7 @@ export function SettingsTab({ started }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
restore default settings
|
restore default settings
|
||||||
</SpecialActionButton>
|
</button>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 onended = () => trigRef.current?.node?.disconnect();
|
|
||||||
try {
|
|
||||||
// Pre-load the sample by calling onTrigger with a future time
|
|
||||||
// This triggers the loading but schedules playback for later
|
|
||||||
const time = ctx.currentTime + 0.05;
|
const time = ctx.currentTime + 0.05;
|
||||||
const ref = await onTrigger(time, params, onended);
|
const onended = () => trigRef.current?.node?.disconnect();
|
||||||
trigRef.current = ref;
|
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||||
if (ref?.node) {
|
trigRef.current.then((ref) => {
|
||||||
connectToDestination(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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st
|
|||||||
import { registerSamplesFromDB } from './idbutils.mjs';
|
import { registerSamplesFromDB } from './idbutils.mjs';
|
||||||
import './piano.mjs';
|
import './piano.mjs';
|
||||||
import './files.mjs';
|
import './files.mjs';
|
||||||
import { settingsMap } from '@src/settings.mjs';
|
|
||||||
import { evaluate } from '@strudel/transpiler';
|
|
||||||
|
|
||||||
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
|
||||||
@@ -22,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'],
|
||||||
@@ -148,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');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
webaudioOutput,
|
webaudioOutput,
|
||||||
@@ -63,10 +63,11 @@ async function getModule(name) {
|
|||||||
const initialCode = `// LOADING`;
|
const initialCode = `// LOADING`;
|
||||||
|
|
||||||
export function useReplContext() {
|
export function useReplContext() {
|
||||||
const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
|
const { isSyncEnabled, audioEngineTarget } = useSettings();
|
||||||
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
||||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||||
|
|
||||||
const init = useCallback(() => {
|
const init = useCallback(() => {
|
||||||
const drawTime = [-2, 2];
|
const drawTime = [-2, 2];
|
||||||
const drawContext = getDrawContext();
|
const drawContext = getDrawContext();
|
||||||
@@ -83,12 +84,7 @@ export function useReplContext() {
|
|||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
prebake: async () =>
|
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||||
Promise.all([modulesLoading, presets]).then(() => {
|
|
||||||
if (prebakeScript?.length) {
|
|
||||||
return evaluate(prebakeScript ?? '');
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
setReplState({ ...state });
|
setReplState({ ...state });
|
||||||
},
|
},
|
||||||
@@ -218,13 +214,7 @@ export function useReplContext() {
|
|||||||
editorRef.current.repl.evaluate(code);
|
editorRef.current.repl.evaluate(code);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => shareCode(replState.code);
|
||||||
let code = replState.code;
|
|
||||||
if (includePrebakeScriptInShare) {
|
|
||||||
code = prebakeScript + '\n' + code;
|
|
||||||
}
|
|
||||||
shareCode(code);
|
|
||||||
};
|
|
||||||
const context = {
|
const context = {
|
||||||
started,
|
started,
|
||||||
pending,
|
pending,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
|
import { evalScope, hash2code, logger } from '@strudel/core';
|
||||||
import { settingPatterns } from '../settings.mjs';
|
import { settingPatterns } from '../settings.mjs';
|
||||||
import { setVersionDefaults } from '@strudel/webaudio';
|
import { setVersionDefaults } from '@strudel/webaudio';
|
||||||
import { getMetadata } from '../metadata_parser';
|
import { getMetadata } from '../metadata_parser';
|
||||||
import { isTauri } from '../tauri.mjs';
|
import { isTauri } from '../tauri.mjs';
|
||||||
import './Repl.css';
|
import './Repl.css';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
||||||
|
|
||||||
@@ -108,8 +109,9 @@ export function confirmDialog(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastShared;
|
||||||
|
|
||||||
//RIP due to SPAM
|
//RIP due to SPAM
|
||||||
// let lastShared;
|
|
||||||
// export async function shareCode(codeToShare) {
|
// export async function shareCode(codeToShare) {
|
||||||
// // const codeToShare = activeCode || code;
|
// // const codeToShare = activeCode || code;
|
||||||
// if (lastShared === codeToShare) {
|
// if (lastShared === codeToShare) {
|
||||||
@@ -144,10 +146,9 @@ export function confirmDialog(msg) {
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export async function shareCode(codeToShare) {
|
export async function shareCode() {
|
||||||
try {
|
try {
|
||||||
const hash = '#' + code2hash(codeToShare);
|
const shareUrl = window.location.href;
|
||||||
const shareUrl = window.location.origin + window.location.pathname + hash;
|
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
await writeText(shareUrl);
|
await writeText(shareUrl);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user