Merge remote-tracking branch 'origin/main' into uzu
This commit is contained in:
commit
2da03ab46f
43 changed files with 1726 additions and 418 deletions
|
|
@ -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([]);
|
||||
|
|
|
|||
56
packages/codemirror/themes.mjs
vendored
56
packages/codemirror/themes.mjs
vendored
|
|
@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
|
|||
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
|
||||
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
|
||||
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
|
||||
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
|
||||
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
|
||||
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
|
||||
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
|
||||
import aura, { settings as auraSettings } from './themes/aura.mjs';
|
||||
import bespin, { settings as bespinSettings } from './themes/bespin.mjs';
|
||||
import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
|
||||
import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
|
||||
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
|
||||
import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs';
|
||||
import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
|
||||
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
|
||||
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
|
||||
|
|
@ -32,52 +31,47 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t
|
|||
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
|
||||
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
|
||||
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
|
||||
// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs';
|
||||
import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
|
||||
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
|
||||
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
|
||||
|
||||
import { setTheme } from '@strudel/draw';
|
||||
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
bluescreen,
|
||||
blackscreen,
|
||||
whitescreen,
|
||||
teletext,
|
||||
algoboy,
|
||||
androidstudio,
|
||||
atomone,
|
||||
aura,
|
||||
bbedit,
|
||||
blackscreen,
|
||||
bluescreen,
|
||||
CutiePi,
|
||||
darcula,
|
||||
dracula,
|
||||
// todo: optimize
|
||||
// bespin,
|
||||
//abcdef,
|
||||
androidstudio,
|
||||
duotoneDark,
|
||||
eclipse,
|
||||
githubDark,
|
||||
CutiePi,
|
||||
githubLight,
|
||||
greenText,
|
||||
gruvboxDark,
|
||||
gruvboxLight,
|
||||
sonicPink,
|
||||
materialDark,
|
||||
nord,
|
||||
materialLight,
|
||||
monokai,
|
||||
noctisLilac,
|
||||
nord,
|
||||
redText,
|
||||
solarizedDark,
|
||||
solarizedLight,
|
||||
sublime,
|
||||
teletext,
|
||||
tokyoNight,
|
||||
tokyoNightDay,
|
||||
tokyoNightStorm,
|
||||
vscodeDark,
|
||||
//xcodeDark,
|
||||
// LIGHT
|
||||
bbedit,
|
||||
//duotoneLight,
|
||||
eclipse,
|
||||
githubLight,
|
||||
gruvboxLight,
|
||||
materialLight,
|
||||
vscodeLight,
|
||||
noctisLilac,
|
||||
solarizedLight,
|
||||
tokyoNightDay,
|
||||
whitescreen,
|
||||
xcodeLight,
|
||||
};
|
||||
|
||||
|
|
@ -88,21 +82,19 @@ export const settings = {
|
|||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
terminal: terminalSettings,
|
||||
abcdef: abcdefSettings,
|
||||
androidstudio: androidstudioSettings,
|
||||
atomone: atomOneSettings,
|
||||
aura: auraSettings,
|
||||
bbedit: bbeditSettings,
|
||||
bespin: bespinSettings,
|
||||
darcula: darculaSettings,
|
||||
dracula: draculaSettings,
|
||||
duotoneLight: duotoneLightSettings,
|
||||
duotoneDark: duotoneDarkSettings,
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
sonicPink: sonicPinkSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
greenText: greenTextSettings,
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
gruvboxLight: gruvboxLightSettings,
|
||||
materialDark: materialDarkSettings,
|
||||
|
|
@ -110,6 +102,7 @@ export const settings = {
|
|||
noctisLilac: noctisLilacSettings,
|
||||
nord: nordSettings,
|
||||
monokai: monokaiSettings,
|
||||
redText: redTextSettings,
|
||||
solarizedLight: solarizedLightSettings,
|
||||
solarizedDark: solarizedDarkSettings,
|
||||
sublime: sublimeSettings,
|
||||
|
|
@ -118,7 +111,6 @@ export const settings = {
|
|||
vscodeDark: vscodeDarkSettings,
|
||||
vscodeLight: vscodeLightSettings,
|
||||
xcodeLight: xcodeLightSettings,
|
||||
//xcodeDark: xcodeDarkSettings,
|
||||
tokyoNightDay: tokyoNightDaySettings,
|
||||
};
|
||||
|
||||
|
|
|
|||
55
packages/codemirror/themes/abcdef.mjs
vendored
55
packages/codemirror/themes/abcdef.mjs
vendored
|
|
@ -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
packages/codemirror/themes/algoboy.mjs
vendored
1
packages/codemirror/themes/algoboy.mjs
vendored
|
|
@ -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
packages/codemirror/themes/atomone.mjs
vendored
2
packages/codemirror/themes/atomone.mjs
vendored
|
|
@ -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
packages/codemirror/themes/bespin.mjs
vendored
39
packages/codemirror/themes/bespin.mjs
vendored
|
|
@ -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
packages/codemirror/themes/green-text.mjs
vendored
Normal file
39
packages/codemirror/themes/green-text.mjs
vendored
Normal file
|
|
@ -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
packages/codemirror/themes/red-text.mjs
vendored
Normal file
39
packages/codemirror/themes/red-text.mjs
vendored
Normal file
|
|
@ -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
packages/codemirror/themes/sonic-pink.mjs
vendored
Normal file
39
packages/codemirror/themes/sonic-pink.mjs
vendored
Normal file
|
|
@ -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] },
|
||||
],
|
||||
});
|
||||
4
packages/codemirror/themes/strudel-theme.mjs
vendored
4
packages/codemirror/themes/strudel-theme.mjs
vendored
|
|
@ -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
packages/codemirror/themes/xcodeDark.mjs
vendored
34
packages/codemirror/themes/xcodeDark.mjs
vendored
|
|
@ -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' },
|
||||
],
|
||||
});
|
||||
|
|
@ -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/>.
|
||||
*/
|
||||
|
|
@ -1523,22 +1523,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');
|
||||
|
|
@ -1631,6 +1620,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);
|
||||
|
|
@ -1642,14 +1776,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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2108,11 +2108,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);
|
||||
});
|
||||
|
||||
|
|
@ -2488,16 +2490,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})`);
|
||||
|
|
|
|||
|
|
@ -528,6 +528,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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
```
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,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.`}
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 (['-', '~'].includes(s)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -495,10 +566,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?`);
|
||||
|
|
@ -628,6 +704,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;
|
||||
|
|
@ -645,6 +722,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
|
||||
|
|
@ -652,14 +730,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 waveformAliases = [
|
||||
|
|
@ -69,7 +73,9 @@ export function registerSynthSounds() {
|
|||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (releaseTime) => {},
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
|
|
@ -116,10 +122,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();
|
||||
|
|
@ -129,11 +137,11 @@ export function registerSynthSounds() {
|
|||
end,
|
||||
);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {},
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
|
|
@ -175,10 +183,12 @@ export function registerSynthSounds() {
|
|||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
webAudioTimeout(
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
o.disconnect();
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
onended();
|
||||
fm?.stop();
|
||||
|
|
@ -188,11 +198,11 @@ export function registerSynthSounds() {
|
|||
end,
|
||||
);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {},
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
|
|
@ -235,7 +245,9 @@ export function registerSynthSounds() {
|
|||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (releaseTime) => {},
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
|
|
|
|||
|
|
@ -710,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue