mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 23:03:45 -04:00
Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5702914661 | |||
| f2a9e2d4e6 | |||
| 6398b5fb38 | |||
| 9ceebd4679 | |||
| 177f5c1431 | |||
| d6c2a88496 | |||
| 3ee7d0cd4f | |||
| 53d43dc44c | |||
| 637c619345 | |||
| 47fb441feb | |||
| d02b1fc05c | |||
| 480b5ff58b | |||
| c088c0c9d2 | |||
| f32abac06f | |||
| 697f0a82e6 | |||
| 98970417c3 | |||
| b5d48b2f0b | |||
| c48aba50f0 | |||
| f539930509 | |||
| 759cdb6276 | |||
| 3dae2a80ab | |||
| 7cc44a8829 | |||
| 90506f9e61 | |||
| c852b5fab0 | |||
| bd08b74ebc | |||
| 8f4dcb6bc6 | |||
| 17e1ebeb93 | |||
| ad25ce7b21 | |||
| 6e3aa693d5 | |||
| 2968017300 | |||
| ab76b4424a | |||
| b64daf6284 | |||
| 2b69004eb4 | |||
| a32bb58fb3 | |||
| 9e233fe587 | |||
| b1090a1dd8 | |||
| 344d9ca1a8 | |||
| b92a28242e | |||
| 695ac6a5d3 | |||
| a468201822 | |||
| 05ea9c86fb | |||
| 6e29210bdc | |||
| e81345ca2f | |||
| 6b8ee8ad11 | |||
| c2d4b529f6 | |||
| 999c0b2349 | |||
| 5ed28d6168 | |||
| bceb37e8ea | |||
| 2f4cba4e94 | |||
| d8e4055c6e | |||
| 6f9bcc53dc | |||
| 8962e5a4f0 | |||
| e55b7c6ce3 | |||
| 9b4574c7b0 | |||
| 6d355a803a | |||
| dee75bc759 | |||
| dc8e5d5394 | |||
| 282180ef1c | |||
| 3eea908636 | |||
| bfbd920b1d | |||
| 739fb8562f | |||
| cbd448e934 | |||
| b192c6823c | |||
| dccfefacd7 | |||
| 38876e135b | |||
| f652c2ca86 | |||
| 4f6c4bb95f | |||
| f5bb88a696 | |||
| b27c9dbf37 | |||
| 5fcb96f73c | |||
| 17df418935 | |||
| 2ccb95aec0 | |||
| 3c2692bdda | |||
| 756a65016d | |||
| a18d485283 | |||
| fcf6450ff0 | |||
| 09dd374722 | |||
| 9c9ce50b03 | |||
| edc2903106 | |||
| b15843f3a7 | |||
| 86333ab6f6 | |||
| 003e3b790d | |||
| 4048ced7e6 | |||
| 64b99f5a8c | |||
| b4af840240 | |||
| 8a502e886e | |||
| c7ee90922f | |||
| 530339ed8f | |||
| fece4e8b57 | |||
| afdcd42039 | |||
| 690b8648a8 | |||
| 7c1e968d41 | |||
| c0b3187029 | |||
| af1f7658c7 | |||
| b531962b1f | |||
| 8867a843ac | |||
| 6e972f4380 | |||
| 65282bf5ea | |||
| 3df9744d79 | |||
| 5a5bbfc8da | |||
| f66478b9da | |||
| 904c213d03 | |||
| d9c3f051b9 | |||
| 470a345296 | |||
| 0925ea56dd | |||
| 1f233b9e7d | |||
| 5c5d411588 | |||
| bbb88a2302 | |||
| 64c94b3623 | |||
| 42c2262576 | |||
| 4959bff060 | |||
| 1ded398468 | |||
| b016720696 | |||
| c639e44ca7 | |||
| b664bd3d1f | |||
| dfb7d7bb7f | |||
| 3e6a45cf8b | |||
| f47f5c1344 | |||
| 47f65da621 | |||
| c210ea3f89 | |||
| 50f076084f | |||
| 5cd98880ca | |||
| 43523b73c4 | |||
| b98ebc696d | |||
| f802f18d68 | |||
| 919c2854d5 | |||
| 451cdcc3a9 | |||
| 5ff1d35272 | |||
| 54c5454306 | |||
| 9b279ff671 | |||
| 20dcae68c1 | |||
| 52d1443cf8 | |||
| ed7dc4ef6e | |||
| 4636d824fa | |||
| 9d5d86bb09 | |||
| e2b7c3a62b | |||
| 1fb68af5af | |||
| b611c9c2b4 | |||
| ceb466ab9c | |||
| d09809ed12 | |||
| 23ed342635 | |||
| 443760c0d1 | |||
| bb56ed479f | |||
| c5d7f95441 | |||
| 3ffe3957ba | |||
| 3189b365c8 | |||
| e19d059c0f | |||
| b8b999eab5 | |||
| 393d17a41b | |||
| d06a75a2cd | |||
| 57c48f0c45 | |||
| a4792e29f5 | |||
| f95eadab2f | |||
| 1349ecb4af | |||
| 268c66cd3d | |||
| a8803784e1 | |||
| bfb4eee122 | |||
| 3325a8dbe9 | |||
| 13a4512601 | |||
| e085819fe2 | |||
| 155ef9e95f | |||
| c242f5f625 | |||
| 859f153ec6 | |||
| 0be9f439bc | |||
| 36fe942e24 | |||
| 06958a5281 | |||
| 033b123ae0 | |||
| f01d51f76a | |||
| d741b4d17a | |||
| bd69ffb4b7 | |||
| 23a4bf6641 | |||
| ec470aa2c6 | |||
| b233c33f56 | |||
| e3b6884b86 |
@@ -31,6 +31,10 @@ This project is organized into many [packages](./packages), which are also avail
|
||||
|
||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
|
||||
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
|
||||
|
||||
## Contributing
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@tauri-apps/cli": "^2.2.7",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/ui": "^3.0.4",
|
||||
"acorn": "^8.14.0",
|
||||
"dependency-tree": "^11.0.1",
|
||||
|
||||
@@ -67,6 +67,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
);
|
||||
|
||||
initTheme(settings.theme);
|
||||
let state = EditorState.create({
|
||||
doc: initialCode,
|
||||
|
||||
@@ -11,7 +11,9 @@ export const flashField = StateField.define({
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value && tr.newDoc.length > 0) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
const mark = Decoration.mark({
|
||||
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
|
||||
});
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
Vendored
+26
-31
@@ -4,16 +4,16 @@ import blackscreen, { settings as blackscreenSettings } from './themes/blackscre
|
||||
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
|
||||
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
|
||||
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
|
||||
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
|
||||
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
|
||||
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
|
||||
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
|
||||
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
|
||||
import aura, { settings as auraSettings } from './themes/aura.mjs';
|
||||
import bespin, { settings as bespinSettings } from './themes/bespin.mjs';
|
||||
import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
|
||||
import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
|
||||
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
|
||||
import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs';
|
||||
import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
|
||||
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
|
||||
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
|
||||
@@ -31,51 +31,47 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t
|
||||
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
|
||||
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
|
||||
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
|
||||
// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs';
|
||||
import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
|
||||
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
|
||||
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
|
||||
|
||||
import { setTheme } from '@strudel/draw';
|
||||
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
bluescreen,
|
||||
blackscreen,
|
||||
whitescreen,
|
||||
teletext,
|
||||
algoboy,
|
||||
androidstudio,
|
||||
atomone,
|
||||
aura,
|
||||
bbedit,
|
||||
blackscreen,
|
||||
bluescreen,
|
||||
CutiePi,
|
||||
darcula,
|
||||
dracula,
|
||||
// todo: optimize
|
||||
// bespin,
|
||||
//abcdef,
|
||||
androidstudio,
|
||||
duotoneDark,
|
||||
eclipse,
|
||||
githubDark,
|
||||
githubLight,
|
||||
greenText,
|
||||
gruvboxDark,
|
||||
gruvboxLight,
|
||||
sonicPink,
|
||||
materialDark,
|
||||
nord,
|
||||
materialLight,
|
||||
monokai,
|
||||
noctisLilac,
|
||||
nord,
|
||||
redText,
|
||||
solarizedDark,
|
||||
solarizedLight,
|
||||
sublime,
|
||||
teletext,
|
||||
tokyoNight,
|
||||
tokyoNightDay,
|
||||
tokyoNightStorm,
|
||||
vscodeDark,
|
||||
//xcodeDark,
|
||||
// LIGHT
|
||||
bbedit,
|
||||
//duotoneLight,
|
||||
eclipse,
|
||||
githubLight,
|
||||
gruvboxLight,
|
||||
materialLight,
|
||||
vscodeLight,
|
||||
noctisLilac,
|
||||
solarizedLight,
|
||||
tokyoNightDay,
|
||||
whitescreen,
|
||||
xcodeLight,
|
||||
};
|
||||
|
||||
@@ -86,20 +82,19 @@ export const settings = {
|
||||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
terminal: terminalSettings,
|
||||
abcdef: abcdefSettings,
|
||||
androidstudio: androidstudioSettings,
|
||||
atomone: atomOneSettings,
|
||||
aura: auraSettings,
|
||||
bbedit: bbeditSettings,
|
||||
bespin: bespinSettings,
|
||||
darcula: darculaSettings,
|
||||
dracula: draculaSettings,
|
||||
duotoneLight: duotoneLightSettings,
|
||||
duotoneDark: duotoneDarkSettings,
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
sonicPink: sonicPinkSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
greenText: greenTextSettings,
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
gruvboxLight: gruvboxLightSettings,
|
||||
materialDark: materialDarkSettings,
|
||||
@@ -107,6 +102,7 @@ export const settings = {
|
||||
noctisLilac: noctisLilacSettings,
|
||||
nord: nordSettings,
|
||||
monokai: monokaiSettings,
|
||||
redText: redTextSettings,
|
||||
solarizedLight: solarizedLightSettings,
|
||||
solarizedDark: solarizedDarkSettings,
|
||||
sublime: sublimeSettings,
|
||||
@@ -115,7 +111,6 @@ export const settings = {
|
||||
vscodeDark: vscodeDarkSettings,
|
||||
vscodeLight: vscodeLightSettings,
|
||||
xcodeLight: xcodeLightSettings,
|
||||
//xcodeDark: xcodeDarkSettings,
|
||||
tokyoNightDay: tokyoNightDaySettings,
|
||||
};
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @name Cutie Pi
|
||||
* by Switch Angel
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
const deepPurple = '#5c019a';
|
||||
const yellowPink = '#fbeffc';
|
||||
const grey = '#272C35';
|
||||
const pinkAccent = '#fee1ff';
|
||||
const lightGrey = '#465063';
|
||||
const bratGreen = '#9acd3f';
|
||||
const lighterGrey = '#97a1b7';
|
||||
const pink = '#f6a6fd';
|
||||
|
||||
export const settings = {
|
||||
background: 'white',
|
||||
lineBackground: 'transparent',
|
||||
foreground: deepPurple,
|
||||
caret: '#797977',
|
||||
selection: yellowPink,
|
||||
selectionMatch: '#2B323D',
|
||||
gutterBackground: grey,
|
||||
gutterForeground: lightGrey,
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: pinkAccent,
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: deepPurple,
|
||||
},
|
||||
{ tag: [t.tagName, t.heading], color: settings.foreground },
|
||||
{ tag: t.comment, color: lighterGrey },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: pink },
|
||||
{ tag: [t.attributeName, t.number], color: '#d19a66' },
|
||||
{ tag: t.className, color: grey },
|
||||
{ tag: t.keyword, color: deepPurple },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen },
|
||||
],
|
||||
});
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* @name abcdef
|
||||
* @author codemirror.net
|
||||
* https://codemirror.net/5/theme/abcdef.css
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
export const settings = {
|
||||
background: '#0f0f0f',
|
||||
lineBackground: '#0f0f0f99',
|
||||
foreground: '#defdef',
|
||||
caret: '#00FF00',
|
||||
selection: '#515151',
|
||||
selectionMatch: '#515151',
|
||||
gutterBackground: '#555',
|
||||
gutterForeground: '#FFFFFF',
|
||||
lineHighlight: '#314151',
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
background: '#0f0f0f',
|
||||
foreground: '#defdef',
|
||||
caret: '#00FF00',
|
||||
selection: '#515151',
|
||||
selectionMatch: '#515151',
|
||||
// gutterBackground: '#555',
|
||||
gutterBackground: 'transparent',
|
||||
/* gutterForeground: '#FFFFFF', */
|
||||
gutterForeground: '#7a7b7c',
|
||||
lineHighlight: '#0a6bcb3d',
|
||||
},
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' },
|
||||
{ tag: t.atom, color: '#77F' },
|
||||
{ tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' },
|
||||
{ tag: t.number, color: 'violet' },
|
||||
{ tag: t.definition(t.variableName), color: '#fffabc' },
|
||||
{ tag: t.variableName, color: '#abcdef' },
|
||||
{ tag: t.function(t.variableName), color: '#fffabc' },
|
||||
{ tag: t.typeName, color: '#FFDD44' },
|
||||
{ tag: t.tagName, color: '#def' },
|
||||
{ tag: t.string, color: '#2b4' },
|
||||
{ tag: t.meta, color: '#C9F' },
|
||||
// { tag: t.qualifier, color: '#FFF700' },
|
||||
// { tag: t.builtin, color: '#30aabc' },
|
||||
{ tag: t.bracket, color: '#8a8a8a' },
|
||||
{ tag: t.attributeName, color: '#DDFF00' },
|
||||
{ tag: t.heading, color: 'aquamarine', fontWeight: 'bold' },
|
||||
{ tag: t.link, color: 'blueviolet', fontWeight: 'bold' },
|
||||
],
|
||||
});
|
||||
+1
@@ -54,6 +54,7 @@ export default createTheme({
|
||||
tag: [t.keyword, t.tagName, t.arithmeticOperator],
|
||||
color: palette[1],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] },
|
||||
{ tag: [t.function(t.variableName), t.propertyName], color: palette[0] },
|
||||
{ tag: t.atom, color: palette[1] },
|
||||
],
|
||||
|
||||
+2
@@ -38,12 +38,14 @@ export default createTheme({
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: 'hsl(207, 82%, 66%)',
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' },
|
||||
{ tag: [t.tagName, t.heading], color: '#e06c75' },
|
||||
{ tag: t.comment, color: '#54636D' },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
|
||||
{ tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
|
||||
{ tag: t.className, color: 'hsl( 39, 67%, 69%)' },
|
||||
{ tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
|
||||
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
|
||||
],
|
||||
});
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
// this is different from https://thememirror.net/bespin
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
export const settings = {
|
||||
background: '#28211c',
|
||||
lineBackground: '#28211c99',
|
||||
foreground: '#9d9b97',
|
||||
caret: '#797977',
|
||||
selection: '#36312e',
|
||||
selectionMatch: '#4f382b',
|
||||
gutterBackground: '#28211c',
|
||||
gutterForeground: '#666666',
|
||||
lineHighlight: 'rgba(255, 255, 255, 0.1)',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
background: '#28211c',
|
||||
foreground: '#9d9b97',
|
||||
caret: '#797977',
|
||||
selection: '#4f382b',
|
||||
selectionMatch: '#4f382b',
|
||||
gutterBackground: '#28211c',
|
||||
gutterForeground: '#666666',
|
||||
lineHighlight: '#ffffff1a',
|
||||
},
|
||||
styles: [
|
||||
{ tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' },
|
||||
{ tag: t.comment, color: '#937121' },
|
||||
{ tag: [t.keyword, t.tagName], color: '#cf6a4c' },
|
||||
{ tag: t.string, color: '#f9ee98' },
|
||||
{ tag: t.bracket, color: '#9d9b97' },
|
||||
{ tag: [t.variableName], color: '#5ea6ea' },
|
||||
{ tag: t.definition(t.variableName), color: '#cf7d34' },
|
||||
{ tag: [t.function(t.variableName), t.className], color: '#cf7d34' },
|
||||
{ tag: [t.propertyName, t.attributeName], color: '#54be0d' },
|
||||
],
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @name Atom One
|
||||
* Atom One dark syntax theme
|
||||
*
|
||||
* https://github.com/atom/one-dark-syntax
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[2],
|
||||
selection: hex[4],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[3],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[2],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: t.comment, color: hex[3] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[1] },
|
||||
{ tag: t.keyword, color: hex[2] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
|
||||
],
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @name Atom One
|
||||
* Atom One dark syntax theme
|
||||
*
|
||||
* https://github.com/atom/one-dark-syntax
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[2],
|
||||
selection: hex[4],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[3],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[2],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: t.comment, color: hex[3] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[1] },
|
||||
{ tag: t.keyword, color: hex[2] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
|
||||
],
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @name Atom One
|
||||
* Atom One dark syntax theme
|
||||
*
|
||||
* https://github.com/atom/one-dark-syntax
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c'];
|
||||
|
||||
export const settings = {
|
||||
background: '#000000',
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[4],
|
||||
selection: hex[6],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[5],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[4],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] },
|
||||
|
||||
{ tag: t.comment, color: '#54636D' },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[3] },
|
||||
{ tag: t.keyword, color: hex[1] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
|
||||
],
|
||||
});
|
||||
+1
-3
@@ -5,14 +5,11 @@ export const settings = {
|
||||
background: '#222',
|
||||
lineBackground: '#22222299',
|
||||
foreground: '#fff',
|
||||
// foreground: '#75baff',
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
// lineHighlight: '#8a91991a', // original
|
||||
lineHighlight: '#00000050',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#8a919966',
|
||||
};
|
||||
|
||||
@@ -20,6 +17,7 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
|
||||
{ tag: t.labelName, color: '#89ddff' },
|
||||
{ tag: t.keyword, color: '#c792ea' },
|
||||
{ tag: t.operator, color: '#89ddff' },
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
export const settings = {
|
||||
background: '#292A30',
|
||||
lineBackground: '#292A3099',
|
||||
foreground: '#CECFD0',
|
||||
caret: '#fff',
|
||||
selection: '#727377',
|
||||
selectionMatch: '#727377',
|
||||
lineHighlight: '#2F3239',
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
background: '#292A30',
|
||||
foreground: '#CECFD0',
|
||||
caret: '#fff',
|
||||
selection: '#727377',
|
||||
selectionMatch: '#727377',
|
||||
lineHighlight: '#ffffff0f',
|
||||
},
|
||||
styles: [
|
||||
{ tag: [t.comment, t.quote], color: '#7F8C98' },
|
||||
{ tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' },
|
||||
{ tag: [t.string, t.meta], color: '#FF8170' },
|
||||
{ tag: [t.typeName], color: '#DABAFF' },
|
||||
{ tag: [t.definition(t.variableName)], color: '#6BDFFF' },
|
||||
{ tag: [t.name], color: '#6BAA9F' },
|
||||
{ tag: [t.variableName], color: '#ACF2E4' },
|
||||
{ tag: [t.regexp, t.link], color: '#FF8170' },
|
||||
],
|
||||
});
|
||||
+180
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
controls.mjs - <short description TODO>
|
||||
controls.mjs - Registers audio controls for pattern manipulation and effects.
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs>
|
||||
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/>.
|
||||
*/
|
||||
@@ -1513,22 +1513,11 @@ export const { scram } = registerControl('scram');
|
||||
export const { binshift } = registerControl('binshift');
|
||||
export const { hbrick } = registerControl('hbrick');
|
||||
export const { lbrick } = registerControl('lbrick');
|
||||
export const { midichan } = registerControl('midichan');
|
||||
export const { midimap } = registerControl('midimap');
|
||||
export const { midiport } = registerControl('midiport');
|
||||
export const { control } = registerControl('control');
|
||||
export const { ccn } = registerControl('ccn');
|
||||
export const { ccv } = registerControl('ccv');
|
||||
export const { polyTouch } = registerControl('polyTouch');
|
||||
export const { midibend } = registerControl('midibend');
|
||||
export const { miditouch } = registerControl('miditouch');
|
||||
export const { ctlNum } = registerControl('ctlNum');
|
||||
|
||||
export const { frameRate } = registerControl('frameRate');
|
||||
export const { frames } = registerControl('frames');
|
||||
export const { hours } = registerControl('hours');
|
||||
export const { midicmd } = registerControl('midicmd');
|
||||
export const { minutes } = registerControl('minutes');
|
||||
export const { progNum } = registerControl('progNum');
|
||||
export const { seconds } = registerControl('seconds');
|
||||
export const { songPtr } = registerControl('songPtr');
|
||||
export const { uid } = registerControl('uid');
|
||||
@@ -1621,6 +1610,151 @@ export const ar = register('ar', (t, pat) => {
|
||||
return pat.set({ attack, release });
|
||||
});
|
||||
|
||||
//MIDI
|
||||
|
||||
/**
|
||||
* MIDI channel: Sets the MIDI channel for the event.
|
||||
*
|
||||
* @name midichan
|
||||
* @param {number | Pattern} channel MIDI channel number (0-15)
|
||||
* @example
|
||||
* note("c4").midichan(1).midi()
|
||||
*/
|
||||
export const { midichan } = registerControl('midichan');
|
||||
|
||||
export const { midimap } = registerControl('midimap');
|
||||
|
||||
/**
|
||||
* MIDI port: Sets the MIDI port for the event.
|
||||
*
|
||||
* @name midiport
|
||||
* @param {number | Pattern} port MIDI port
|
||||
* @example
|
||||
* note("c a f e").midiport("<0 1 2 3>").midi()
|
||||
*/
|
||||
export const { midiport } = registerControl('midiport');
|
||||
|
||||
/**
|
||||
* MIDI command: Sends a MIDI command message.
|
||||
*
|
||||
* @name midicmd
|
||||
* @param {number | Pattern} command MIDI command
|
||||
* @example
|
||||
* midicmd("clock*48,<start stop>/2").midi()
|
||||
*/
|
||||
export const { midicmd } = registerControl('midicmd');
|
||||
|
||||
/**
|
||||
* MIDI control: Sends a MIDI control change message.
|
||||
*
|
||||
* @name control
|
||||
* @param {number | Pattern} MIDI control number (0-127)
|
||||
* @param {number | Pattern} MIDI controller value (0-127)
|
||||
*/
|
||||
export const control = register('control', (args, pat) => {
|
||||
if (!Array.isArray(args)) {
|
||||
throw new Error('control expects an array of [ccn, ccv]');
|
||||
}
|
||||
const [_ccn, _ccv] = args;
|
||||
return pat.ccn(_ccn).ccv(_ccv);
|
||||
});
|
||||
|
||||
/**
|
||||
* MIDI control number: Sends a MIDI control change message.
|
||||
*
|
||||
* @name ccn
|
||||
* @param {number | Pattern} MIDI control number (0-127)
|
||||
*/
|
||||
export const { ccn } = registerControl('ccn');
|
||||
/**
|
||||
* MIDI control value: Sends a MIDI control change message.
|
||||
*
|
||||
* @name ccv
|
||||
* @param {number | Pattern} MIDI control value (0-127)
|
||||
*/
|
||||
export const { ccv } = registerControl('ccv');
|
||||
export const { ctlNum } = registerControl('ctlNum');
|
||||
// TODO: ctlVal?
|
||||
|
||||
/**
|
||||
* MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message.
|
||||
* @name nrpnn
|
||||
* @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127)
|
||||
* @example
|
||||
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
|
||||
*/
|
||||
export const { nrpnn } = registerControl('nrpnn');
|
||||
/**
|
||||
* MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message.
|
||||
* @name nrpv
|
||||
* @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127)
|
||||
* @example
|
||||
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
|
||||
*/
|
||||
export const { nrpv } = registerControl('nrpv');
|
||||
|
||||
/**
|
||||
* MIDI program number: Sends a MIDI program change message.
|
||||
*
|
||||
* @name progNum
|
||||
* @param {number | Pattern} program MIDI program number (0-127)
|
||||
* @example
|
||||
* note("c4").progNum(10).midichan(1).midi()
|
||||
*/
|
||||
export const { progNum } = registerControl('progNum');
|
||||
|
||||
/**
|
||||
* MIDI sysex: Sends a MIDI sysex message.
|
||||
* @name sysex
|
||||
* @param {number | Pattern} id Sysex ID
|
||||
* @param {number | Pattern} data Sysex data
|
||||
* @example
|
||||
* note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi()
|
||||
*/
|
||||
export const sysex = register('sysex', (args, pat) => {
|
||||
if (!Array.isArray(args)) {
|
||||
throw new Error('sysex expects an array of [id, data]');
|
||||
}
|
||||
const [id, data] = args;
|
||||
return pat.sysexid(id).sysexdata(data);
|
||||
});
|
||||
/**
|
||||
* MIDI sysex ID: Sends a MIDI sysex identifier message.
|
||||
* @name sysexid
|
||||
* @param {number | Pattern} id Sysex ID
|
||||
* @example
|
||||
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
|
||||
*/
|
||||
export const { sysexid } = registerControl('sysexid');
|
||||
/**
|
||||
* MIDI sysex data: Sends a MIDI sysex message.
|
||||
* @name sysexdata
|
||||
* @param {number | Pattern} data Sysex data
|
||||
* @example
|
||||
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
|
||||
*/
|
||||
export const { sysexdata } = registerControl('sysexdata');
|
||||
|
||||
/**
|
||||
* MIDI pitch bend: Sends a MIDI pitch bend message.
|
||||
* @name midibend
|
||||
* @param {number | Pattern} midibend MIDI pitch bend (-1 - 1)
|
||||
* @example
|
||||
* note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi()
|
||||
*/
|
||||
export const { midibend } = registerControl('midibend');
|
||||
/**
|
||||
* MIDI key after touch: Sends a MIDI key after touch message.
|
||||
* @name miditouch
|
||||
* @param {number | Pattern} miditouch MIDI key after touch (0-1)
|
||||
* @example
|
||||
* note("c4").miditouch(sine.slow(4).range(0,1)).midi()
|
||||
*/
|
||||
export const { miditouch } = registerControl('miditouch');
|
||||
|
||||
// TODO: what is this?
|
||||
export const { polyTouch } = registerControl('polyTouch');
|
||||
|
||||
export const getControlName = (alias) => {
|
||||
if (controlAlias.has(alias)) {
|
||||
return controlAlias.get(alias);
|
||||
@@ -1632,14 +1766,46 @@ export const getControlName = (alias) => {
|
||||
* Sets properties in a batch.
|
||||
*
|
||||
* @name as
|
||||
* @param {Array} mapping the control names that are set
|
||||
* @param {String | Array} mapping the control names that are set
|
||||
* @example
|
||||
* "c:.5 a:1 f:.25 e:.8".as("note:clip")
|
||||
* @example
|
||||
* "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1)
|
||||
*/
|
||||
export const as = register('as', (mapping, pat) => {
|
||||
mapping = Array.isArray(mapping) ? mapping : [mapping];
|
||||
return pat.fmap((v) => {
|
||||
v = Array.isArray(v) ? v : [v];
|
||||
v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]]));
|
||||
return v;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file
|
||||
* in the optional array syntax ex: "0.5:2", the second value controls the speed of playback
|
||||
* @name scrub
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8")
|
||||
* @example
|
||||
* samples('github:yaxu/clean-breaks/main');
|
||||
* s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16))
|
||||
*/
|
||||
|
||||
export const scrub = register(
|
||||
'scrub',
|
||||
(beginPat, pat) => {
|
||||
return beginPat.outerBind((v) => {
|
||||
if (!Array.isArray(v)) {
|
||||
v = [v];
|
||||
}
|
||||
const [beginVal, speedMultiplier = 1] = v;
|
||||
|
||||
return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1);
|
||||
});
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
+31
-38
@@ -1244,6 +1244,16 @@ export function reify(thing) {
|
||||
return pure(thing);
|
||||
}
|
||||
|
||||
/** Takes a list of patterns, and returns a pattern of lists.
|
||||
*/
|
||||
export function sequenceP(pats) {
|
||||
let result = pure([]);
|
||||
for (const pat of pats) {
|
||||
result = result.bind((list) => pat.fmap((v) => list.concat([v])));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
*
|
||||
* @return {Pattern}
|
||||
@@ -1318,7 +1328,7 @@ export function stackBy(by, ...pats) {
|
||||
left: stackLeft,
|
||||
right: stackRight,
|
||||
expand: stack,
|
||||
repeat: (...args) => polymeterSteps(steps, ...args),
|
||||
repeat: (...args) => polymeter(...args).steps(steps),
|
||||
};
|
||||
return by
|
||||
.inhabit(lookup)
|
||||
@@ -2033,7 +2043,7 @@ export const zoom = register('zoom', function (s, e, pat) {
|
||||
return nothing;
|
||||
}
|
||||
const d = e.sub(s);
|
||||
const steps = __steps ? pat._steps.mulmaybe(d) : undefined;
|
||||
const steps = __steps ? pat._steps?.mulmaybe(d) : undefined;
|
||||
return pat
|
||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
@@ -2095,11 +2105,13 @@ export const linger = register(
|
||||
|
||||
/**
|
||||
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
|
||||
* @name segment
|
||||
* @synonyms seg
|
||||
* @param {number} segments number of segments per cycle
|
||||
* @example
|
||||
* note(saw.range(40,52).segment(24))
|
||||
*/
|
||||
export const segment = register('segment', function (rate, pat) {
|
||||
export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) {
|
||||
return pat.struct(pure(true)._fast(rate)).setSteps(rate);
|
||||
});
|
||||
|
||||
@@ -2475,16 +2487,24 @@ export const bypass = register(
|
||||
);
|
||||
|
||||
/**
|
||||
* Loops the pattern inside at `offset` for `cycles`.
|
||||
* Loops the pattern inside an `offset` for `cycles`.
|
||||
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
|
||||
* @name ribbon
|
||||
* @synonym rib
|
||||
* @param {number} offset start point of loop in cycles
|
||||
* @param {number} cycles loop length in cycles
|
||||
* @example
|
||||
* note("<c d e f>").ribbon(1, 2).fast(2)
|
||||
* note("<c d e f>").ribbon(1, 2)
|
||||
* @example
|
||||
* // Looping a portion of randomness
|
||||
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
|
||||
* n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2)
|
||||
* @example
|
||||
* // rhythm generator
|
||||
* s("bd!16?").ribbon(29,.5)
|
||||
*/
|
||||
export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles)));
|
||||
export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) =>
|
||||
pat.early(offset).restart(pure(1).slow(cycles)),
|
||||
);
|
||||
|
||||
export const hsla = register('hsla', (h, s, l, a, pat) => {
|
||||
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
|
||||
@@ -2638,34 +2658,10 @@ export function _polymeterListSteps(steps, ...args) {
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
* Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters.
|
||||
*
|
||||
* @name polymeterSteps
|
||||
* @param {number} steps how many items are placed in one cycle
|
||||
* @param {any[]} patterns one or more patterns
|
||||
* @example
|
||||
* // the same as "{c d, e f g}%4"
|
||||
* polymeterSteps(4, "c d", "e f g").note()
|
||||
*/
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(steps, ...args);
|
||||
}
|
||||
|
||||
return polymeter(...args).pace(steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
* Aligns the steps of the patterns, to match the steps per cycle of the first pattern, creating polymeters. See `polymeterSteps` to set the target steps explicitly.
|
||||
* Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
|
||||
* @synonyms pm
|
||||
* @example
|
||||
* // The same as note("{c eb g, c2 g2}")
|
||||
* // The same as note("{c eb g, c2 g2}%6")
|
||||
* polymeter("c eb g", "c2 g2").note()
|
||||
*
|
||||
*/
|
||||
@@ -2681,13 +2677,12 @@ export function polymeter(...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
const steps = args[0]._steps;
|
||||
const steps = lcm(...args.map((x) => x._steps));
|
||||
if (steps.eq(Fraction(0))) {
|
||||
return nothing;
|
||||
}
|
||||
const [head, ...tail] = args;
|
||||
|
||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps))));
|
||||
const result = stack(...args.map((x) => x.pace(steps)));
|
||||
result._steps = steps;
|
||||
return result;
|
||||
}
|
||||
@@ -3055,8 +3050,6 @@ export const timeCat = stepcat;
|
||||
// Deprecated stepwise aliases
|
||||
export const s_cat = stepcat;
|
||||
export const s_alt = stepalt;
|
||||
export const s_polymeterSteps = polymeterSteps;
|
||||
Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps;
|
||||
export const s_polymeter = polymeter;
|
||||
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
|
||||
export const s_taper = shrink;
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
|
||||
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||
@@ -433,19 +433,30 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||
export const randcat = chooseCycles;
|
||||
|
||||
const _wchooseWith = function (pat, ...pairs) {
|
||||
// A list of patterns of values
|
||||
const values = pairs.map((pair) => reify(pair[0]));
|
||||
|
||||
// A list of weight patterns
|
||||
const weights = [];
|
||||
let accum = 0;
|
||||
|
||||
let total = pure(0);
|
||||
for (const pair of pairs) {
|
||||
accum += pair[1];
|
||||
weights.push(accum);
|
||||
// 'add' accepts either values or patterns of values here, so no need
|
||||
// to explicitly reify
|
||||
total = total.add(pair[1]);
|
||||
// accumulate our list of weight patterns
|
||||
weights.push(total);
|
||||
}
|
||||
const total = accum;
|
||||
// a pattern of lists of weights
|
||||
const weightspat = sequenceP(weights);
|
||||
|
||||
// Takes a number from 0-1, returns a pattern of patterns of values
|
||||
const match = function (r) {
|
||||
const find = r * total;
|
||||
return values[weights.findIndex((x) => x > find, weights)];
|
||||
const findpat = total.mul(r);
|
||||
return weightspat.fmap((weights) => (find) => values[weights.findIndex((x) => x > find, weights)]).appLeft(findpat);
|
||||
};
|
||||
return pat.fmap(match);
|
||||
// This returns a pattern of patterns.. The innerJoin is in wchooseCycles
|
||||
return pat.bind(match);
|
||||
};
|
||||
|
||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||
@@ -467,6 +478,9 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||
* @example
|
||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||
* @example
|
||||
* // The probability can itself be a pattern
|
||||
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||
*/
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
||||
|
||||
@@ -512,6 +526,9 @@ export const degradeByWith = register(
|
||||
* s("hh*8").degradeBy(0.2)
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
* @example
|
||||
* //beat generator
|
||||
* s("bd").segment(16).degradeBy(.5).ribbon(16,1)
|
||||
*/
|
||||
export const degradeBy = register(
|
||||
'degradeBy',
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
sequence,
|
||||
palindrome,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
silence,
|
||||
fast,
|
||||
@@ -611,17 +610,10 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('polymeter()', () => {
|
||||
it('Can layer up cycles, stepwise, with lists', () => {
|
||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||
);
|
||||
|
||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
||||
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstOf()', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# @strudel/gamepad
|
||||
|
||||
This package adds gamepad input functionality to strudel Patterns.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel/gamepad --save
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { gamepad } from '@strudel/gamepad';
|
||||
|
||||
// Initialize gamepad (optional index parameter, defaults to 0)
|
||||
const pad = gamepad(0);
|
||||
|
||||
// Use gamepad inputs in patterns
|
||||
const pattern = sequence([
|
||||
// Button inputs
|
||||
pad.a, // A button value (0-1)
|
||||
pad.tglA, // A button toggle (0 or 1)
|
||||
|
||||
// Analog stick inputs
|
||||
pad.x1, // Left stick X (0-1)
|
||||
pad.x1_2, // Left stick X (-1 to 1)
|
||||
]);
|
||||
```
|
||||
|
||||
## Available Controls
|
||||
|
||||
### Buttons
|
||||
- Face Buttons
|
||||
- `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`)
|
||||
- Toggle versions: `tglA`, `tglB`, `tglX`, `tglY`
|
||||
- Shoulder Buttons
|
||||
- `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`)
|
||||
- Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT`
|
||||
- D-Pad
|
||||
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
|
||||
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
|
||||
|
||||
### Analog Sticks
|
||||
- Left Stick
|
||||
- `x1`, `y1` (0 to 1 range)
|
||||
- `x1_2`, `y1_2` (-1 to 1 range)
|
||||
- Right Stick
|
||||
- `x2`, `y2` (0 to 1 range)
|
||||
- `x2_2`, `y2_2` (-1 to 1 range)
|
||||
|
||||
## Examples
|
||||
|
||||
```javascript
|
||||
// Use button values to control amplitude
|
||||
$: sequence([
|
||||
s("bd").gain(pad.X), // X button controls gain
|
||||
s("[hh oh]").gain(pad.tglY), // Y button toggles gain
|
||||
]);
|
||||
|
||||
// Use analog stick for continuous control
|
||||
$: note("c4*4".add(pad.y1_2.range(-24,24))) // Left stick Y controls pitch shift
|
||||
.pan(pad.x1_2); // Left stick X controls panning
|
||||
|
||||
// Use toggle buttons to switch patterns on/off
|
||||
|
||||
// Define button sequences
|
||||
const HADOKEN = [
|
||||
'd', // Down
|
||||
'r', // Right
|
||||
'a', // A
|
||||
];
|
||||
|
||||
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||
|
||||
// Add these lines to enable buttons(but why?)
|
||||
$:pad.D.segment(16).gain(0)
|
||||
$:pad.R.segment(16).gain(0)
|
||||
$:pad.A.segment(16).gain(0)
|
||||
|
||||
// Check button sequence (returns 1 when detected, 0 when not within last 1 second)
|
||||
$: sound("hadoken").gain(pad.checkSequence(HADOKEN))
|
||||
|
||||
```
|
||||
|
||||
## Multiple Gamepads
|
||||
|
||||
You can connect multiple gamepads by specifying the gamepad index:
|
||||
|
||||
```javascript
|
||||
const pad1 = gamepad(0); // First gamepad
|
||||
const pad2 = gamepad(1); // Second gamepad
|
||||
```
|
||||
@@ -0,0 +1,117 @@
|
||||
import { MiniRepl } from '../../../website/src/docs/MiniRepl';
|
||||
|
||||
# Gamepad
|
||||
|
||||
The Gamepad module allows you to integrate gamepad input functionality into your musical patterns. This can be particularly useful for live performances or interactive installations where you want to manipulate sounds using a game controller.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Initialize a gamepad by calling the gamepad() function with an optional index parameter.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`// Initialize gamepad (optional index parameter, defaults to 0)
|
||||
const gp = gamepad(0)
|
||||
note("c a f e").mask(gp.a)`}
|
||||
/>
|
||||
|
||||
## Available Controls
|
||||
|
||||
The gamepad module provides access to buttons and analog sticks as normalized signals (0-1) that can modulate your patterns.
|
||||
|
||||
### Buttons
|
||||
|
||||
| Type | Controls |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Face Buttons | `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) |
|
||||
| | Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` |
|
||||
| Shoulder Buttons | `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) |
|
||||
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
|
||||
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
|
||||
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
|
||||
|
||||
### Analog Sticks
|
||||
|
||||
| Stick | Controls |
|
||||
| ----------- | ------------------------------ |
|
||||
| Left Stick | `x1`, `y1` (0 to 1 range) |
|
||||
| | `x1_2`, `y1_2` (-1 to 1 range) |
|
||||
| Right Stick | `x2`, `y2` (0 to 1 range) |
|
||||
| | `x2_2`, `y2_2` (-1 to 1 range) |
|
||||
|
||||
### Button Sequence
|
||||
|
||||
| Stick | Controls |
|
||||
| --------------- | --------------------------------------- |
|
||||
| Button Sequence | `btnSequence()`, `btnSeq()`, `btnseq()` |
|
||||
|
||||
## Using Gamepad Inputs
|
||||
|
||||
Once initialized, you can use various gamepad inputs in your patterns. Here are some examples:
|
||||
|
||||
### Button Inputs
|
||||
|
||||
You can use button inputs to control different aspects of your music, such as gain or triggering events.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use button values to control amplitude
|
||||
$: stack(
|
||||
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
|
||||
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
|
||||
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
|
||||
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
|
||||
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
|
||||
).cpm(120)
|
||||
`}
|
||||
/>
|
||||
|
||||
### Analog Stick Inputs
|
||||
|
||||
Analog sticks can be used for continuous control, such as pitch shifting or panning.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use analog stick for continuous control
|
||||
$: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
.lpf(gp.x1.range(100,4000))
|
||||
.lpq(gp.y1.range(5,30))
|
||||
.decay(gp.y2.range(0.1,2))
|
||||
.lpenv(gp.x2.range(-5,5))
|
||||
.cpm(120)
|
||||
`}
|
||||
/>
|
||||
|
||||
### Button Sequences
|
||||
|
||||
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
|
||||
|
||||
<MiniRepl client:idle tune={`const gp = gamepad(0)
|
||||
// Define button sequences
|
||||
const HADOUKEN = [
|
||||
'd', // Down
|
||||
'r', // Right
|
||||
'a', // A
|
||||
]
|
||||
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||
|
||||
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
|
||||
$: s("free_hadouken -").slow(2)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
|
||||
|
||||
// hadouken.wav by Syna-Max
|
||||
//https://freesound.org/people/Syna-Max/sounds/67674/
|
||||
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
||||
`} />
|
||||
|
||||
## Multiple Gamepads
|
||||
|
||||
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const pad1 = gamepad(0); // First gamepad
|
||||
const pad2 = gamepad(1); // Second gamepad`}
|
||||
/>
|
||||
@@ -0,0 +1,246 @@
|
||||
// @strudel/gamepad/index.mjs
|
||||
|
||||
import { signal } from '@strudel/core';
|
||||
|
||||
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
||||
export const buttonMap = {
|
||||
a: 0,
|
||||
b: 1,
|
||||
x: 2,
|
||||
y: 3,
|
||||
lb: 4,
|
||||
rb: 5,
|
||||
lt: 6,
|
||||
rt: 7,
|
||||
back: 8,
|
||||
start: 9,
|
||||
u: 12,
|
||||
up: 12,
|
||||
d: 13,
|
||||
down: 13,
|
||||
l: 14,
|
||||
left: 14,
|
||||
r: 15,
|
||||
right: 15,
|
||||
};
|
||||
|
||||
class ButtonSequenceDetector {
|
||||
constructor(timeWindow = 1000) {
|
||||
this.sequence = [];
|
||||
this.timeWindow = timeWindow;
|
||||
this.lastInputTime = 0;
|
||||
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
||||
// Button mapping for character inputs
|
||||
}
|
||||
|
||||
addInput(buttonIndex, buttonValue) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Only add input on button press (rising edge)
|
||||
if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) {
|
||||
// Clear sequence if too much time has passed
|
||||
if (currentTime - this.lastInputTime > this.timeWindow) {
|
||||
this.sequence = [];
|
||||
}
|
||||
|
||||
// Store the button name instead of index
|
||||
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
||||
|
||||
this.sequence.push({
|
||||
input: buttonName,
|
||||
timestamp: currentTime,
|
||||
});
|
||||
|
||||
this.lastInputTime = currentTime;
|
||||
|
||||
//console.log(this.sequence);
|
||||
// Keep only inputs within the time window
|
||||
this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow);
|
||||
}
|
||||
|
||||
// Update button state
|
||||
this.buttonStates[buttonIndex] = buttonValue;
|
||||
}
|
||||
|
||||
checkSequence(targetSequence) {
|
||||
if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') {
|
||||
console.error('ButtonSequenceDetector: targetSequence must be an array or string');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.sequence.length < targetSequence.length) return 0;
|
||||
|
||||
// Convert string input to array if needed
|
||||
const sequence =
|
||||
typeof targetSequence === 'string'
|
||||
? targetSequence.toLowerCase().split('')
|
||||
: targetSequence.map((s) => s.toString().toLowerCase());
|
||||
|
||||
//console.log(this.sequence);
|
||||
|
||||
// Get the last n inputs where n is the target sequence length
|
||||
const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input);
|
||||
|
||||
// Compare sequences
|
||||
return lastInputs.every((input, index) => {
|
||||
const target = sequence[index];
|
||||
// Check if either the input matches directly or they refer to the same button in the map
|
||||
return (
|
||||
input === target ||
|
||||
buttonMap[input] === buttonMap[target] ||
|
||||
// Also check if the numerical index matches
|
||||
buttonMap[input] === parseInt(target)
|
||||
);
|
||||
})
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
class GamepadHandler {
|
||||
constructor(index = 0) {
|
||||
// Add index parameter
|
||||
this._gamepads = {};
|
||||
this._activeGamepad = index; // Use provided index
|
||||
this._axes = [0, 0, 0, 0];
|
||||
this._buttons = Array(16).fill(0);
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
window.addEventListener('gamepadconnected', (e) => {
|
||||
this._gamepads[e.gamepad.index] = e.gamepad;
|
||||
if (!this._activeGamepad) {
|
||||
this._activeGamepad = e.gamepad.index;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('gamepaddisconnected', (e) => {
|
||||
delete this._gamepads[e.gamepad.index];
|
||||
if (this._activeGamepad === e.gamepad.index) {
|
||||
this._activeGamepad = Object.keys(this._gamepads)[0] || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
poll() {
|
||||
if (this._activeGamepad !== null) {
|
||||
const gamepad = navigator.getGamepads()[this._activeGamepad];
|
||||
if (gamepad) {
|
||||
// Update axes (normalized to 0-1 range)
|
||||
this._axes = gamepad.axes.map((axis) => (axis + 1) / 2);
|
||||
// Update buttons
|
||||
this._buttons = gamepad.buttons.map((button) => button.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAxes() {
|
||||
return this._axes;
|
||||
}
|
||||
getButtons() {
|
||||
return this._buttons;
|
||||
}
|
||||
}
|
||||
|
||||
// Module-level state store for toggle states
|
||||
const gamepadStates = new Map();
|
||||
|
||||
export const gamepad = (index = 0) => {
|
||||
const handler = new GamepadHandler(index);
|
||||
const sequenceDetector = new ButtonSequenceDetector(2000);
|
||||
|
||||
// Base signal that polls gamepad state and handles sequence detection
|
||||
const baseSignal = signal((t) => {
|
||||
handler.poll();
|
||||
const axes = handler.getAxes();
|
||||
const buttons = handler.getButtons();
|
||||
|
||||
// Add all button inputs to sequence detector
|
||||
buttons.forEach((value, i) => {
|
||||
sequenceDetector.addInput(i, value);
|
||||
});
|
||||
|
||||
return { axes, buttons, t };
|
||||
});
|
||||
|
||||
// Create axes patterns
|
||||
const axes = {
|
||||
x1: baseSignal.fmap((state) => state.axes[0]),
|
||||
y1: baseSignal.fmap((state) => state.axes[1]),
|
||||
x2: baseSignal.fmap((state) => state.axes[2]),
|
||||
y2: baseSignal.fmap((state) => state.axes[3]),
|
||||
};
|
||||
|
||||
// Add bipolar versions
|
||||
axes.x1_2 = axes.x1.toBipolar();
|
||||
axes.y1_2 = axes.y1.toBipolar();
|
||||
axes.x2_2 = axes.x2.toBipolar();
|
||||
axes.y2_2 = axes.y2.toBipolar();
|
||||
|
||||
// Create button patterns
|
||||
const buttons = Array(16)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
// Create unique key for this gamepad+button combination
|
||||
const stateKey = `gamepad${index}_btn${i}`;
|
||||
|
||||
// Initialize toggle state if it doesn't exist
|
||||
if (!gamepadStates.has(stateKey)) {
|
||||
gamepadStates.set(stateKey, {
|
||||
lastButtonState: 0,
|
||||
toggleState: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Direct button value pattern (no longer needs to call addInput)
|
||||
const btn = baseSignal.fmap((state) => state.buttons[i]);
|
||||
|
||||
// Button toggle pattern with persistent state
|
||||
const toggle = baseSignal.fmap((state) => {
|
||||
const currentState = state.buttons[i];
|
||||
const buttonState = gamepadStates.get(stateKey);
|
||||
|
||||
if (currentState === 1 && buttonState.lastButtonState === 0) {
|
||||
// Toggle the state on rising edge
|
||||
buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
buttonState.lastButtonState = currentState;
|
||||
return buttonState.toggleState;
|
||||
});
|
||||
|
||||
return { value: btn, toggle };
|
||||
});
|
||||
|
||||
// Create sequence checker pattern
|
||||
const btnSequence = (sequence) => {
|
||||
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
||||
};
|
||||
const checkSequence = btnSequence;
|
||||
const btnSeq = btnSequence;
|
||||
const btnseq = btnSeq;
|
||||
|
||||
// Return an object with all controls
|
||||
return {
|
||||
...axes,
|
||||
buttons,
|
||||
...Object.fromEntries(
|
||||
Object.entries(buttonMap).flatMap(([key, index]) => [
|
||||
[key.toLowerCase(), buttons[index].value],
|
||||
[key.toUpperCase(), buttons[index].value],
|
||||
[`tgl${key.toLowerCase()}`, buttons[index].toggle],
|
||||
[`tgl${key.toUpperCase()}`, buttons[index].toggle],
|
||||
]),
|
||||
),
|
||||
checkSequence,
|
||||
btnSequence,
|
||||
btnSeq,
|
||||
btnseq,
|
||||
raw: baseSignal,
|
||||
};
|
||||
};
|
||||
|
||||
// Optional: Export for debugging or state management
|
||||
export const getGamepadStates = () => Object.fromEntries(gamepadStates);
|
||||
export const clearGamepadStates = () => gamepadStates.clear();
|
||||
@@ -0,0 +1,3 @@
|
||||
import './gamepad.mjs';
|
||||
|
||||
export * from './gamepad.mjs';
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.0",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Yuta Nakayama <nkymut@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "hs2js",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "Experimental Haskell in JavaScript interpreter",
|
||||
"main": "src/index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
|
||||
```sh
|
||||
npm i @strudel/midi --save
|
||||
```
|
||||
|
||||
## Available Controls
|
||||
|
||||
The following MIDI controls are available:
|
||||
|
||||
OUTPUT:
|
||||
|
||||
- `midi` - opens a midi output device.
|
||||
- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127)
|
||||
- `midichan` - Sets the MIDI channel (1-16, defaults to 1)
|
||||
- `velocity` - Sets note velocity (0-1, defaults to 0.9)
|
||||
- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1)
|
||||
- `control` - Sets MIDI control change messages
|
||||
- `ccn` - Sets MIDI CC controller number (0-127)
|
||||
- `ccv` - Sets MIDI CC value (0-1)
|
||||
- `progNum` - Sends MIDI program change messages (0-127)
|
||||
- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127)
|
||||
- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127)
|
||||
- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127)
|
||||
- `midibend` - Sets MIDI pitch bend (-1 - 1)
|
||||
- `miditouch` - Sets MIDI key after touch (0-1)
|
||||
- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||
- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127)
|
||||
- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127)
|
||||
|
||||
|
||||
INPUT:
|
||||
|
||||
- `midin` - Opens a MIDI input port to receive MIDI control change messages.
|
||||
|
||||
Additional controls can be mapped using the mapping object passed to `.midi()`:
|
||||
|
||||
## Examples
|
||||
|
||||
### midi(outputName?, options?)
|
||||
|
||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||
If no outputName is given, it uses the first midi output it finds.
|
||||
|
||||
```javascript
|
||||
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
|
||||
```
|
||||
|
||||
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
|
||||
|
||||
### Options
|
||||
|
||||
The `.midi()` function accepts an options object with the following properties:
|
||||
|
||||
```javascript
|
||||
$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'})
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Available Options</summary>
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
|
||||
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
|
||||
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
|
||||
| midichannel | number | 1 | Default MIDI channel (1-16) |
|
||||
| velocity | number | 0.9 | Default note velocity (0-1) |
|
||||
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
|
||||
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
|
||||
| midiport | string/number | - | MIDI device name or index |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
### midiport(outputName)
|
||||
|
||||
Selects the MIDI output device to use, pattern can be used to switch between devices.
|
||||
|
||||
```javascript
|
||||
$: midiport('IAC Driver')
|
||||
$: note("c a f e").midiport("<0 1 2 3>").midi()
|
||||
```
|
||||
|
||||
### midichan(number)
|
||||
|
||||
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
|
||||
|
||||
### control, ccn && ccv
|
||||
|
||||
`control` sends MIDI control change messages to your MIDI device.
|
||||
|
||||
- `ccn` sets the cc number. Depends on your synths midi mapping
|
||||
- `ccv` sets the cc value. normalized from 0 to 1.
|
||||
|
||||
```javascript
|
||||
$: note("c a f e").control([74, sine.slow(4)]).midi()
|
||||
$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi()
|
||||
```
|
||||
|
||||
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
|
||||
Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default.
|
||||
But you can also control cc messages separately like this:
|
||||
|
||||
```javascript
|
||||
$: note("c a f e").midi()
|
||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()
|
||||
```
|
||||
|
||||
### progNum (Program Change)
|
||||
|
||||
`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device.
|
||||
Program change values should be numbers between 0 and 127.
|
||||
|
||||
```javascript
|
||||
// Play notes while changing programs
|
||||
note("c3 e3 g3").progNum("<0 1 2>").midi()
|
||||
```
|
||||
|
||||
Program change messages are useful for switching between different instrument sounds or presets during a performance.
|
||||
The exact sound that each program number maps to depends on your MIDI device's configuration.
|
||||
|
||||
## sysex, sysexid && sysexdata (System Exclusive Message)
|
||||
|
||||
`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device.
|
||||
sysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
|
||||
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
|
||||
|
||||
```javascript
|
||||
// Send a simple SysEx message
|
||||
let id = 0x43; //Yamaha
|
||||
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
|
||||
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
|
||||
$: note("c d e f e d c").sysex(id, data).midi();
|
||||
$: note("c d e f e d c").sysexid(id).sysexdata(data).midi();
|
||||
```
|
||||
|
||||
The exact format of SysEx messages depends on your MIDI device's specification.
|
||||
Consult your device's MIDI implementation guide for details on supported SysEx messages.
|
||||
|
||||
### midibend && miditouch
|
||||
|
||||
`midibend` sets MIDI pitch bend (-1 - 1)
|
||||
`miditouch` sets MIDI key after touch (0-1)
|
||||
|
||||
```javascript
|
||||
|
||||
$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi();
|
||||
$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi();
|
||||
|
||||
```
|
||||
|
||||
### midicmd
|
||||
|
||||
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||
|
||||
It supports the following commands:
|
||||
|
||||
- `clock`/`midiClock` - Sends MIDI timing clock messages
|
||||
- `start` - Sends MIDI start message
|
||||
- `stop` - Sends MIDI stop message
|
||||
- `continue` - Sends MIDI continue message
|
||||
|
||||
```javascript
|
||||
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
|
||||
// Note: It might act unexpectedly if MIDI isn't set up initially.
|
||||
stack(
|
||||
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
|
||||
)
|
||||
```
|
||||
|
||||
`midicmd` also supports sending control change, program change and sysex messages.
|
||||
|
||||
- `cc` - sends MIDI control change messages.
|
||||
- `progNum` - sends MIDI program change messages.
|
||||
- `sysex` - sends MIDI system exclusive messages.
|
||||
|
||||
```javascript
|
||||
stack(
|
||||
// "cc:ccn:ccv"
|
||||
midicmd("cc:74:1").midi('IAC Driver'),
|
||||
// "progNum:progNum"
|
||||
midicmd("progNum:1").midi('IAC Driver'),
|
||||
// "sysex:[sysexid]:[sysexdata]"
|
||||
midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver')
|
||||
)
|
||||
```
|
||||
+209
-27
@@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||
import { noteToMidi, getControlName } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
|
||||
@@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) {
|
||||
resolve(WebMidi);
|
||||
return;
|
||||
}
|
||||
WebMidi.enable((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
onReady?.(WebMidi);
|
||||
resolve(WebMidi);
|
||||
});
|
||||
WebMidi.enable(
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
onReady?.(WebMidi);
|
||||
resolve(WebMidi);
|
||||
},
|
||||
{ sysex: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
|
||||
normalized = Math.min(1, Math.max(0, normalized));
|
||||
return Math.pow(normalized, exp);
|
||||
}
|
||||
|
||||
function mapCC(mapping, value) {
|
||||
return Object.keys(value)
|
||||
.filter((key) => !!mapping[getControlName(key)])
|
||||
@@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
|
||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
if (isPattern(output)) {
|
||||
// sends a program change message to the given device on the given channel
|
||||
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
|
||||
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
||||
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
||||
}
|
||||
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
// sends a sysex message to the given device on the given channel
|
||||
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
|
||||
if (Array.isArray(sysexid)) {
|
||||
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
||||
}
|
||||
} else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) {
|
||||
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
||||
}
|
||||
|
||||
if (!Array.isArray(sysexdata)) {
|
||||
throw new Error('expected sysex to be an array of numbers (0-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');
|
||||
}
|
||||
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
// sends a NRPN message to the given device on the given channel
|
||||
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
|
||||
if (Array.isArray(nrpnn)) {
|
||||
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||
throw new Error('all nrpnn bytes must be integers between 0 and 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');
|
||||
}
|
||||
|
||||
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
// sends a pitch bend message to the given device on the given channel
|
||||
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
|
||||
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
||||
throw new Error('expected midibend to be a number between -1 and 1');
|
||||
}
|
||||
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
// sends a channel aftertouch message to the given device on the given channel
|
||||
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
|
||||
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
||||
throw new Error('expected miditouch to be a number between 0 and 1');
|
||||
}
|
||||
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
|
||||
}
|
||||
|
||||
// sends a note message to the given device on the given channel
|
||||
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
|
||||
if (note == null || note === '') {
|
||||
throw new Error('note cannot be null or empty');
|
||||
}
|
||||
if (velocity != null && (typeof velocity !== 'number' || velocity < 0 || velocity > 1)) {
|
||||
throw new Error('velocity must be a number between 0 and 1');
|
||||
}
|
||||
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
|
||||
throw new Error('duration must be a positive number');
|
||||
}
|
||||
|
||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||
device.playNote(midiNote, midichan, {
|
||||
time: timeOffsetString,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* MIDI output: Opens a MIDI output port.
|
||||
* @param {string | number} midiport MIDI device name or index defaulting to 0
|
||||
* @param {object} options Additional MIDI configuration options
|
||||
* @example
|
||||
* note("c4").midichan(1).midi('IAC Driver Bus 1')
|
||||
* @example
|
||||
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
|
||||
*/
|
||||
|
||||
Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
if (isPattern(midiport)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
`.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
if (typeof midiport === 'object') {
|
||||
const { port, isController = false, ...configOptions } = midiport;
|
||||
options = {
|
||||
isController,
|
||||
...configOptions,
|
||||
...options, // Keep any options passed separately
|
||||
};
|
||||
midiport = port;
|
||||
}
|
||||
|
||||
let midiConfig = {
|
||||
// Default configuration values
|
||||
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
|
||||
midichannel: 1, // Default MIDI channel
|
||||
velocity: 0.9, // Default velocity
|
||||
gain: 1, // Default gain
|
||||
midimap: 'default', // Default MIDI map
|
||||
midiport: midiport, // Store the port in the config
|
||||
...options, // Override defaults with provided options
|
||||
};
|
||||
|
||||
enableWebMidi({
|
||||
onEnabled: ({ outputs }) => {
|
||||
const device = getDevice(output, outputs);
|
||||
const device = getDevice(midiConfig.midiport, outputs);
|
||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`Midi enabled! Using "${device.name}". ${
|
||||
@@ -221,26 +335,35 @@ Pattern.prototype.midi = function (output) {
|
||||
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
if (!WebMidi.enabled) {
|
||||
console.log('not enabled');
|
||||
logger('Midi not enabled');
|
||||
return;
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
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}`;
|
||||
|
||||
// destructure value
|
||||
// midi event values from hap with configurable defaults
|
||||
let {
|
||||
note,
|
||||
nrpnn,
|
||||
nrpv,
|
||||
ccn,
|
||||
ccv,
|
||||
midichan = 1,
|
||||
midichan = midiConfig.midichannel,
|
||||
midicmd,
|
||||
gain = 1,
|
||||
velocity = 0.9,
|
||||
midimap = 'default',
|
||||
midiport = output,
|
||||
midibend,
|
||||
miditouch,
|
||||
polyTouch,
|
||||
gain = midiConfig.gain,
|
||||
velocity = midiConfig.velocity,
|
||||
progNum,
|
||||
sysexid,
|
||||
sysexdata,
|
||||
midimap = midiConfig.midimap,
|
||||
midiport = midiConfig.midiport,
|
||||
} = hap.value;
|
||||
|
||||
const device = getDevice(midiport, WebMidi.outputs);
|
||||
@@ -252,24 +375,62 @@ Pattern.prototype.midi = function (output) {
|
||||
}
|
||||
|
||||
velocity = gain * velocity;
|
||||
|
||||
// Handle midimap
|
||||
// if midimap is set, send a cc messages from defined controls
|
||||
if (midicontrolMap.has(midimap)) {
|
||||
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
||||
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
||||
} else if (midimap !== 'default') {
|
||||
// Add warning when a non-existent midimap is specified
|
||||
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
||||
}
|
||||
|
||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
||||
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
|
||||
if (note != null) {
|
||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||
device.playNote(midiNote, midichan, {
|
||||
time: timeOffsetString,
|
||||
});
|
||||
// Handle note
|
||||
if (note !== undefined && !midiConfig.isController) {
|
||||
// note off messages will often a few ms arrive late,
|
||||
// try to prevent glitching by subtracting noteOffsetMs from the duration length
|
||||
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
|
||||
|
||||
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle program change
|
||||
if (progNum !== undefined) {
|
||||
sendProgramChange(progNum, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle sysex
|
||||
// sysex data is consist of 2 arrays, first is sysexid, second is sysexdata
|
||||
// sysexid is a manufacturer id it is either a number or an array of 3 numbers.
|
||||
// list of manufacturer ids can be found here : https://midi.org/sysexidtable
|
||||
// if sysexid is an array the first byte is 0x00
|
||||
|
||||
if (sysexid !== undefined && sysexdata !== undefined) {
|
||||
sendSysex(sysexid, sysexdata, device, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle control change
|
||||
if (ccv !== undefined && ccn !== undefined) {
|
||||
sendCC(ccn, ccv, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle NRPN non-registered parameter number
|
||||
if (nrpnn !== undefined && nrpv !== undefined) {
|
||||
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle midibend
|
||||
if (midibend !== undefined) {
|
||||
sendPitchBend(midibend, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle miditouch
|
||||
if (miditouch !== undefined) {
|
||||
sendAftertouch(miditouch, device, midichan, timeOffsetString);
|
||||
}
|
||||
|
||||
// Handle midicmd
|
||||
if (hap.whole.begin + 0 === 0) {
|
||||
// we need to start here because we have the timing info
|
||||
device.sendStart({ time: timeOffsetString });
|
||||
@@ -282,6 +443,19 @@ Pattern.prototype.midi = function (output) {
|
||||
device.sendStop({ time: timeOffsetString });
|
||||
} else if (['continue'].includes(midicmd)) {
|
||||
device.sendContinue({ time: timeOffsetString });
|
||||
} else if (Array.isArray(midicmd)) {
|
||||
if (midicmd[0] === 'progNum') {
|
||||
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
|
||||
} else if (midicmd[0] === 'cc') {
|
||||
if (midicmd.length === 2) {
|
||||
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
|
||||
}
|
||||
} else if (midicmd[0] === 'sysex') {
|
||||
if (midicmd.length === 3) {
|
||||
const [_, id, data] = midicmd;
|
||||
sendSysex(id, data, device, timeOffsetString);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -289,6 +463,14 @@ Pattern.prototype.midi = function (output) {
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
|
||||
/**
|
||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {Function}
|
||||
* @example
|
||||
* let cc = await midin('IAC Driver Bus 1')
|
||||
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
|
||||
*/
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.1.0",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Yuta Nakayama <nkymut@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.0",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Yuta Nakayama <nkymut@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -37,7 +37,7 @@ function connect() {
|
||||
export function parseControlsFromHap(hap, cps) {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const delta = hap.duration.valueOf() / cps;
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/reference",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Headless reference of all strudel functions",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -31,8 +31,6 @@
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -11,9 +10,6 @@ export default defineConfig({
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cowsay from 'cowsay';
|
||||
import { createReadStream } from 'fs';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import http from 'http';
|
||||
import { join, sep } from 'path';
|
||||
@@ -70,12 +70,15 @@ const server = http.createServer(async (req, res) => {
|
||||
return res.end(JSON.stringify(banks));
|
||||
}
|
||||
let subpath = decodeURIComponent(req.url);
|
||||
if (!files.includes(subpath)) {
|
||||
const filePath = join(directory, subpath.split('/').join(sep));
|
||||
|
||||
//console.log('GET:', filePath);
|
||||
const isFound = existsSync(filePath);
|
||||
if (!isFound) {
|
||||
res.statusCode = 404;
|
||||
res.end('File not found');
|
||||
return;
|
||||
}
|
||||
const filePath = join(directory, subpath.split('/').join(sep));
|
||||
const readStream = createReadStream(filePath);
|
||||
readStream.on('error', (err) => {
|
||||
res.statusCode = 500;
|
||||
@@ -99,12 +102,6 @@ Object.keys(networkInterfaces).forEach((key) => {
|
||||
});
|
||||
});
|
||||
|
||||
if (!IP) {
|
||||
console.error("Unable to determine server's IP address.");
|
||||
// eslint-disable-next-line
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
server.listen(PORT, IP_ADDRESS, () => {
|
||||
console.log(`@strudel/sampler is now serving audio files from:
|
||||
${directory}
|
||||
@@ -113,6 +110,6 @@ To use them in the Strudel REPL, run:
|
||||
samples('http://localhost:${PORT}')
|
||||
|
||||
Or on a machine in the same network:
|
||||
samples('http://${IP}:${PORT}')
|
||||
${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
} from '@strudel/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound';
|
||||
let soundfontUrl = defaultSoundfontUrl;
|
||||
|
||||
export function setSoundfontUrl(value) {
|
||||
soundfontUrl = value;
|
||||
}
|
||||
|
||||
let loadCache = {};
|
||||
async function loadFont(name) {
|
||||
if (loadCache[name]) {
|
||||
@@ -16,7 +23,7 @@ async function loadFont(name) {
|
||||
}
|
||||
const load = async () => {
|
||||
// TODO: make soundfont source configurable
|
||||
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
||||
const url = `${soundfontUrl}/${name}.js`;
|
||||
const preset = await fetch(url).then((res) => res.text());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
|
||||
import { getFontBufferSource, registerSoundfonts, setSoundfontUrl } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts, setSoundfontUrl };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||
constantNode.onended = () => {
|
||||
onComplete();
|
||||
};
|
||||
return constantNode;
|
||||
}
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -344,7 +344,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
};
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (endTime, playWholeBuffer) => {};
|
||||
const stop = (endTime) => {
|
||||
bufferSource.stop(endTime);
|
||||
};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
|
||||
// cut groups
|
||||
|
||||
@@ -14,10 +14,29 @@ import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||
|
||||
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
||||
|
||||
export function setMaxPolyphony(polyphony) {
|
||||
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
|
||||
}
|
||||
export const soundMap = map();
|
||||
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
soundMap.setKey(key.toLowerCase(), { onTrigger, data });
|
||||
key = key.toLowerCase().replace(/\s+/g, '_');
|
||||
soundMap.setKey(key, { onTrigger, data });
|
||||
}
|
||||
|
||||
let gainCurveFunc = (val) => val;
|
||||
|
||||
export function applyGainCurve(val) {
|
||||
return gainCurveFunc(val);
|
||||
}
|
||||
|
||||
export function setGainCurve(newGainCurveFunc) {
|
||||
gainCurveFunc = newGainCurveFunc;
|
||||
}
|
||||
|
||||
function aliasBankMap(aliasMap) {
|
||||
@@ -85,6 +104,18 @@ export function getSound(s) {
|
||||
return soundMap.get()[s.toLowerCase()];
|
||||
}
|
||||
|
||||
export const getAudioDevices = async () => {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
|
||||
const devicesMap = new Map();
|
||||
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
|
||||
mediaDevices.forEach((device) => {
|
||||
devicesMap.set(device.label, device.deviceId);
|
||||
});
|
||||
return devicesMap;
|
||||
};
|
||||
|
||||
const defaultDefaultValues = {
|
||||
s: 'triangle',
|
||||
gain: 0.8,
|
||||
@@ -155,18 +186,40 @@ export function getAudioContextCurrentTime() {
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
|
||||
const audioCtx = getAudioContext();
|
||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
||||
}
|
||||
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio(options = {}) {
|
||||
const { disableWorklets = false } = options;
|
||||
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
|
||||
setMaxPolyphony(maxPolyphony);
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
await getAudioContext().resume();
|
||||
|
||||
const audioCtx = getAudioContext();
|
||||
|
||||
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
||||
try {
|
||||
const devices = await getAudioDevices();
|
||||
const id = devices.get(audioDeviceName);
|
||||
const isValidID = (id ?? '').length > 0;
|
||||
if (audioCtx.sinkId !== id && isValidID) {
|
||||
await audioCtx.setSinkId(id);
|
||||
}
|
||||
logger(
|
||||
`[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`,
|
||||
);
|
||||
} catch {
|
||||
logger('[superdough] failed to set audio interface', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
await audioCtx.resume();
|
||||
if (disableWorklets) {
|
||||
logger('[superdough]: AudioWorklets disabled with disableWorklets');
|
||||
return;
|
||||
@@ -374,6 +427,8 @@ export function resetGlobalEffects() {
|
||||
analysersData = {};
|
||||
}
|
||||
|
||||
let activeSoundSources = new Map();
|
||||
|
||||
export const superdough = async (value, t, hapDuration) => {
|
||||
const ac = getAudioContext();
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
@@ -471,16 +526,32 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
compressorRelease,
|
||||
} = value;
|
||||
|
||||
gain = nanFallback(gain, 1);
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
postgain = applyGainCurve(postgain);
|
||||
shapevol = applyGainCurve(shapevol);
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
|
||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
|
||||
const ch = activeSoundSources.entries().next();
|
||||
const source = ch.value[1];
|
||||
const chainID = ch.value[0];
|
||||
const endTime = t + 0.25;
|
||||
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
|
||||
source?.stop?.(endTime);
|
||||
activeSoundSources.delete(chainID);
|
||||
}
|
||||
|
||||
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
|
||||
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
toDisconnect.forEach((n) => n?.disconnect());
|
||||
};
|
||||
let audioNodes = [];
|
||||
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
@@ -492,10 +563,15 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
sourceNode = source(t, value, hapDuration);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, value, onended);
|
||||
const onEnded = () => {
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
const soundHandle = await onTrigger(t, value, onEnded);
|
||||
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
soundHandle.stop(t + hapDuration);
|
||||
activeSoundSources.set(chainID, soundHandle);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
@@ -625,6 +701,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
audioNodes.push(delaySend);
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
@@ -642,6 +719,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
}
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
audioNodes.push(reverbSend);
|
||||
}
|
||||
|
||||
// analyser
|
||||
@@ -649,14 +727,12 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
if (analyze) {
|
||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||
analyserSend = effectSend(post, analyserNode, 1);
|
||||
audioNodes.push(analyserSend);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// toDisconnect = all the node that should be disconnected in onended callback
|
||||
// this is crucial for performance
|
||||
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
|
||||
audioNodes = audioNodes.concat(chain);
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||
|
||||
@@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => {
|
||||
|
||||
return Number(freq);
|
||||
};
|
||||
function destroyAudioWorkletNode(node) {
|
||||
node.disconnect();
|
||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||
}
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||
const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
@@ -63,7 +67,9 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (releaseTime) => {},
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
@@ -110,10 +116,12 @@ export function registerSynthSounds() {
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
webAudioTimeout(
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
o.disconnect();
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
onended();
|
||||
fm?.stop();
|
||||
@@ -123,11 +131,72 @@ export function registerSynthSounds() {
|
||||
end,
|
||||
);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
);
|
||||
|
||||
registerSound(
|
||||
'pulse',
|
||||
(begin, value, onended) => {
|
||||
const ac = getAudioContext();
|
||||
let { duration, n: pulsewidth = 0.5 } = value;
|
||||
const frequency = getFrequencyFromValue(value);
|
||||
|
||||
const [attack, decay, sustain, release] = getADSRValues(
|
||||
[value.attack, value.decay, value.sustain, value.release],
|
||||
'linear',
|
||||
[0.001, 0.05, 0.6, 0.01],
|
||||
);
|
||||
|
||||
const holdend = begin + duration;
|
||||
const end = holdend + release + 0.01;
|
||||
|
||||
let o = getWorklet(
|
||||
ac,
|
||||
'pulse-oscillator',
|
||||
{
|
||||
frequency,
|
||||
begin,
|
||||
end,
|
||||
pulsewidth,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {},
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
@@ -170,7 +239,9 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (releaseTime) => {},
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
|
||||
@@ -75,7 +75,12 @@ const waveshapes = {
|
||||
return v - polyBlep(phase, dt);
|
||||
},
|
||||
};
|
||||
|
||||
function getParamValue(block, param) {
|
||||
if (param.length > 1) {
|
||||
return param[block];
|
||||
}
|
||||
return param[0];
|
||||
}
|
||||
const waveShapeNames = Object.keys(waveshapes);
|
||||
class LFOProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
@@ -362,6 +367,11 @@ function getUnisonDetune(unison, detune, voiceIndex) {
|
||||
}
|
||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||
}
|
||||
|
||||
function applySemitoneDetuneToFrequency(frequency, detune) {
|
||||
return frequency * Math.pow(2, detune / 12);
|
||||
}
|
||||
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -438,7 +448,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
const isOdd = (n & 1) == 1;
|
||||
|
||||
//applies unison "spread" detune in semitones
|
||||
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
|
||||
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
@@ -648,3 +658,106 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
}
|
||||
|
||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
||||
|
||||
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
|
||||
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.pi = _PI;
|
||||
this.phi = -this.pi; // phase
|
||||
this.Y0 = 0; // feedback memories
|
||||
this.Y1 = 0;
|
||||
this.PW = this.pi; // pulse width
|
||||
this.B = 2.3; // feedback coefficient
|
||||
this.dphif = 0; // filtered phase increment
|
||||
this.envf = 0; // filtered envelope
|
||||
}
|
||||
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
min: Number.EPSILON,
|
||||
},
|
||||
{
|
||||
name: 'detune',
|
||||
defaultValue: 0,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
{
|
||||
name: 'pulsewidth',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
let env = 1,
|
||||
dphi;
|
||||
|
||||
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
||||
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi;
|
||||
const detune = getParamValue(i, params.detune);
|
||||
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||
|
||||
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
|
||||
this.dphif += 0.1 * (dphi - this.dphif);
|
||||
|
||||
env *= 0.9998; // exponential decay envelope
|
||||
this.envf += 0.1 * (env - this.envf);
|
||||
|
||||
// Feedback coefficient control
|
||||
this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation
|
||||
if (this.B < 0) this.B = 0;
|
||||
|
||||
// Waveform generation (half-Tomisawa oscillators)
|
||||
this.phi += this.dphif; // phase increment
|
||||
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping
|
||||
|
||||
// First half-Tomisawa generator
|
||||
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
|
||||
this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter
|
||||
|
||||
// Second half-Tomisawa generator (with phase offset for pulse width)
|
||||
let out1 = Math.cos(this.phi + this.B * this.Y1 + pw);
|
||||
this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter
|
||||
|
||||
for (let o = 0; o < output.length; o++) {
|
||||
// Combination of both oscillators with envelope applied
|
||||
output[o][i] = 0.15 * (out0 - out1) * this.envf;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // keep the audio processing going
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@strudel/tidal",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "Experimental Tidal Code interpreter",
|
||||
"module": "tidal.mjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/felixroos/hs2js.git"
|
||||
"url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal"
|
||||
},
|
||||
"keywords": [
|
||||
"haskell",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
Generated
+108
@@ -39,6 +39,9 @@ importers:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.2.7
|
||||
version: 2.2.7
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 3.0.4
|
||||
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(vitest@3.0.4)
|
||||
@@ -267,6 +270,16 @@ importers:
|
||||
|
||||
packages/embed: {}
|
||||
|
||||
packages/gamepad:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/hs2js:
|
||||
dependencies:
|
||||
web-tree-sitter:
|
||||
@@ -640,6 +653,9 @@ importers:
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/draw
|
||||
'@strudel/gamepad':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/gamepad
|
||||
'@strudel/hydra':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/hydra
|
||||
@@ -1412,6 +1428,10 @@ packages:
|
||||
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@codemirror/autocomplete@6.18.4':
|
||||
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
|
||||
|
||||
@@ -1834,6 +1854,10 @@ packages:
|
||||
'@isaacs/string-locale-compare@1.1.0':
|
||||
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
|
||||
|
||||
'@istanbuljs/schema@0.1.3':
|
||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -2818,6 +2842,15 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
||||
|
||||
'@vitest/coverage-v8@3.0.4':
|
||||
resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 3.0.4
|
||||
vitest: 3.0.4
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@3.0.4':
|
||||
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
|
||||
|
||||
@@ -4487,6 +4520,9 @@ packages:
|
||||
hs2js@0.1.0:
|
||||
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
html-escaper@3.0.3:
|
||||
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||
|
||||
@@ -4837,6 +4873,22 @@ packages:
|
||||
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-reports@3.1.7:
|
||||
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
@@ -6887,6 +6939,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
text-encoding-shim@1.0.5:
|
||||
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
|
||||
|
||||
@@ -7505,6 +7561,7 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
|
||||
workbox-navigation-preload@7.0.0:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@@ -8568,6 +8625,8 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@codemirror/autocomplete@6.18.4':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.8
|
||||
@@ -8934,6 +8993,8 @@ snapshots:
|
||||
|
||||
'@isaacs/string-locale-compare@1.1.0': {}
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
@@ -10187,6 +10248,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
debug: 4.4.0
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6
|
||||
istanbul-reports: 3.1.7
|
||||
magic-string: 0.30.17
|
||||
magicast: 0.3.5
|
||||
std-env: 3.8.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@3.0.4':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.4
|
||||
@@ -12202,6 +12281,8 @@ snapshots:
|
||||
dependencies:
|
||||
web-tree-sitter: 0.20.8
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-escaper@3.0.3: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
@@ -12540,6 +12621,27 @@ snapshots:
|
||||
|
||||
isobject@3.0.1: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
dependencies:
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
make-dir: 4.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
debug: 4.4.0
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-reports@3.1.7:
|
||||
dependencies:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -15262,6 +15364,12 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.4.5
|
||||
minimatch: 9.0.5
|
||||
|
||||
text-encoding-shim@1.0.5: {}
|
||||
|
||||
text-extensions@1.9.0: {}
|
||||
|
||||
@@ -933,6 +933,39 @@ exports[`runs examples > example "as" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "as" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 1/4 → 3/8 | begin:0.25 s:sax_vib clip:1 ]",
|
||||
"[ 3/8 → 1/2 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 1/2 → 5/8 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 5/8 → 3/4 | begin:0.3 s:sax_vib clip:1 ]",
|
||||
"[ 3/4 → 7/8 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ (7/8 → 1/1) ⇝ 9/8 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 7/8 ⇜ (1/1 → 9/8) | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 9/8 → 5/4 | begin:0.25 s:sax_vib clip:1 ]",
|
||||
"[ 5/4 → 11/8 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 11/8 → 3/2 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 3/2 → 13/8 | begin:0.3 s:sax_vib clip:1 ]",
|
||||
"[ 13/8 → 7/4 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 7/4 → 2/1 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 2/1 → 17/8 | begin:0.25 s:sax_vib clip:1 ]",
|
||||
"[ 17/8 → 9/4 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 9/4 → 19/8 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 19/8 → 5/2 | begin:0.3 s:sax_vib clip:1 ]",
|
||||
"[ 5/2 → 21/8 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 21/8 → 23/8 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 23/8 → 3/1 | begin:0.25 s:sax_vib clip:1 ]",
|
||||
"[ 3/1 → 25/8 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 25/8 → 13/4 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 13/4 → 27/8 | begin:0.3 s:sax_vib clip:1 ]",
|
||||
"[ 27/8 → 7/2 | begin:0.5 s:sax_vib clip:1 ]",
|
||||
"[ 7/2 → 15/4 | begin:0 s:sax_vib clip:1 ]",
|
||||
"[ 15/4 → 31/8 | begin:0.25 s:sax_vib clip:1 ]",
|
||||
"[ 31/8 → 4/1 | begin:0 s:sax_vib clip:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "attack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c3 attack:0 ]",
|
||||
@@ -2289,6 +2322,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "degradeBy" example index 2 1`] = `
|
||||
[
|
||||
"[ 1/8 → 3/16 | s:bd ]",
|
||||
"[ 1/4 → 5/16 | s:bd ]",
|
||||
"[ 5/16 → 3/8 | s:bd ]",
|
||||
"[ 1/2 → 9/16 | s:bd ]",
|
||||
"[ 9/16 → 5/8 | s:bd ]",
|
||||
"[ 11/16 → 3/4 | s:bd ]",
|
||||
"[ 15/16 → 1/1 | s:bd ]",
|
||||
"[ 9/8 → 19/16 | s:bd ]",
|
||||
"[ 5/4 → 21/16 | s:bd ]",
|
||||
"[ 21/16 → 11/8 | s:bd ]",
|
||||
"[ 3/2 → 25/16 | s:bd ]",
|
||||
"[ 25/16 → 13/8 | s:bd ]",
|
||||
"[ 27/16 → 7/4 | s:bd ]",
|
||||
"[ 31/16 → 2/1 | s:bd ]",
|
||||
"[ 17/8 → 35/16 | s:bd ]",
|
||||
"[ 9/4 → 37/16 | s:bd ]",
|
||||
"[ 37/16 → 19/8 | s:bd ]",
|
||||
"[ 5/2 → 41/16 | s:bd ]",
|
||||
"[ 41/16 → 21/8 | s:bd ]",
|
||||
"[ 43/16 → 11/4 | s:bd ]",
|
||||
"[ 47/16 → 3/1 | s:bd ]",
|
||||
"[ 25/8 → 51/16 | s:bd ]",
|
||||
"[ 13/4 → 53/16 | s:bd ]",
|
||||
"[ 53/16 → 27/8 | s:bd ]",
|
||||
"[ 7/2 → 57/16 | s:bd ]",
|
||||
"[ 57/16 → 29/8 | s:bd ]",
|
||||
"[ 59/16 → 15/4 | s:bd ]",
|
||||
"[ 63/16 → 4/1 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delay" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0 ]",
|
||||
@@ -5206,6 +5272,292 @@ exports[`runs examples > example "mask" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midi" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midi" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midibend" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 midibend:0 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 midibend:0.4 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 midibend:1.1102230246251565e-16 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 midibend:-0.4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midichan" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midicmd" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/48 | midicmd:clock ]",
|
||||
"[ 0/1 → 2/1 | midicmd:start ]",
|
||||
"[ 1/48 → 1/24 | midicmd:clock ]",
|
||||
"[ 1/24 → 1/16 | midicmd:clock ]",
|
||||
"[ 1/16 → 1/12 | midicmd:clock ]",
|
||||
"[ 1/12 → 5/48 | midicmd:clock ]",
|
||||
"[ 5/48 → 1/8 | midicmd:clock ]",
|
||||
"[ 1/8 → 7/48 | midicmd:clock ]",
|
||||
"[ 7/48 → 1/6 | midicmd:clock ]",
|
||||
"[ 1/6 → 3/16 | midicmd:clock ]",
|
||||
"[ 3/16 → 5/24 | midicmd:clock ]",
|
||||
"[ 5/24 → 11/48 | midicmd:clock ]",
|
||||
"[ 11/48 → 1/4 | midicmd:clock ]",
|
||||
"[ 1/4 → 13/48 | midicmd:clock ]",
|
||||
"[ 13/48 → 7/24 | midicmd:clock ]",
|
||||
"[ 7/24 → 5/16 | midicmd:clock ]",
|
||||
"[ 5/16 → 1/3 | midicmd:clock ]",
|
||||
"[ 1/3 → 17/48 | midicmd:clock ]",
|
||||
"[ 17/48 → 3/8 | midicmd:clock ]",
|
||||
"[ 3/8 → 19/48 | midicmd:clock ]",
|
||||
"[ 19/48 → 5/12 | midicmd:clock ]",
|
||||
"[ 5/12 → 7/16 | midicmd:clock ]",
|
||||
"[ 7/16 → 11/24 | midicmd:clock ]",
|
||||
"[ 11/24 → 23/48 | midicmd:clock ]",
|
||||
"[ 23/48 → 1/2 | midicmd:clock ]",
|
||||
"[ 1/2 → 25/48 | midicmd:clock ]",
|
||||
"[ 25/48 → 13/24 | midicmd:clock ]",
|
||||
"[ 13/24 → 9/16 | midicmd:clock ]",
|
||||
"[ 9/16 → 7/12 | midicmd:clock ]",
|
||||
"[ 7/12 → 29/48 | midicmd:clock ]",
|
||||
"[ 29/48 → 5/8 | midicmd:clock ]",
|
||||
"[ 5/8 → 31/48 | midicmd:clock ]",
|
||||
"[ 31/48 → 2/3 | midicmd:clock ]",
|
||||
"[ 2/3 → 11/16 | midicmd:clock ]",
|
||||
"[ 11/16 → 17/24 | midicmd:clock ]",
|
||||
"[ 17/24 → 35/48 | midicmd:clock ]",
|
||||
"[ 35/48 → 3/4 | midicmd:clock ]",
|
||||
"[ 3/4 → 37/48 | midicmd:clock ]",
|
||||
"[ 37/48 → 19/24 | midicmd:clock ]",
|
||||
"[ 19/24 → 13/16 | midicmd:clock ]",
|
||||
"[ 13/16 → 5/6 | midicmd:clock ]",
|
||||
"[ 5/6 → 41/48 | midicmd:clock ]",
|
||||
"[ 41/48 → 7/8 | midicmd:clock ]",
|
||||
"[ 7/8 → 43/48 | midicmd:clock ]",
|
||||
"[ 43/48 → 11/12 | midicmd:clock ]",
|
||||
"[ 11/12 → 15/16 | midicmd:clock ]",
|
||||
"[ 15/16 → 23/24 | midicmd:clock ]",
|
||||
"[ 23/24 → 47/48 | midicmd:clock ]",
|
||||
"[ 47/48 → 1/1 | midicmd:clock ]",
|
||||
"[ 1/1 → 49/48 | midicmd:clock ]",
|
||||
"[ 49/48 → 25/24 | midicmd:clock ]",
|
||||
"[ 25/24 → 17/16 | midicmd:clock ]",
|
||||
"[ 17/16 → 13/12 | midicmd:clock ]",
|
||||
"[ 13/12 → 53/48 | midicmd:clock ]",
|
||||
"[ 53/48 → 9/8 | midicmd:clock ]",
|
||||
"[ 9/8 → 55/48 | midicmd:clock ]",
|
||||
"[ 55/48 → 7/6 | midicmd:clock ]",
|
||||
"[ 7/6 → 19/16 | midicmd:clock ]",
|
||||
"[ 19/16 → 29/24 | midicmd:clock ]",
|
||||
"[ 29/24 → 59/48 | midicmd:clock ]",
|
||||
"[ 59/48 → 5/4 | midicmd:clock ]",
|
||||
"[ 5/4 → 61/48 | midicmd:clock ]",
|
||||
"[ 61/48 → 31/24 | midicmd:clock ]",
|
||||
"[ 31/24 → 21/16 | midicmd:clock ]",
|
||||
"[ 21/16 → 4/3 | midicmd:clock ]",
|
||||
"[ 4/3 → 65/48 | midicmd:clock ]",
|
||||
"[ 65/48 → 11/8 | midicmd:clock ]",
|
||||
"[ 11/8 → 67/48 | midicmd:clock ]",
|
||||
"[ 67/48 → 17/12 | midicmd:clock ]",
|
||||
"[ 17/12 → 23/16 | midicmd:clock ]",
|
||||
"[ 23/16 → 35/24 | midicmd:clock ]",
|
||||
"[ 35/24 → 71/48 | midicmd:clock ]",
|
||||
"[ 71/48 → 3/2 | midicmd:clock ]",
|
||||
"[ 3/2 → 73/48 | midicmd:clock ]",
|
||||
"[ 73/48 → 37/24 | midicmd:clock ]",
|
||||
"[ 37/24 → 25/16 | midicmd:clock ]",
|
||||
"[ 25/16 → 19/12 | midicmd:clock ]",
|
||||
"[ 19/12 → 77/48 | midicmd:clock ]",
|
||||
"[ 77/48 → 13/8 | midicmd:clock ]",
|
||||
"[ 13/8 → 79/48 | midicmd:clock ]",
|
||||
"[ 79/48 → 5/3 | midicmd:clock ]",
|
||||
"[ 5/3 → 27/16 | midicmd:clock ]",
|
||||
"[ 27/16 → 41/24 | midicmd:clock ]",
|
||||
"[ 41/24 → 83/48 | midicmd:clock ]",
|
||||
"[ 83/48 → 7/4 | midicmd:clock ]",
|
||||
"[ 7/4 → 85/48 | midicmd:clock ]",
|
||||
"[ 85/48 → 43/24 | midicmd:clock ]",
|
||||
"[ 43/24 → 29/16 | midicmd:clock ]",
|
||||
"[ 29/16 → 11/6 | midicmd:clock ]",
|
||||
"[ 11/6 → 89/48 | midicmd:clock ]",
|
||||
"[ 89/48 → 15/8 | midicmd:clock ]",
|
||||
"[ 15/8 → 91/48 | midicmd:clock ]",
|
||||
"[ 91/48 → 23/12 | midicmd:clock ]",
|
||||
"[ 23/12 → 31/16 | midicmd:clock ]",
|
||||
"[ 31/16 → 47/24 | midicmd:clock ]",
|
||||
"[ 47/24 → 95/48 | midicmd:clock ]",
|
||||
"[ 95/48 → 2/1 | midicmd:clock ]",
|
||||
"[ 2/1 → 97/48 | midicmd:clock ]",
|
||||
"[ 2/1 → 4/1 | midicmd:stop ]",
|
||||
"[ 97/48 → 49/24 | midicmd:clock ]",
|
||||
"[ 49/24 → 33/16 | midicmd:clock ]",
|
||||
"[ 33/16 → 25/12 | midicmd:clock ]",
|
||||
"[ 25/12 → 101/48 | midicmd:clock ]",
|
||||
"[ 101/48 → 17/8 | midicmd:clock ]",
|
||||
"[ 17/8 → 103/48 | midicmd:clock ]",
|
||||
"[ 103/48 → 13/6 | midicmd:clock ]",
|
||||
"[ 13/6 → 35/16 | midicmd:clock ]",
|
||||
"[ 35/16 → 53/24 | midicmd:clock ]",
|
||||
"[ 53/24 → 107/48 | midicmd:clock ]",
|
||||
"[ 107/48 → 9/4 | midicmd:clock ]",
|
||||
"[ 9/4 → 109/48 | midicmd:clock ]",
|
||||
"[ 109/48 → 55/24 | midicmd:clock ]",
|
||||
"[ 55/24 → 37/16 | midicmd:clock ]",
|
||||
"[ 37/16 → 7/3 | midicmd:clock ]",
|
||||
"[ 7/3 → 113/48 | midicmd:clock ]",
|
||||
"[ 113/48 → 19/8 | midicmd:clock ]",
|
||||
"[ 19/8 → 115/48 | midicmd:clock ]",
|
||||
"[ 115/48 → 29/12 | midicmd:clock ]",
|
||||
"[ 29/12 → 39/16 | midicmd:clock ]",
|
||||
"[ 39/16 → 59/24 | midicmd:clock ]",
|
||||
"[ 59/24 → 119/48 | midicmd:clock ]",
|
||||
"[ 119/48 → 5/2 | midicmd:clock ]",
|
||||
"[ 5/2 → 121/48 | midicmd:clock ]",
|
||||
"[ 121/48 → 61/24 | midicmd:clock ]",
|
||||
"[ 61/24 → 41/16 | midicmd:clock ]",
|
||||
"[ 41/16 → 31/12 | midicmd:clock ]",
|
||||
"[ 31/12 → 125/48 | midicmd:clock ]",
|
||||
"[ 125/48 → 21/8 | midicmd:clock ]",
|
||||
"[ 21/8 → 127/48 | midicmd:clock ]",
|
||||
"[ 127/48 → 8/3 | midicmd:clock ]",
|
||||
"[ 8/3 → 43/16 | midicmd:clock ]",
|
||||
"[ 43/16 → 65/24 | midicmd:clock ]",
|
||||
"[ 65/24 → 131/48 | midicmd:clock ]",
|
||||
"[ 131/48 → 11/4 | midicmd:clock ]",
|
||||
"[ 11/4 → 133/48 | midicmd:clock ]",
|
||||
"[ 133/48 → 67/24 | midicmd:clock ]",
|
||||
"[ 67/24 → 45/16 | midicmd:clock ]",
|
||||
"[ 45/16 → 17/6 | midicmd:clock ]",
|
||||
"[ 17/6 → 137/48 | midicmd:clock ]",
|
||||
"[ 137/48 → 23/8 | midicmd:clock ]",
|
||||
"[ 23/8 → 139/48 | midicmd:clock ]",
|
||||
"[ 139/48 → 35/12 | midicmd:clock ]",
|
||||
"[ 35/12 → 47/16 | midicmd:clock ]",
|
||||
"[ 47/16 → 71/24 | midicmd:clock ]",
|
||||
"[ 71/24 → 143/48 | midicmd:clock ]",
|
||||
"[ 143/48 → 3/1 | midicmd:clock ]",
|
||||
"[ 3/1 → 145/48 | midicmd:clock ]",
|
||||
"[ 145/48 → 73/24 | midicmd:clock ]",
|
||||
"[ 73/24 → 49/16 | midicmd:clock ]",
|
||||
"[ 49/16 → 37/12 | midicmd:clock ]",
|
||||
"[ 37/12 → 149/48 | midicmd:clock ]",
|
||||
"[ 149/48 → 25/8 | midicmd:clock ]",
|
||||
"[ 25/8 → 151/48 | midicmd:clock ]",
|
||||
"[ 151/48 → 19/6 | midicmd:clock ]",
|
||||
"[ 19/6 → 51/16 | midicmd:clock ]",
|
||||
"[ 51/16 → 77/24 | midicmd:clock ]",
|
||||
"[ 77/24 → 155/48 | midicmd:clock ]",
|
||||
"[ 155/48 → 13/4 | midicmd:clock ]",
|
||||
"[ 13/4 → 157/48 | midicmd:clock ]",
|
||||
"[ 157/48 → 79/24 | midicmd:clock ]",
|
||||
"[ 79/24 → 53/16 | midicmd:clock ]",
|
||||
"[ 53/16 → 10/3 | midicmd:clock ]",
|
||||
"[ 10/3 → 161/48 | midicmd:clock ]",
|
||||
"[ 161/48 → 27/8 | midicmd:clock ]",
|
||||
"[ 27/8 → 163/48 | midicmd:clock ]",
|
||||
"[ 163/48 → 41/12 | midicmd:clock ]",
|
||||
"[ 41/12 → 55/16 | midicmd:clock ]",
|
||||
"[ 55/16 → 83/24 | midicmd:clock ]",
|
||||
"[ 83/24 → 167/48 | midicmd:clock ]",
|
||||
"[ 167/48 → 7/2 | midicmd:clock ]",
|
||||
"[ 7/2 → 169/48 | midicmd:clock ]",
|
||||
"[ 169/48 → 85/24 | midicmd:clock ]",
|
||||
"[ 85/24 → 57/16 | midicmd:clock ]",
|
||||
"[ 57/16 → 43/12 | midicmd:clock ]",
|
||||
"[ 43/12 → 173/48 | midicmd:clock ]",
|
||||
"[ 173/48 → 29/8 | midicmd:clock ]",
|
||||
"[ 29/8 → 175/48 | midicmd:clock ]",
|
||||
"[ 175/48 → 11/3 | midicmd:clock ]",
|
||||
"[ 11/3 → 59/16 | midicmd:clock ]",
|
||||
"[ 59/16 → 89/24 | midicmd:clock ]",
|
||||
"[ 89/24 → 179/48 | midicmd:clock ]",
|
||||
"[ 179/48 → 15/4 | midicmd:clock ]",
|
||||
"[ 15/4 → 181/48 | midicmd:clock ]",
|
||||
"[ 181/48 → 91/24 | midicmd:clock ]",
|
||||
"[ 91/24 → 61/16 | midicmd:clock ]",
|
||||
"[ 61/16 → 23/6 | midicmd:clock ]",
|
||||
"[ 23/6 → 185/48 | midicmd:clock ]",
|
||||
"[ 185/48 → 31/8 | midicmd:clock ]",
|
||||
"[ 31/8 → 187/48 | midicmd:clock ]",
|
||||
"[ 187/48 → 47/12 | midicmd:clock ]",
|
||||
"[ 47/12 → 63/16 | midicmd:clock ]",
|
||||
"[ 63/16 → 95/24 | midicmd:clock ]",
|
||||
"[ 95/24 → 191/48 | midicmd:clock ]",
|
||||
"[ 191/48 → 4/1 | midicmd:clock ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 1/4 → 1/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 1/2 → 3/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 3/4 → 1/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 1/1 → 5/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 5/4 → 3/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 3/2 → 7/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 7/4 → 2/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 2/1 → 9/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 9/4 → 5/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 5/2 → 11/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 11/4 → 3/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 3/1 → 13/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 13/4 → 7/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 7/2 → 15/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||
"[ 15/4 → 4/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midiport" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c midiport:0 ]",
|
||||
"[ 1/4 → 1/2 | note:a midiport:0 ]",
|
||||
"[ 1/2 → 3/4 | note:f midiport:0 ]",
|
||||
"[ 3/4 → 1/1 | note:e midiport:0 ]",
|
||||
"[ 1/1 → 5/4 | note:c midiport:1 ]",
|
||||
"[ 5/4 → 3/2 | note:a midiport:1 ]",
|
||||
"[ 3/2 → 7/4 | note:f midiport:1 ]",
|
||||
"[ 7/4 → 2/1 | note:e midiport:1 ]",
|
||||
"[ 2/1 → 9/4 | note:c midiport:2 ]",
|
||||
"[ 9/4 → 5/2 | note:a midiport:2 ]",
|
||||
"[ 5/2 → 11/4 | note:f midiport:2 ]",
|
||||
"[ 11/4 → 3/1 | note:e midiport:2 ]",
|
||||
"[ 3/1 → 13/4 | note:c midiport:3 ]",
|
||||
"[ 13/4 → 7/2 | note:a midiport:3 ]",
|
||||
"[ 7/2 → 15/4 | note:f midiport:3 ]",
|
||||
"[ 15/4 → 4/1 | note:e midiport:3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "miditouch" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 miditouch:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 miditouch:0.5000000000000001 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 miditouch:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "mousex" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 ]",
|
||||
@@ -5426,6 +5778,24 @@ exports[`runs examples > example "note" example index 2 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "nrpv" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "octave" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
|
||||
@@ -6178,67 +6548,54 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:c ]",
|
||||
"[ 0/1 → 1/3 | note:c2 ]",
|
||||
"[ 1/3 → 2/3 | note:eb ]",
|
||||
"[ 1/3 → 2/3 | note:g2 ]",
|
||||
"[ 2/3 → 1/1 | note:g ]",
|
||||
"[ 2/3 → 1/1 | note:c2 ]",
|
||||
"[ 1/1 → 4/3 | note:c ]",
|
||||
"[ 1/1 → 4/3 | note:g2 ]",
|
||||
"[ 4/3 → 5/3 | note:eb ]",
|
||||
"[ 4/3 → 5/3 | note:c2 ]",
|
||||
"[ 5/3 → 2/1 | note:g ]",
|
||||
"[ 5/3 → 2/1 | note:g2 ]",
|
||||
"[ 2/1 → 7/3 | note:c ]",
|
||||
"[ 2/1 → 7/3 | note:c2 ]",
|
||||
"[ 7/3 → 8/3 | note:eb ]",
|
||||
"[ 7/3 → 8/3 | note:g2 ]",
|
||||
"[ 8/3 → 3/1 | note:g ]",
|
||||
"[ 8/3 → 3/1 | note:c2 ]",
|
||||
"[ 3/1 → 10/3 | note:c ]",
|
||||
"[ 3/1 → 10/3 | note:g2 ]",
|
||||
"[ 10/3 → 11/3 | note:eb ]",
|
||||
"[ 10/3 → 11/3 | note:c2 ]",
|
||||
"[ 11/3 → 4/1 | note:g ]",
|
||||
"[ 11/3 → 4/1 | note:g2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
"[ 0/1 → 1/4 | note:e ]",
|
||||
"[ 1/4 → 1/2 | note:d ]",
|
||||
"[ 1/4 → 1/2 | note:f ]",
|
||||
"[ 1/2 → 3/4 | note:c ]",
|
||||
"[ 1/2 → 3/4 | note:g ]",
|
||||
"[ 3/4 → 1/1 | note:d ]",
|
||||
"[ 3/4 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 5/4 | note:c ]",
|
||||
"[ 1/1 → 5/4 | note:f ]",
|
||||
"[ 5/4 → 3/2 | note:d ]",
|
||||
"[ 5/4 → 3/2 | note:g ]",
|
||||
"[ 3/2 → 7/4 | note:c ]",
|
||||
"[ 3/2 → 7/4 | note:e ]",
|
||||
"[ 7/4 → 2/1 | note:d ]",
|
||||
"[ 7/4 → 2/1 | note:f ]",
|
||||
"[ 2/1 → 9/4 | note:c ]",
|
||||
"[ 2/1 → 9/4 | note:g ]",
|
||||
"[ 9/4 → 5/2 | note:d ]",
|
||||
"[ 9/4 → 5/2 | note:e ]",
|
||||
"[ 5/2 → 11/4 | note:c ]",
|
||||
"[ 5/2 → 11/4 | note:f ]",
|
||||
"[ 11/4 → 3/1 | note:d ]",
|
||||
"[ 11/4 → 3/1 | note:g ]",
|
||||
"[ 3/1 → 13/4 | note:c ]",
|
||||
"[ 3/1 → 13/4 | note:e ]",
|
||||
"[ 13/4 → 7/2 | note:d ]",
|
||||
"[ 13/4 → 7/2 | note:f ]",
|
||||
"[ 7/2 → 15/4 | note:c ]",
|
||||
"[ 7/2 → 15/4 | note:g ]",
|
||||
"[ 15/4 → 4/1 | note:d ]",
|
||||
"[ 15/4 → 4/1 | note:e ]",
|
||||
"[ 0/1 → 1/6 | note:c ]",
|
||||
"[ 0/1 → 1/6 | note:c2 ]",
|
||||
"[ 1/6 → 1/3 | note:eb ]",
|
||||
"[ 1/6 → 1/3 | note:g2 ]",
|
||||
"[ 1/3 → 1/2 | note:g ]",
|
||||
"[ 1/3 → 1/2 | note:c2 ]",
|
||||
"[ 1/2 → 2/3 | note:c ]",
|
||||
"[ 1/2 → 2/3 | note:g2 ]",
|
||||
"[ 2/3 → 5/6 | note:eb ]",
|
||||
"[ 2/3 → 5/6 | note:c2 ]",
|
||||
"[ 5/6 → 1/1 | note:g ]",
|
||||
"[ 5/6 → 1/1 | note:g2 ]",
|
||||
"[ 1/1 → 7/6 | note:c ]",
|
||||
"[ 1/1 → 7/6 | note:c2 ]",
|
||||
"[ 7/6 → 4/3 | note:eb ]",
|
||||
"[ 7/6 → 4/3 | note:g2 ]",
|
||||
"[ 4/3 → 3/2 | note:g ]",
|
||||
"[ 4/3 → 3/2 | note:c2 ]",
|
||||
"[ 3/2 → 5/3 | note:c ]",
|
||||
"[ 3/2 → 5/3 | note:g2 ]",
|
||||
"[ 5/3 → 11/6 | note:eb ]",
|
||||
"[ 5/3 → 11/6 | note:c2 ]",
|
||||
"[ 11/6 → 2/1 | note:g ]",
|
||||
"[ 11/6 → 2/1 | note:g2 ]",
|
||||
"[ 2/1 → 13/6 | note:c ]",
|
||||
"[ 2/1 → 13/6 | note:c2 ]",
|
||||
"[ 13/6 → 7/3 | note:eb ]",
|
||||
"[ 13/6 → 7/3 | note:g2 ]",
|
||||
"[ 7/3 → 5/2 | note:g ]",
|
||||
"[ 7/3 → 5/2 | note:c2 ]",
|
||||
"[ 5/2 → 8/3 | note:c ]",
|
||||
"[ 5/2 → 8/3 | note:g2 ]",
|
||||
"[ 8/3 → 17/6 | note:eb ]",
|
||||
"[ 8/3 → 17/6 | note:c2 ]",
|
||||
"[ 17/6 → 3/1 | note:g ]",
|
||||
"[ 17/6 → 3/1 | note:g2 ]",
|
||||
"[ 3/1 → 19/6 | note:c ]",
|
||||
"[ 3/1 → 19/6 | note:c2 ]",
|
||||
"[ 19/6 → 10/3 | note:eb ]",
|
||||
"[ 19/6 → 10/3 | note:g2 ]",
|
||||
"[ 10/3 → 7/2 | note:g ]",
|
||||
"[ 10/3 → 7/2 | note:c2 ]",
|
||||
"[ 7/2 → 11/3 | note:c ]",
|
||||
"[ 7/2 → 11/3 | note:g2 ]",
|
||||
"[ 11/3 → 23/6 | note:eb ]",
|
||||
"[ 11/3 → 23/6 | note:c2 ]",
|
||||
"[ 23/6 → 4/1 | note:g ]",
|
||||
"[ 23/6 → 4/1 | note:g2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6346,6 +6703,15 @@ exports[`runs examples > example "pressBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "progNum" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 progNum:10 midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 progNum:10 midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 progNum:10 midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 progNum:10 midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pure" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | e4 ]",
|
||||
@@ -6790,35 +7156,76 @@ exports[`runs examples > example "rev" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "ribbon" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:d ]",
|
||||
"[ 1/2 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 3/2 | note:d ]",
|
||||
"[ 3/2 → 2/1 | note:e ]",
|
||||
"[ 2/1 → 5/2 | note:d ]",
|
||||
"[ 5/2 → 3/1 | note:e ]",
|
||||
"[ 3/1 → 7/2 | note:d ]",
|
||||
"[ 7/2 → 4/1 | note:e ]",
|
||||
"[ 0/1 → 1/1 | note:d ]",
|
||||
"[ 1/1 → 2/1 | note:e ]",
|
||||
"[ 2/1 → 3/1 | note:d ]",
|
||||
"[ 3/1 → 4/1 | note:e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ribbon" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:G3 ]",
|
||||
"[ 0/1 → 1/4 | note:A3 ]",
|
||||
"[ 1/4 → 1/2 | note:C3 ]",
|
||||
"[ 1/2 → 3/4 | note:D3 ]",
|
||||
"[ 3/4 → 1/1 | note:F3 ]",
|
||||
"[ 1/1 → 5/4 | note:Eb3 ]",
|
||||
"[ 5/4 → 3/2 | note:Ab3 ]",
|
||||
"[ 3/2 → 7/4 | note:G3 ]",
|
||||
"[ 7/4 → 2/1 | note:C4 ]",
|
||||
"[ 2/1 → 9/4 | note:G3 ]",
|
||||
"[ 3/4 → 1/1 | note:G3 ]",
|
||||
"[ 1/1 → 5/4 | note:E3 ]",
|
||||
"[ 5/4 → 3/2 | note:C4 ]",
|
||||
"[ 3/2 → 7/4 | note:A3 ]",
|
||||
"[ 7/4 → 2/1 | note:E4 ]",
|
||||
"[ 2/1 → 9/4 | note:A3 ]",
|
||||
"[ 9/4 → 5/2 | note:C3 ]",
|
||||
"[ 5/2 → 11/4 | note:D3 ]",
|
||||
"[ 11/4 → 3/1 | note:F3 ]",
|
||||
"[ 3/1 → 13/4 | note:Eb3 ]",
|
||||
"[ 13/4 → 7/2 | note:Ab3 ]",
|
||||
"[ 7/2 → 15/4 | note:G3 ]",
|
||||
"[ 15/4 → 4/1 | note:C4 ]",
|
||||
"[ 11/4 → 3/1 | note:G3 ]",
|
||||
"[ 3/1 → 13/4 | note:E3 ]",
|
||||
"[ 13/4 → 7/2 | note:C4 ]",
|
||||
"[ 7/2 → 15/4 | note:A3 ]",
|
||||
"[ 15/4 → 4/1 | note:E4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ribbon" example index 2 1`] = `
|
||||
[
|
||||
"[ 1/16 → 1/8 | s:bd ]",
|
||||
"[ 1/8 → 3/16 | s:bd ]",
|
||||
"[ 3/16 → 1/4 | s:bd ]",
|
||||
"[ 1/4 → 5/16 | s:bd ]",
|
||||
"[ 3/8 → 7/16 | s:bd ]",
|
||||
"[ 9/16 → 5/8 | s:bd ]",
|
||||
"[ 5/8 → 11/16 | s:bd ]",
|
||||
"[ 11/16 → 3/4 | s:bd ]",
|
||||
"[ 3/4 → 13/16 | s:bd ]",
|
||||
"[ 7/8 → 15/16 | s:bd ]",
|
||||
"[ 17/16 → 9/8 | s:bd ]",
|
||||
"[ 9/8 → 19/16 | s:bd ]",
|
||||
"[ 19/16 → 5/4 | s:bd ]",
|
||||
"[ 5/4 → 21/16 | s:bd ]",
|
||||
"[ 11/8 → 23/16 | s:bd ]",
|
||||
"[ 25/16 → 13/8 | s:bd ]",
|
||||
"[ 13/8 → 27/16 | s:bd ]",
|
||||
"[ 27/16 → 7/4 | s:bd ]",
|
||||
"[ 7/4 → 29/16 | s:bd ]",
|
||||
"[ 15/8 → 31/16 | s:bd ]",
|
||||
"[ 33/16 → 17/8 | s:bd ]",
|
||||
"[ 17/8 → 35/16 | s:bd ]",
|
||||
"[ 35/16 → 9/4 | s:bd ]",
|
||||
"[ 9/4 → 37/16 | s:bd ]",
|
||||
"[ 19/8 → 39/16 | s:bd ]",
|
||||
"[ 41/16 → 21/8 | s:bd ]",
|
||||
"[ 21/8 → 43/16 | s:bd ]",
|
||||
"[ 43/16 → 11/4 | s:bd ]",
|
||||
"[ 11/4 → 45/16 | s:bd ]",
|
||||
"[ 23/8 → 47/16 | s:bd ]",
|
||||
"[ 49/16 → 25/8 | s:bd ]",
|
||||
"[ 25/8 → 51/16 | s:bd ]",
|
||||
"[ 51/16 → 13/4 | s:bd ]",
|
||||
"[ 13/4 → 53/16 | s:bd ]",
|
||||
"[ 27/8 → 55/16 | s:bd ]",
|
||||
"[ 57/16 → 29/8 | s:bd ]",
|
||||
"[ 29/8 → 59/16 | s:bd ]",
|
||||
"[ 59/16 → 15/4 | s:bd ]",
|
||||
"[ 15/4 → 61/16 | s:bd ]",
|
||||
"[ 31/8 → 63/16 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -7464,6 +7871,52 @@ but parts might be played more than once, or not at all, per cycle." example ind
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scrub" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
|
||||
"[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
|
||||
"[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
|
||||
"[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
|
||||
"[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
|
||||
"[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
|
||||
"[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
|
||||
"[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
|
||||
"[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
|
||||
"[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scrub" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
|
||||
"[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
|
||||
"[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
|
||||
"[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
|
||||
"[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "segment" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/24 | note:40 ]",
|
||||
@@ -8970,6 +9423,33 @@ exports[`runs examples > example "swingBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "sysex" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "sysexdata" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "sysexid" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "take" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd ]",
|
||||
@@ -9687,6 +10167,59 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "wchooseCycles" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/32 | s:bd ]",
|
||||
"[ 3/32 → 1/8 | s:bd ]",
|
||||
"[ 3/16 → 7/32 | s:bd ]",
|
||||
"[ 1/4 → 1/3 | s:hh ]",
|
||||
"[ 1/3 → 5/12 | s:hh ]",
|
||||
"[ 5/12 → 1/2 | s:hh ]",
|
||||
"[ 1/2 → 7/12 | s:hh ]",
|
||||
"[ 7/12 → 2/3 | s:hh ]",
|
||||
"[ 2/3 → 3/4 | s:hh ]",
|
||||
"[ 3/4 → 5/6 | s:hh ]",
|
||||
"[ 5/6 → 11/12 | s:hh ]",
|
||||
"[ 11/12 → 1/1 | s:hh ]",
|
||||
"[ 1/1 → 33/32 | s:bd ]",
|
||||
"[ 35/32 → 9/8 | s:bd ]",
|
||||
"[ 19/16 → 39/32 | s:bd ]",
|
||||
"[ 5/4 → 4/3 | s:hh ]",
|
||||
"[ 4/3 → 17/12 | s:hh ]",
|
||||
"[ 17/12 → 3/2 | s:hh ]",
|
||||
"[ 3/2 → 49/32 | s:bd ]",
|
||||
"[ 51/32 → 13/8 | s:bd ]",
|
||||
"[ 27/16 → 55/32 | s:bd ]",
|
||||
"[ 7/4 → 11/6 | s:hh ]",
|
||||
"[ 11/6 → 23/12 | s:hh ]",
|
||||
"[ 23/12 → 2/1 | s:hh ]",
|
||||
"[ 2/1 → 25/12 | s:hh ]",
|
||||
"[ 25/12 → 13/6 | s:hh ]",
|
||||
"[ 13/6 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 7/3 | s:hh ]",
|
||||
"[ 7/3 → 29/12 | s:hh ]",
|
||||
"[ 29/12 → 5/2 | s:hh ]",
|
||||
"[ 5/2 → 81/32 | s:bd ]",
|
||||
"[ 83/32 → 21/8 | s:bd ]",
|
||||
"[ 43/16 → 87/32 | s:bd ]",
|
||||
"[ 11/4 → 17/6 | s:hh ]",
|
||||
"[ 17/6 → 35/12 | s:hh ]",
|
||||
"[ 35/12 → 3/1 | s:hh ]",
|
||||
"[ 3/1 → 97/32 | s:bd ]",
|
||||
"[ 99/32 → 25/8 | s:bd ]",
|
||||
"[ 51/16 → 103/32 | s:bd ]",
|
||||
"[ 13/4 → 10/3 | s:hh ]",
|
||||
"[ 10/3 → 41/12 | s:hh ]",
|
||||
"[ 41/12 → 7/2 | s:hh ]",
|
||||
"[ 7/2 → 43/12 | s:hh ]",
|
||||
"[ 43/12 → 11/3 | s:hh ]",
|
||||
"[ 11/3 → 15/4 | s:hh ]",
|
||||
"[ 15/4 → 23/6 | s:hh ]",
|
||||
"[ 23/6 → 47/12 | s:hh ]",
|
||||
"[ 47/12 → 4/1 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "when" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:c3 ]",
|
||||
|
||||
+13
-1
@@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio';
|
||||
import { mini, m } from '@strudel/mini/mini.mjs';
|
||||
// import * as voicingHelpers from '@strudel/tonal/voicings.mjs';
|
||||
// import euclid from '@strudel/core/euclid.mjs';
|
||||
// import '@strudel/midi/midi.mjs';
|
||||
//import '@strudel/midi/midi.mjs';
|
||||
import * as tonalHelpers from '@strudel/tonal';
|
||||
import '@strudel/xen/xen.mjs';
|
||||
// import '@strudel/xen/tune.mjs';
|
||||
@@ -21,6 +21,9 @@ import '@strudel/xen/xen.mjs';
|
||||
// import '@strudel/webaudio/webaudio.mjs';
|
||||
// import '@strudel/serial/serial.mjs';
|
||||
import '../website/src/repl/piano';
|
||||
//import * as motionHelpers from '../packages/motion/index.mjs';
|
||||
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
|
||||
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
|
||||
|
||||
class MockedNode {
|
||||
chain() {
|
||||
@@ -123,6 +126,12 @@ const loadCsound = () => {};
|
||||
const loadCSound = () => {};
|
||||
const loadcsound = () => {};
|
||||
|
||||
const midin = () => {
|
||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||
};
|
||||
|
||||
const sysex = ([id, data]) => {};
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
evalScope(
|
||||
// Tone,
|
||||
@@ -131,6 +140,7 @@ evalScope(
|
||||
uiHelpersMocked,
|
||||
webaudio,
|
||||
tonalHelpers,
|
||||
gamepadHelpers,
|
||||
/*
|
||||
toneHelpers,
|
||||
voicingHelpers,
|
||||
@@ -138,6 +148,8 @@ evalScope(
|
||||
uiHelpers,
|
||||
*/
|
||||
{
|
||||
midin,
|
||||
sysex,
|
||||
// gist,
|
||||
// euclid,
|
||||
csound: id,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@strudel/csound": "workspace:*",
|
||||
"@strudel/desktopbridge": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/gamepad": "workspace:*",
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
|
||||
100% free for personal and commercial use.
|
||||
However it's limited on basic latin only,
|
||||
contact riedjal@gmail.com for full glyph (based on ANSI encoding)
|
||||
and OTF features (alternates).
|
||||
|
||||
src: https://www.dafont.com/cute-aurora.font?text=%24%3A+s%28%22bd%285%2C8%29%22%29.superimpose%28x+%3D%3E+x.note%28%22c2%22%29.midi%28device%29%29
|
||||
@@ -36,8 +36,6 @@ export function Showcase() {
|
||||
}
|
||||
|
||||
let _videos = [
|
||||
{ title: 'Coding Music With Strudel Workhop by Dan Gorelick and Viola He', id: 'oqyAJ4WeKoU' },
|
||||
{ title: 'Hexe - playing w strudel live coding music', id: '03m3F5xVOMg' },
|
||||
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
|
||||
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
|
||||
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
|
||||
@@ -58,7 +56,6 @@ let _videos = [
|
||||
},
|
||||
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
|
||||
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
|
||||
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
|
||||
//
|
||||
/* { // not sure if this is copyrighted ...
|
||||
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
|
||||
@@ -126,6 +123,11 @@ let _videos = [
|
||||
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
|
||||
},
|
||||
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
|
||||
{ title: 'todepasta 1.5', id: 'gCwaVu1Mijg' },
|
||||
{ title: 'Djenerative Music by Bogdan Vera @ TOPLAP solstice Dec 2024', id: 'LtMX4Lr1nzY' },
|
||||
{ title: 'La musique by BuboBubo @ TOPLAP solstice Dec 2024', id: 'Oz00Y_f80wU' },
|
||||
{ title: 'Livecode and vocal breaks by Switch Angel @ TOPLAP solstice Dec 2024', id: '2kzjOIsL6CM' },
|
||||
{ title: 'Eddyflux algorave set @ rudolf5', id: 'MXz8131Ut0A' },
|
||||
];
|
||||
|
||||
_shuffled = shuffleArray(_videos);
|
||||
|
||||
@@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'
|
||||
// }
|
||||
|
||||
export default function UdelsEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { context, ...editorProps } = Props;
|
||||
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
||||
|
||||
return (
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<div className={'h-full flex w-full flex-col relative'} {...editorProps}>
|
||||
<Loader active={pending} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function UdelsHeader(Props) {
|
||||
const { numWindows, setNumWindows } = Props;
|
||||
|
||||
return (
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
|
||||
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
||||
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||
|
||||
@@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = {
|
||||
{ text: 'Music metadata', link: 'learn/metadata' },
|
||||
{ text: 'CSound', link: 'learn/csound' },
|
||||
{ text: 'Hydra', link: 'learn/hydra' },
|
||||
{ text: 'Input Devices', link: 'learn/input-devices' },
|
||||
{ text: 'Device Motion', link: 'learn/devicemotion' },
|
||||
],
|
||||
'Pattern Functions': [
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
|
||||
export function getMetadata(raw_code) {
|
||||
if (raw_code == null) {
|
||||
console.error('could not extract metadata from missing pattern code');
|
||||
raw_code = '';
|
||||
}
|
||||
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
|
||||
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
|
||||
const tags = {};
|
||||
|
||||
@@ -5,6 +5,6 @@ layout: ../../layouts/MainLayout.astro
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx';
|
||||
import DeviceMotion from '@strudel/motion/docs/devicemotion.mdx';
|
||||
|
||||
<DeviceMotion />
|
||||
|
||||
@@ -293,8 +293,6 @@ global effects use the same chain for all events of the same orbit:
|
||||
|
||||
<JsDoc client:idle name="iresponse" h={0} />
|
||||
|
||||
Next, we'll look at strudel's support for [Csound](/learn/csound).
|
||||
|
||||
## Phaser
|
||||
|
||||
### phaser
|
||||
@@ -312,3 +310,5 @@ Next, we'll look at strudel's support for [Csound](/learn/csound).
|
||||
### phasersweep
|
||||
|
||||
<JsDoc client:idle name="phasersweep" h={0} />
|
||||
|
||||
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Input Devices
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
import Gamepad from '@strudel/gamepad/docs/gamepad.mdx';
|
||||
|
||||
# Input Devices
|
||||
|
||||
Strudel supports various input devices like Gamepads and MIDI controllers to manipulate patterns in real-time.
|
||||
|
||||
<Gamepad />
|
||||
@@ -16,24 +16,97 @@ It is also possible to pattern other things with Strudel, such as software and h
|
||||
|
||||
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
||||
|
||||
## midi(outputName?)
|
||||
## midiin(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midin" h={0} />
|
||||
|
||||
## midi(outputName?,options?)
|
||||
|
||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||
If no outputName is given, it uses the first midi output it finds.
|
||||
|
||||
<MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().midi()`} />
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
|
||||
`}
|
||||
/>
|
||||
|
||||
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
|
||||
In the console, you will see a log of the available MIDI devices as soon as you run the code,
|
||||
e.g.
|
||||
|
||||
```
|
||||
`Midi connected! Using "Midi Through Port-0".`
|
||||
```
|
||||
|
||||
The `.midi()` function accepts an options object with the following properties:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`$: note("d e c a f").midi('IAC Driver', { isController: true, midimap: 'default'})
|
||||
`}
|
||||
/>
|
||||
|
||||
<details>
|
||||
<summary>Available Options</summary>
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------ | ------------- | --------- | ---------------------------------------------------------------------- |
|
||||
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
|
||||
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
|
||||
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
|
||||
| midichannel | number | 1 | Default MIDI channel (1-16) |
|
||||
| velocity | number | 0.9 | Default note velocity (0-1) |
|
||||
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
|
||||
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
|
||||
| midiport | string/number | - | MIDI device name or index |
|
||||
|
||||
</details>
|
||||
|
||||
### midiport(outputName)
|
||||
|
||||
Selects the MIDI output device to use, pattern can be used to switch between devices.
|
||||
|
||||
```javascript
|
||||
$: midiport('IAC Driver');
|
||||
$: note('c a f e').midiport('<0 1 2 3>').midi();
|
||||
```
|
||||
|
||||
<JsDoc client:idle name="midiport" h={0} />
|
||||
|
||||
## midichan(number)
|
||||
|
||||
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
|
||||
|
||||
## ccn && ccv
|
||||
## midicmd(command)
|
||||
|
||||
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||
|
||||
It supports the following commands:
|
||||
|
||||
- `clock`/`midiClock` - Sends MIDI timing clock messages
|
||||
- `start` - Sends MIDI start message
|
||||
- `stop` - Sends MIDI stop message
|
||||
- `continue` - Sends MIDI continue message
|
||||
|
||||
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
|
||||
// Note: It might act unexpectedly if MIDI isn't set up initially.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`$:stack(
|
||||
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
|
||||
)`}
|
||||
/>
|
||||
|
||||
## control, ccn && ccv
|
||||
|
||||
- `control` sends MIDI control change messages to your MIDI device.
|
||||
- `ccn` sets the cc number. Depends on your synths midi mapping
|
||||
- `ccv` sets the cc value. normalized from 0 to 1.
|
||||
|
||||
<MiniRepl client:idle tune={`note("c a f e").control([74, sine.slow(4)]).midi()`} />
|
||||
|
||||
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} />
|
||||
|
||||
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
|
||||
@@ -56,6 +129,48 @@ Instead of setting `ccn` and `ccv` directly, you can also create mappings with `
|
||||
|
||||
<JsDoc client:idle name="defaultmidimap" h={0} />
|
||||
|
||||
## progNum (Program Change)
|
||||
|
||||
`progNum` sends MIDI program change messages to switch between different presets/patches on your MIDI device.
|
||||
Program change values should be numbers between 0 and 127.
|
||||
|
||||
<MiniRepl client:idle tune={`// Switch between programs 0 and 1 every cycle
|
||||
progNum("<0 1>").midi()
|
||||
|
||||
// Play notes while changing programs
|
||||
note("c3 e3 g3").progNum("<0 1 2>").midi()`} />
|
||||
|
||||
Program change messages are useful for switching between different instrument sounds or presets during a performance.
|
||||
The exact sound that each program number maps to depends on your MIDI device's configuration.
|
||||
|
||||
## sysex, sysexid && sysexdata (System Exclusive Message)
|
||||
|
||||
`sysex` sends MIDI System Exclusive (SysEx) messages to your MIDI device.
|
||||
ysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
|
||||
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`// Send a simple SysEx message
|
||||
let id = 0x43; //Yamaha
|
||||
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
|
||||
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
|
||||
$: note("c a f e").sysex(id, data).midi();
|
||||
$: note("c a f e").sysexid(id).sysexdata(data).midi();`}
|
||||
/>
|
||||
|
||||
The exact format of SysEx messages depends on your MIDI device's specification.
|
||||
Consult your device's MIDI implementation guide for details on supported SysEx messages.
|
||||
|
||||
## midibend && miditouch
|
||||
|
||||
`midibend` sets MIDI pitch bend (-1 - 1)
|
||||
`miditouch` sets MIDI key after touch (0-1)
|
||||
|
||||
<MiniRepl client:idle tune={`note("c a f e").midibend(sine.slow(4).range(-0.4,0.4)).midi()`} />
|
||||
|
||||
<MiniRepl client:idle tune={`note("c a f e").miditouch(sine.slow(4).range(0,1)).midi()`} />
|
||||
|
||||
# OSC/SuperDirt/StrudelDirt
|
||||
|
||||
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
||||
@@ -118,8 +233,8 @@ The following example shows how to send a pattern to an MQTT broker:
|
||||
client:only="react"
|
||||
tune={`"hello world"
|
||||
.mqtt(undefined, // username (undefined for open/public servers)
|
||||
undefined, // password
|
||||
'/strudel-pattern', // mqtt 'topic'
|
||||
undefined, // password
|
||||
'/strudel-pattern', // mqtt 'topic'
|
||||
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
|
||||
'mystrudel', // MQTT client id - randomly generated if not supplied
|
||||
0 // latency / delay before sending messages (0 = no delay)
|
||||
@@ -130,12 +245,14 @@ The following example shows how to send a pattern to an MQTT broker:
|
||||
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
|
||||
|
||||
```
|
||||
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
|
||||
hello
|
||||
world
|
||||
hello
|
||||
world
|
||||
...
|
||||
|
||||
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
|
||||
> hello
|
||||
> world
|
||||
> hello
|
||||
> world
|
||||
> ...
|
||||
|
||||
```
|
||||
|
||||
Control patterns will be encoded as JSON, for example:
|
||||
@@ -155,11 +272,17 @@ Control patterns will be encoded as JSON, for example:
|
||||
Will send messages like the following:
|
||||
|
||||
```
|
||||
|
||||
{"s":"sax","speed":2}
|
||||
{"s":"sax","speed":2}
|
||||
{"s":"sax","speed":3}
|
||||
{"s":"sax","speed":2}
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
Libraries for receiving MQTT are available for many programming languages.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first
|
||||
|
||||
# Sound Banks
|
||||
|
||||
If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
|
||||
If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
|
||||
|
||||
We _could_ use them like this:
|
||||
|
||||
@@ -180,11 +180,12 @@ It assumes a `strudel.json` file to be present at the root of the repository:
|
||||
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
|
||||
```
|
||||
|
||||
## From Disk via "Import Sounds"
|
||||
## From Disk via "Import Sounds Folder"
|
||||
|
||||
If you don't want to upload your samples to the internet, you can also load them from your local disk.
|
||||
Go to the `sounds` tab in the REPL and press "import sounds".
|
||||
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
|
||||
Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar.
|
||||
Press the "import sounds folder" button and select a folder that contains audio files.
|
||||
The folder you select can also contain subfolders with audio files.
|
||||
Example:
|
||||
|
||||
```
|
||||
@@ -200,12 +201,13 @@ Example:
|
||||
```
|
||||
|
||||
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
|
||||
If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
|
||||
If you select that `samples` folder, the `user` tab (next to the `import-sounds` tab) will then contain 2 new sounds: `swoop(3) smash(3)`
|
||||
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
|
||||
The samples within each sound use zero-based indexing in alphabetical order.
|
||||
|
||||
## From Disk via @strudel/sampler
|
||||
|
||||
Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
|
||||
Instead of loading your samples into your browser with the "import sounds folder" button, you can also serve the samples from a local file server.
|
||||
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
|
||||
|
||||
```sh
|
||||
|
||||
@@ -24,7 +24,7 @@ With the new stepwise `stepcat` function, the steps of the two patterns will be
|
||||
|
||||
By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step.
|
||||
|
||||
However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each.
|
||||
However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattern are twice as long, and so counted as two steps each.
|
||||
|
||||
## Pacing the steps
|
||||
|
||||
@@ -116,10 +116,6 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
|
||||
|
||||
<JsDoc client:idle name="polymeter" h={0} />
|
||||
|
||||
### polymeterSteps
|
||||
|
||||
<JsDoc client:idle name="polymeterSteps" h={0} />
|
||||
|
||||
### shrink
|
||||
|
||||
<JsDoc client:idle name="shrink" h={0} />
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
import HeadCommon from '@components/HeadCommon.astro';
|
||||
import { Udels } from '../../components/Udels/Udels.jsx';
|
||||
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
---
|
||||
|
||||
|
||||
<body class="m-0">
|
||||
<Udels client:only="react" />
|
||||
</body>
|
||||
<html lang="en" class="m-0">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<title>Strudel UDELS</title>
|
||||
</head>
|
||||
<body class="m-0">
|
||||
<Udels client:only="react" />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,10 +9,12 @@ import UdelsEditor from '@components/Udels/UdelsEditor';
|
||||
import ReplEditor from './components/ReplEditor';
|
||||
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
||||
import { useReplContext } from './useReplContext';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || isIframe();
|
||||
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
||||
const context = useReplContext();
|
||||
return <Editor context={context} />;
|
||||
const { fontFamily } = useSettings();
|
||||
return <Editor context={context} style={{ fontFamily }} />;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { Header } from './Header';
|
||||
// }
|
||||
|
||||
export default function EmbeddedReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { context, ...editorProps } = Props;
|
||||
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} embedded={true} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
|
||||
@@ -11,7 +11,7 @@ export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -22,6 +22,7 @@ export function Header({ context, embedded = false }) {
|
||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
||||
isEmbedded ? 'flex' : 'md:flex',
|
||||
)}
|
||||
style={{ fontFamily }}
|
||||
>
|
||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||
<h1
|
||||
@@ -46,7 +47,7 @@ export function Header({ context, embedded = false }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block rotate-90">꩜</span>
|
||||
<span className="block text-foreground rotate-90">꩜</span>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="space-x-2">
|
||||
|
||||
@@ -10,13 +10,13 @@ import { useSettings } from '@src/settings.mjs';
|
||||
// }
|
||||
|
||||
export default function ReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { context, ...editorProps } = Props;
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition, isZen } = settings;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
|
||||
@@ -4,5 +4,9 @@ export default function UserFacingErrorMessage(Props) {
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
return <div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>;
|
||||
return (
|
||||
<div className="text-background px-2 py-1 bg-foreground w-full ml-auto">
|
||||
Error: {error.message || 'Unknown Error :-/'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
function IncButton({ children, className, ...buttonProps }) {
|
||||
return (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className={cx(
|
||||
'border border-transparent p-1 text-center hover:text-background text-sm transition-all hover:bg-foreground active:bg-lineBackground disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
|
||||
className,
|
||||
)}
|
||||
type="button"
|
||||
{...buttonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
export function Incrementor({
|
||||
onChange,
|
||||
value,
|
||||
min = -Infinity,
|
||||
max = Infinity,
|
||||
className,
|
||||
incrementLabel = 'next page',
|
||||
decrementLabel = 'prev page',
|
||||
...incrementorProps
|
||||
}) {
|
||||
value = parseInt(value);
|
||||
value = isNaN(value) ? '' : value;
|
||||
return (
|
||||
<div className={cx('w-fit bg-background relative flex items-center"> rounded-md', className)}>
|
||||
<Textbox
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(v) => {
|
||||
if (v.length && v < min) {
|
||||
return;
|
||||
}
|
||||
onChange(v);
|
||||
}}
|
||||
type="number"
|
||||
placeholder=""
|
||||
value={value}
|
||||
className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
{...incrementorProps}
|
||||
/>
|
||||
<div className="flex gap-1 ">
|
||||
<IncButton disabled={value <= min} onClick={() => onChange(value - 1)} aria-label={decrementLabel}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</IncButton>
|
||||
<IncButton
|
||||
className="rounded-r-md"
|
||||
disabled={value >= max}
|
||||
onClick={() => onChange(value + 1)}
|
||||
aria-label={incrementLabel}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
|
||||
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
|
||||
</svg>
|
||||
</IncButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Incrementor } from '../incrementor/Incrementor';
|
||||
|
||||
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
|
||||
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
|
||||
|
||||
import { SelectInput } from './SelectInput';
|
||||
import { getAudioDevices } from '@strudel/webaudio';
|
||||
|
||||
const initdevices = new Map();
|
||||
|
||||
@@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
|
||||
if (!devicesInitialized) {
|
||||
return;
|
||||
}
|
||||
const deviceID = devices.get(deviceName);
|
||||
onChange(deviceName);
|
||||
setAudioDevice(deviceID);
|
||||
};
|
||||
const options = new Map();
|
||||
Array.from(devices.keys()).forEach((deviceName) => {
|
||||
|
||||
@@ -1,53 +1,32 @@
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $strudel_log_history } from '../useLogger';
|
||||
|
||||
export function ConsoleTab() {
|
||||
const [log, setLog] = useState([]);
|
||||
const { fontFamily, fontSize } = useSettings();
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
const log = useStore($strudel_log_history);
|
||||
const { fontFamily } = useSettings();
|
||||
return (
|
||||
<div
|
||||
id="console-tab"
|
||||
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
|
||||
style={{ fontFamily, fontSize }}
|
||||
>
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
|
||||
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(
|
||||
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
|
||||
l.type === 'highlight' && 'underline',
|
||||
)}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +51,3 @@ function linkify(inputText) {
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
@@ -20,25 +20,42 @@ export default function ImportSoundsButton({ onComplete }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
style={{ alignItems: 'center' }}
|
||||
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<input
|
||||
disabled={isUploading}
|
||||
ref={fileUploadRef}
|
||||
id="audio_file"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
directory=""
|
||||
webkitdirectory=""
|
||||
multiple
|
||||
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
|
||||
onChange={() => {
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
{isUploading ? 'importing...' : 'import sounds'}
|
||||
</label>
|
||||
<div>
|
||||
<label
|
||||
style={{ alignItems: 'center', borderColor: 'red', border: 1 }}
|
||||
className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6 mr-2"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m0-3-3-3m0 0-3 3m3-3v11.25m6-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<input
|
||||
disabled={isUploading}
|
||||
ref={fileUploadRef}
|
||||
id="audio_file"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
directory=""
|
||||
webkitdirectory=""
|
||||
multiple
|
||||
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
|
||||
onChange={() => {
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
{isUploading ? 'importing...' : 'import sounds folder'}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { useLogger } from '../useLogger';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
@@ -115,6 +116,7 @@ function PanelNav({ children, className, settings, ...props }) {
|
||||
}
|
||||
|
||||
function PanelContent({ context, tab }) {
|
||||
useLogger();
|
||||
switch (tab) {
|
||||
case tabNames.patterns:
|
||||
return <PatternsTab context={context} />;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
exportPatterns,
|
||||
importPatterns,
|
||||
loadAndSetFeaturedPatterns,
|
||||
loadAndSetPublicPatterns,
|
||||
patternFilterName,
|
||||
useActivePattern,
|
||||
useViewingPatternData,
|
||||
@@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -25,21 +27,19 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const date = new Date(pattern.created_at);
|
||||
if (!isNaN(date)) {
|
||||
title = date.toLocaleDateString();
|
||||
} else {
|
||||
title = 'unnamed';
|
||||
}
|
||||
}
|
||||
if (title == null) {
|
||||
title = pattern.hash;
|
||||
}
|
||||
if (title == null) {
|
||||
title = 'unnamed';
|
||||
}
|
||||
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
|
||||
|
||||
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
|
||||
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
return (
|
||||
<a
|
||||
className={classNames(
|
||||
className={cx(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||
showOutline && 'outline outline-1',
|
||||
showHiglight && 'bg-selection',
|
||||
@@ -56,7 +56,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const viewingPatternID = viewingPatternData.id;
|
||||
return (
|
||||
<div className="font-mono text-sm">
|
||||
<div className="">
|
||||
{Object.values(patterns)
|
||||
.reverse()
|
||||
.map((pattern) => {
|
||||
@@ -84,82 +84,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
|
||||
const { userPatterns, patternFilter } = useSettings();
|
||||
|
||||
const examplePatterns = useExamplePatterns();
|
||||
const collections = examplePatterns.collections;
|
||||
|
||||
const updateCodeWindow = (patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<div>
|
||||
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow({ ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
|
||||
updateCodeWindow(
|
||||
context,
|
||||
{ ...userPatterns[id], collection: userPattern.collection },
|
||||
autoResetPatternOnChange,
|
||||
)
|
||||
}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
@@ -167,24 +157,111 @@ export function PatternsTab({ context }) {
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
)}
|
||||
{patternFilter !== patternFilterName.user &&
|
||||
Array.from(collections.keys()).map((collection) => {
|
||||
const patterns = collections.get(collection);
|
||||
return (
|
||||
<section key={collection} className="py-2">
|
||||
<h2 className="text-xl mb-2">{collection}</h2>
|
||||
<div className="font-mono text-sm">
|
||||
<PatternButtons
|
||||
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
|
||||
started={context.started}
|
||||
patterns={patterns}
|
||||
activePattern={activePattern}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
|
||||
const [page, setPage] = useState(initialPage);
|
||||
const debouncedPageChange = useDebounce(() => {
|
||||
paginationOnChange(page);
|
||||
});
|
||||
|
||||
const onPageChange = (pageNum) => {
|
||||
setPage(pageNum);
|
||||
debouncedPageChange();
|
||||
};
|
||||
|
||||
const activePattern = useActivePattern();
|
||||
return (
|
||||
<div className="flex flex-grow flex-col h-full overflow-hidden justify-between">
|
||||
<div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md ">
|
||||
<PatternButtons
|
||||
onClick={(id) => patternOnClick(id)}
|
||||
started={context.started}
|
||||
patterns={patterns}
|
||||
activePattern={activePattern}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<label htmlFor="pattern pagination">Page</label>
|
||||
<Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let featuredPageNum = 1;
|
||||
function FeaturedPatterns({ context }) {
|
||||
const examplePatterns = useExamplePatterns();
|
||||
const collections = examplePatterns.collections;
|
||||
const patterns = collections.get(patternFilterName.featured);
|
||||
return (
|
||||
<PatternPageWithPagination
|
||||
patterns={patterns}
|
||||
context={context}
|
||||
initialPage={featuredPageNum}
|
||||
patternOnClick={(id) => {
|
||||
updateCodeWindow(
|
||||
context,
|
||||
{ ...patterns[id], collection: patternFilterName.featured },
|
||||
autoResetPatternOnChange,
|
||||
);
|
||||
}}
|
||||
paginationOnChange={async (pageNum) => {
|
||||
await loadAndSetFeaturedPatterns(pageNum - 1);
|
||||
featuredPageNum = pageNum;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let latestPageNum = 1;
|
||||
function LatestPatterns({ context }) {
|
||||
const examplePatterns = useExamplePatterns();
|
||||
const collections = examplePatterns.collections;
|
||||
const patterns = collections.get(patternFilterName.public);
|
||||
return (
|
||||
<PatternPageWithPagination
|
||||
patterns={patterns}
|
||||
context={context}
|
||||
initialPage={latestPageNum}
|
||||
patternOnClick={(id) => {
|
||||
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
|
||||
}}
|
||||
paginationOnChange={async (pageNum) => {
|
||||
await loadAndSetPublicPatterns(pageNum - 1);
|
||||
latestPageNum = pageNum;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PublicPatterns({ context }) {
|
||||
const { patternFilter } = useSettings();
|
||||
if (patternFilter === patternFilterName.featured) {
|
||||
return <FeaturedPatterns context={context} />;
|
||||
}
|
||||
return <LatestPatterns context={context} />;
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const { patternFilter } = useSettings();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
|
||||
{patternFilter === patternFilterName.user ? (
|
||||
<UserPatterns context={context} />
|
||||
) : (
|
||||
<PublicPatterns context={context} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
@@ -25,21 +26,16 @@ export function Reference() {
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
|
||||
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md border-none"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onInput={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
@@ -79,7 +75,9 @@ export function Reference() {
|
||||
))}
|
||||
</ul>
|
||||
{entry.examples?.map((example, j) => (
|
||||
<pre key={j}>{example}</pre>
|
||||
<pre className="bg-background" key={j}>
|
||||
{example}
|
||||
</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
|
||||
<select
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
className="p-2 bg-background rounded-md text-foreground"
|
||||
className="p-2 bg-background rounded-md text-foreground border-foreground"
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { themes } from '@strudel/codemirror';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio';
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
@@ -18,7 +20,7 @@ function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
function SelectInput({ value, options, onChange }) {
|
||||
return (
|
||||
<select
|
||||
className="p-2 bg-background rounded-md text-foreground"
|
||||
className="p-2 bg-background rounded-md text-foreground border-foreground"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
@@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FormItem({ label, children }) {
|
||||
function FormItem({ label, children, sublabel }) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<label>{label}</label>
|
||||
@@ -66,6 +68,7 @@ const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
|
||||
const fontFamilyOptions = {
|
||||
monospace: 'monospace',
|
||||
Courier: 'Courier',
|
||||
CutiePi: 'CutiePi',
|
||||
JetBrains: 'JetBrains',
|
||||
Hack: 'Hack',
|
||||
FiraCode: 'FiraCode',
|
||||
@@ -104,11 +107,12 @@ export function SettingsTab({ started }) {
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
togglePanelTrigger,
|
||||
maxPolyphony,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4 w-full">
|
||||
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -138,10 +142,30 @@ export function SettingsTab({ started }) {
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Maximum Polyphony">
|
||||
<Textbox
|
||||
min={1}
|
||||
max={Infinity}
|
||||
onBlur={(e) => {
|
||||
let v = parseInt(e.target.value);
|
||||
v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v;
|
||||
setMaxPolyphony(v);
|
||||
settingsMap.setKey('maxPolyphony', v);
|
||||
}}
|
||||
onChange={(v) => {
|
||||
v = Math.max(1, parseInt(v));
|
||||
settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v);
|
||||
}}
|
||||
type="number"
|
||||
placeholder=""
|
||||
value={maxPolyphony ?? ''}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
|
||||
<FormItem label="Font Family">
|
||||
<SelectInput
|
||||
options={fontFamilyOptions}
|
||||
@@ -178,25 +202,7 @@ export function SettingsTab({ started }) {
|
||||
value={togglePanelTrigger}
|
||||
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
|
||||
items={{ click: 'Click', hover: 'Hover' }}
|
||||
></ButtonGroup>
|
||||
{/* <Checkbox
|
||||
label="Click"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'click');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger != 'hover'}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hover"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'hover');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger == 'hover'}
|
||||
/> */}
|
||||
</FormItem>
|
||||
<FormItem label="More Settings">
|
||||
<Checkbox
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
@@ -13,6 +14,8 @@ export function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
const { soundsFilter } = useSettings();
|
||||
const [search, setSearch] = useState('');
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
const soundEntries = useMemo(() => {
|
||||
if (!sounds) {
|
||||
@@ -36,6 +39,9 @@ export function SoundsTab() {
|
||||
if (soundsFilter === 'synths') {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
if (soundsFilter === 'importSounds') {
|
||||
return [];
|
||||
}
|
||||
return filtered;
|
||||
}, [sounds, soundsFilter, search]);
|
||||
|
||||
@@ -50,15 +56,9 @@ export function SoundsTab() {
|
||||
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md my-2"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
|
||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||
<ButtonGroup
|
||||
@@ -69,12 +69,12 @@ export function SoundsTab() {
|
||||
drums: 'drum-machines',
|
||||
synths: 'Synths',
|
||||
user: 'User',
|
||||
importSounds: 'import-sounds',
|
||||
}}
|
||||
></ButtonGroup>
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
@@ -105,7 +105,55 @@ export function SoundsTab() {
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
|
||||
{!soundEntries.length && soundsFilter === 'importSounds' ? (
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
<p>
|
||||
To import sounds into strudel, they must be contained{' '}
|
||||
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
|
||||
within a folder or subfolder
|
||||
</a>
|
||||
. The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or
|
||||
soundbanks (see diagram below).{' '}
|
||||
</p>
|
||||
<pre className="bg-background" key={'sample-diagram'}>
|
||||
{`└─ samples <-- import this folder
|
||||
├─ swoop
|
||||
│ ├─ swoopshort.wav
|
||||
│ ├─ swooplong.wav
|
||||
│ └─ swooptight.wav
|
||||
└─ smash
|
||||
├─ smashhigh.wav
|
||||
├─ smashlow.wav
|
||||
└─ smashmiddle.wav`}
|
||||
</pre>
|
||||
<p>
|
||||
The name of a subfolder corresponds to the sound name under the “user” tab. Multiple samples within a
|
||||
subfolder are all labelled with the same name, but can be accessed using “.n( )” - remember sounds are
|
||||
zero-indexed and in alphabetical order!
|
||||
</p>
|
||||
<p>
|
||||
For more information, and other ways to use your own sounds in strudel,{' '}
|
||||
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
|
||||
check out the docs
|
||||
</a>
|
||||
!
|
||||
</p>
|
||||
<h3>Preview Sounds</h3>
|
||||
<pre className="bg-background" key={'sample-preview'}>
|
||||
n("0 1 2 3 4 5").s("sample-name")
|
||||
</pre>
|
||||
<p>
|
||||
Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your
|
||||
sample as it appears under the "user" tab.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
||||
? 'No custom sounds loaded in this pattern (yet).'
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import cx from '@src/cx.mjs';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function WelcomeTab({ context }) {
|
||||
const { fontFamily } = useSettings();
|
||||
return (
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
|
||||
<h3>꩜ welcome</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
||||
@@ -43,7 +44,8 @@ export function WelcomeTab({ context }) {
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. Please consider to{' '}
|
||||
. You can also find <a href="https://github.com/felixroos/dough-samples/blob/main/README.md">licensing info</a>{' '}
|
||||
for the default sound banks there. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
export function Textbox({ onChange, className, ...inputProps }) {
|
||||
return (
|
||||
<input
|
||||
className={cx(
|
||||
'p-2 bg-background rounded-md border-foreground text-foreground placeholder-foreground',
|
||||
className,
|
||||
)}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import { logger } from '@strudel/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const $strudel_log_history = atom([]);
|
||||
|
||||
function useLoggerEvent(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
function getUpdatedLog(log, event) {
|
||||
const { message, type, data } = event.detail;
|
||||
const lastLog = log.length ? log[log.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
if (type === 'loaded-sample') {
|
||||
const loadIndex = log.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
log[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
log = log.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
log = log.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return log.slice(-20);
|
||||
}
|
||||
|
||||
export function useLogger() {
|
||||
useLoggerEvent((event) => {
|
||||
const log = $strudel_log_history.get();
|
||||
const newLog = getUpdatedLog(log, event);
|
||||
$strudel_log_history.set(newLog);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
function debounce(fn, wait) {
|
||||
let timer;
|
||||
return function (...args) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function useDebounce(callback) {
|
||||
const ref = useRef;
|
||||
useEffect(() => {
|
||||
ref.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
const debouncedCallback = useMemo(() => {
|
||||
const func = () => {
|
||||
ref.current?.();
|
||||
};
|
||||
|
||||
return debounce(func, 1000);
|
||||
}, []);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
||||
@@ -54,25 +54,23 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
const blob = soundFile.blob;
|
||||
|
||||
// Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted
|
||||
if (typeof blob === 'string') {
|
||||
const soundPaths = sounds.get(parentDirectory) ?? new Set();
|
||||
soundPaths.add(blob);
|
||||
sounds.set(parentDirectory, soundPaths);
|
||||
return;
|
||||
}
|
||||
|
||||
return blobToDataUrl(blob).then((soundPath) => {
|
||||
const soundPaths = sounds.get(parentDirectory) ?? new Set();
|
||||
soundPaths.add(soundPath);
|
||||
sounds.set(parentDirectory, soundPaths);
|
||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||
|
||||
titlePathMap.set(title, soundPath);
|
||||
|
||||
sounds.set(parentDirectory, titlePathMap);
|
||||
return;
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
sounds.forEach((soundPaths, key) => {
|
||||
const value = Array.from(soundPaths);
|
||||
sounds.forEach((titlePathMap, key) => {
|
||||
const value = Array.from(titlePathMap.keys())
|
||||
.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((title) => titlePathMap.get(title));
|
||||
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
|
||||
@@ -115,6 +115,29 @@ export async function prebake() {
|
||||
'numbers/7.wav',
|
||||
'numbers/8.wav',
|
||||
],
|
||||
num: [
|
||||
'num/00.wav',
|
||||
'num/01.wav',
|
||||
'num/02.wav',
|
||||
'num/03.wav',
|
||||
'num/04.wav',
|
||||
'num/05.wav',
|
||||
'num/06.wav',
|
||||
'num/07.wav',
|
||||
'num/08.wav',
|
||||
'num/09.wav',
|
||||
'num/10.wav',
|
||||
'num/11.wav',
|
||||
'num/12.wav',
|
||||
'num/13.wav',
|
||||
'num/14.wav',
|
||||
'num/15.wav',
|
||||
'num/16.wav',
|
||||
'num/17.wav',
|
||||
'num/18.wav',
|
||||
'num/19.wav',
|
||||
'num/20.wav',
|
||||
],
|
||||
},
|
||||
'github:tidalcycles/dirt-samples',
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user