mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 05:33:13 -04:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80875cca95 | |||
| b954a25b2b | |||
| cca346d2c2 | |||
| 3343c7bcf0 | |||
| 6e3c5b23dd | |||
| c12a77a0a1 | |||
| e5ab6b3c93 | |||
| e82bb6c410 | |||
| 8b1fb12388 | |||
| 1f1f3288a6 | |||
| d642c0e12f | |||
| 1eb657e6fe | |||
| 7c5f53f9ae | |||
| efe7c9394f | |||
| 5355a0c563 | |||
| 75db9ff16e | |||
| 12cc1e9b5e | |||
| 3a284696c9 | |||
| 753dc48b69 | |||
| 5ee5d4f1b1 | |||
| 8d2bca59c4 | |||
| af88471f6b | |||
| f9a9993c42 | |||
| dab1676761 | |||
| d753eedb90 | |||
| 4743334a17 | |||
| aa8209ad4c | |||
| 6908ea38e4 | |||
| 103b27c21f | |||
| 597c941991 | |||
| f0aac2098a | |||
| c1a185e852 | |||
| b9fce15042 |
@@ -2,8 +2,62 @@
|
||||
|
||||
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
|
||||
|
||||
## january 2026
|
||||
|
||||
- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851)
|
||||
- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883)
|
||||
- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889)
|
||||
- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880)
|
||||
- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by 1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833)
|
||||
- 2026-01-11T11:37:37+01:00 fix: add trem to top level by froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887)
|
||||
- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888)
|
||||
- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836)
|
||||
- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886)
|
||||
- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885)
|
||||
- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884)
|
||||
- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810)
|
||||
- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878)
|
||||
- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877)
|
||||
- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786)
|
||||
- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826)
|
||||
- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874)
|
||||
- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872)
|
||||
- 2026-01-04T02:01:07+01:00 Make stretch modulatable by glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870)
|
||||
- 2026-01-02T18:44:53+01:00 Make pan modulatable by glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865)
|
||||
- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850)
|
||||
- 2026-01-01T21:39:05+01:00 fix: missing punctuation by eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860)
|
||||
- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864)
|
||||
- 2026-01-01T19:52:15+01:00 Feat: FX Chains by glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861)
|
||||
|
||||
## december 2025
|
||||
|
||||
- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859)
|
||||
- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858)
|
||||
- 2025-12-28T14:20:03+01:00 dough repl fixes by froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855)
|
||||
- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839)
|
||||
- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817)
|
||||
- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816)
|
||||
- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818)
|
||||
- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815)
|
||||
- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832)
|
||||
- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835)
|
||||
- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831)
|
||||
- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830)
|
||||
- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827)
|
||||
- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809)
|
||||
- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781)
|
||||
- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800)
|
||||
- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820)
|
||||
- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805)
|
||||
- **2025-12-01 strudel.cc deployed**
|
||||
|
||||
## november 2025
|
||||
|
||||
- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796)
|
||||
- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791)
|
||||
- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787)
|
||||
- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795)
|
||||
- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794)
|
||||
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
|
||||
- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
|
||||
- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
|
||||
@@ -52,6 +106,7 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly
|
||||
- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
|
||||
- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
|
||||
- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
|
||||
- **2025-10-27 @strudel/core@1.2.5**
|
||||
- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
|
||||
- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
|
||||
- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
|
||||
|
||||
@@ -24,6 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -119,6 +120,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => jumpToCharacter(view, '$', -1),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
|
||||
export function jumpToCharacter(view, character, direction = 1) {
|
||||
const { state, dispatch } = view;
|
||||
const pos = state.selection.main.head;
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
let characterPositions = [];
|
||||
let jumpPos;
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
if (!characterPositions.length) {
|
||||
return false;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
} else {
|
||||
jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0);
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return false;
|
||||
}
|
||||
dispatch({
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export * from './repl.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './schedulerState.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './ui.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
+11
-1
@@ -2,7 +2,13 @@ import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import {
|
||||
setCpsFunc,
|
||||
setIsStarted,
|
||||
setPattern as exposeSchedulerPattern,
|
||||
setTime,
|
||||
setTriggerFunc,
|
||||
} from './schedulerState.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
import { reset_state } from './impure.mjs';
|
||||
@@ -52,6 +58,7 @@ export function repl({
|
||||
getTime,
|
||||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
setIsStarted(started);
|
||||
onToggle?.(started);
|
||||
if (!started) {
|
||||
reset_state();
|
||||
@@ -65,6 +72,8 @@ export function repl({
|
||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||
const scheduler =
|
||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||
setTriggerFunc(schedulerOptions.onTrigger);
|
||||
setCpsFunc(() => scheduler.cps);
|
||||
let pPatterns = {};
|
||||
let anonymousIndex = 0;
|
||||
let allTransform;
|
||||
@@ -89,6 +98,7 @@ export function repl({
|
||||
const setPattern = async (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
await scheduler.setPattern(pattern, autostart);
|
||||
exposeSchedulerPattern(pattern);
|
||||
return pattern;
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
schedulerState.mjs - Module to pipe out various parameters from the scheduler for global consumption
|
||||
Copyright (C) 2026 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/schedulerState.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/>.
|
||||
*/
|
||||
|
||||
let time;
|
||||
let cpsFunc;
|
||||
let pattern;
|
||||
let triggerFunc;
|
||||
let isStarted;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
|
||||
export function setCpsFunc(func) {
|
||||
cpsFunc = func;
|
||||
}
|
||||
|
||||
export function getCps() {
|
||||
return cpsFunc?.();
|
||||
}
|
||||
|
||||
export function setPattern(pat) {
|
||||
pattern = pat;
|
||||
}
|
||||
|
||||
export function getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
export function setTriggerFunc(func) {
|
||||
triggerFunc = func;
|
||||
}
|
||||
|
||||
export function getTriggerFunc() {
|
||||
return triggerFunc;
|
||||
}
|
||||
|
||||
export function setIsStarted(val) {
|
||||
isStarted = !!val;
|
||||
}
|
||||
|
||||
export function getIsStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
let time;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
+179
-28
@@ -5,9 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger, ref } from '@strudel/core';
|
||||
import {
|
||||
Hap,
|
||||
Pattern,
|
||||
TimeSpan,
|
||||
getCps,
|
||||
getIsStarted,
|
||||
getPattern,
|
||||
getTime,
|
||||
getTriggerFunc,
|
||||
isPattern,
|
||||
logger,
|
||||
ref,
|
||||
reify,
|
||||
} from '@strudel/core';
|
||||
import { noteToMidi, getControlName } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
@@ -477,14 +491,41 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
const refsByChan = {};
|
||||
/**
|
||||
* Initialize a midi device
|
||||
*/
|
||||
async function _initialize(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
if (initial) {
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`[midi] Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||
*
|
||||
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
|
||||
*
|
||||
* @name midin
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
|
||||
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
|
||||
@@ -498,29 +539,11 @@ const refsByChan = {};
|
||||
* note("c a f e").s("saw")
|
||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||
*/
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
const refsByChan = {};
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
if (initial) {
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const device = await _initialize(input);
|
||||
refs[input] ??= {};
|
||||
refsByChan[input] ??= {};
|
||||
const cc = (cc, chan) => {
|
||||
@@ -532,8 +555,7 @@ export async function midin(input) {
|
||||
|
||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
||||
listeners[input] = (e) => {
|
||||
const ccNum = e.dataBytes[0];
|
||||
const v = e.dataBytes[1];
|
||||
const [ccNum, v] = e.dataBytes;
|
||||
const chan = e.message.channel;
|
||||
const scaled = v / 127;
|
||||
refsByChan[input][ccNum] ??= {};
|
||||
@@ -543,3 +565,132 @@ export async function midin(input) {
|
||||
device.addListener('midimessage', listeners[input]);
|
||||
return cc;
|
||||
}
|
||||
|
||||
/**
|
||||
* MIDI keyboard: Opens a MIDI input port to receive MIDI keyboard messages.
|
||||
*
|
||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||
* note durations
|
||||
*
|
||||
* @name midikeys
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
|
||||
* When queried, the pattern will produces the most recently played midi notes and velocities,
|
||||
* lasting for the specified duration
|
||||
* @example
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb().s("tri").lpf(80).lpe(6).lpd(0.1).room(2).delay(0.35)
|
||||
* @example
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb("0.5 1")
|
||||
* .s("saw")
|
||||
* .add(note(rand.mul(0.3)))
|
||||
* .lpf(1000).lpe(2).room(0.5)
|
||||
*/
|
||||
const kHaps = {};
|
||||
const kListeners = {};
|
||||
|
||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||
const pattern = getPattern();
|
||||
const trigger = getTriggerFunc();
|
||||
if (!pattern || !trigger) {
|
||||
return false;
|
||||
}
|
||||
const t = now + latencyCycles;
|
||||
const eps = 1e-6;
|
||||
const haps = pattern.queryArc(t - eps, t + eps, { _cps: cps });
|
||||
// Only keep haps coming from `midikeys`
|
||||
const kbHaps = haps.filter((hap) => hap.value?.midikey?.startsWith(`${input}_`));
|
||||
const ctxNow = getAudioContext().currentTime;
|
||||
if (!kbHaps.length) {
|
||||
return false;
|
||||
}
|
||||
kbHaps.forEach((hap) => {
|
||||
if (!hap.hasOnset()) {
|
||||
return;
|
||||
}
|
||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||
const duration = hap.duration / cps;
|
||||
trigger(hap, t - ctxNow, duration, cps, t);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
export async function midikeys(input) {
|
||||
const device = await _initialize(input);
|
||||
if (!kHaps[input]) {
|
||||
kHaps[input] = [];
|
||||
}
|
||||
kListeners[input] && device.removeListener('midimessage', kListeners[input]);
|
||||
kListeners[input] = (e) => {
|
||||
const { dataBytes, message } = e;
|
||||
const noteon = message.command === 9;
|
||||
let noteoff = message.command === 8;
|
||||
// Don't enqueue or trigger midi notes if scheduler is not started
|
||||
const notStarted = !getIsStarted();
|
||||
// Ignore non-note messages (e.g. CC, pitchbend, modwheel, etc.)
|
||||
const notANote = !noteon && !noteoff;
|
||||
if (notStarted || notANote) {
|
||||
return;
|
||||
}
|
||||
const [note, velocity] = dataBytes;
|
||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||
const key = `${input}_${note}`;
|
||||
const cps = getCps() ?? 0.5;
|
||||
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||
const now = getTime();
|
||||
const t = now + latencySeconds * cps;
|
||||
const span = new TimeSpan(t, t);
|
||||
let value = { midikey: key };
|
||||
if (noteoff) {
|
||||
/* TODO: It's a big effort, but we could modify superdough to allow for situations where
|
||||
we don't know the hap duration in advance. This would mean, for example, that if the hap
|
||||
is flagged as such a special note-on event, we have all effects be persistent & all ADSR
|
||||
envelopes stop at the S stage [and store references to them by `midikey`]
|
||||
If this is implemented, then getting full keyboard functionality should be as simple
|
||||
as sending the corresponding note-off event below and triggering `release` on each of those
|
||||
referenced effects/envelopes
|
||||
|
||||
value = { ...value, noteoff: true };
|
||||
|
||||
If this is achieved, we can remove the noteLength parameter
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
|
||||
}
|
||||
kHaps[input].push(new Hap(span, span, value, {}));
|
||||
if (!noteoff && triggerAvailable) {
|
||||
// If we have access to a trigger function, we call it to immediately
|
||||
// dispatch to the audio engine, rather than waiting for cyclist to catch these haps
|
||||
const triggered = _triggerKeyboard(input, cps, now, latencySeconds * cps);
|
||||
if (triggered) {
|
||||
kHaps[input] = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
device.addListener('midimessage', kListeners[input]);
|
||||
const kb = (noteLength = 0.5) => {
|
||||
const nlPat = reify(noteLength);
|
||||
const query = (state) => {
|
||||
const haps = kHaps[input].flatMap((hap) => {
|
||||
const lenHaps = nlPat.query(state.setSpan(hap.wholeOrPart()));
|
||||
return lenHaps.map((lenHap) => {
|
||||
const nl = lenHap.value ?? 0.5;
|
||||
const whole = new TimeSpan(hap.whole.begin, hap.whole.begin.add(nl));
|
||||
const part = new TimeSpan(hap.part.begin, hap.part.begin.add(nl));
|
||||
const context = hap.combineContext(lenHap);
|
||||
return new Hap(whole, part, hap.value, context);
|
||||
});
|
||||
});
|
||||
if (state.controls.cyclist) {
|
||||
// Notes have been sent; clear them
|
||||
kHaps[input] = [];
|
||||
}
|
||||
return haps;
|
||||
};
|
||||
return new Pattern(query);
|
||||
};
|
||||
return kb;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
const nodePools = new Map();
|
||||
const POOL_KEY = Symbol('nodePoolKey');
|
||||
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
|
||||
const MAX_POOL_SIZE = 64;
|
||||
|
||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||
|
||||
const getNodeTime = (node) => {
|
||||
return node.context?.currentTime ?? 0;
|
||||
};
|
||||
|
||||
const getParams = (node) => {
|
||||
const params = new Set();
|
||||
node.parameters?.forEach((param) => params.add(param));
|
||||
@@ -38,34 +40,43 @@ export const releaseNodeToPool = (node) => {
|
||||
// not reusable
|
||||
return;
|
||||
}
|
||||
if (node[IS_WORKLET_DEAD]) {
|
||||
// Worklet already terminated, don't pool it
|
||||
return;
|
||||
}
|
||||
const key = node[POOL_KEY];
|
||||
if (key == null) return;
|
||||
const now = node.context?.currentTime ?? 0;
|
||||
const now = getNodeTime(node);
|
||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
if (pool.length < MAX_POOL_SIZE) {
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
}
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
};
|
||||
|
||||
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
||||
// Audio worklets are given a grace period to survive (`return true`) after
|
||||
// being released. This concludes at time `end + 0.5`. We test here whether we are
|
||||
// within some safe distance of that (`end + 0.45`) and if so, permit the node to be
|
||||
// released. This helps to prevent race conditions between node termination and node
|
||||
// re-use
|
||||
const isNodeAlive = (node) => {
|
||||
// Skip check if node is not a worklet
|
||||
if (!(node instanceof AudioWorkletNode)) return true;
|
||||
const now = getNodeTime(node);
|
||||
const end = node?.parameters?.get('end').value ?? 0;
|
||||
return now < end + 0.45;
|
||||
};
|
||||
|
||||
// Attempt to get node from the pool. If this fails, fall back
|
||||
// to building it with the factory
|
||||
export const getNodeFromPool = (key, factory) => {
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
let node;
|
||||
let found = false;
|
||||
while (pool.length) {
|
||||
const ref = pool.pop();
|
||||
node = ref?.deref();
|
||||
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
||||
if (node != null && isNodeAlive(node)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (node == null || node[IS_WORKLET_DEAD]) {
|
||||
if (!found) {
|
||||
node = factory();
|
||||
}
|
||||
node[POOL_KEY] = key;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||
const waveformAliases = [
|
||||
@@ -185,10 +185,6 @@ export function registerSynthSounds() {
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
o.port.postMessage({ type: 'initialize' });
|
||||
o.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(o);
|
||||
o.port.onmessage = null;
|
||||
};
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
webAudioTimeout,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const Warpmode = Object.freeze({
|
||||
@@ -251,10 +251,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
source.port.postMessage({ type: 'initialize', payload });
|
||||
source.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(source);
|
||||
source.port.onmessage = null;
|
||||
};
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
|
||||
@@ -463,7 +463,6 @@ registerProcessor('distort-processor', DistortProcessor);
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
@@ -479,16 +478,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -523,16 +522,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime >= params.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
const begin = params.begin[0];
|
||||
const end = params.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
return true;
|
||||
}
|
||||
const output = outputs[0];
|
||||
@@ -1148,8 +1148,8 @@ const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
@@ -1164,7 +1164,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
@@ -1332,16 +1331,17 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
const begin = parameters.begin[0];
|
||||
const end = parameters.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
return true;
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
|
||||
@@ -7241,6 +7241,10 @@ exports[`runs examples > example "midicmd" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
|
||||
@@ -131,6 +131,10 @@ const midin = () => {
|
||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||
};
|
||||
|
||||
const midikeys = async () => {
|
||||
return () => strudel.silence;
|
||||
};
|
||||
|
||||
const sysex = ([id, data]) => {};
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
@@ -150,6 +154,7 @@ evalScope(
|
||||
*/
|
||||
{
|
||||
midin,
|
||||
midikeys,
|
||||
sysex,
|
||||
// gist,
|
||||
// euclid,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag'];
|
||||
|
||||
export function getMetadata(raw_code) {
|
||||
if (raw_code == null) {
|
||||
|
||||
@@ -16,10 +16,14 @@ 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:
|
||||
|
||||
## midiin(inputName?)
|
||||
## midin(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midin" h={0} />
|
||||
|
||||
## midikeys(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midikeys" 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.
|
||||
|
||||
@@ -13,7 +13,7 @@ Let's take a closer look about how sounds are implemented in the webaudio output
|
||||
|
||||
All sounds are registered in the sound map, using the the `registerSound` function:
|
||||
|
||||
```ts
|
||||
```js
|
||||
function registerSound(
|
||||
name: string, // The name of the sound that should be given to `s`, e.g. `mysaw`
|
||||
// The function called by the scheduler to trigger the sound:
|
||||
@@ -35,34 +35,36 @@ When `registerSound` is called, it registers `{ onTrigger, data }` under the giv
|
||||
|
||||
This might be a bit abstract, so here is a minimal example:
|
||||
|
||||
```js
|
||||
registerSound(
|
||||
'mysaw',
|
||||
(time, value, onended) => {
|
||||
let { freq } = value; // destructure control params
|
||||
const ctx = getAudioContext();
|
||||
// create oscillator
|
||||
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
|
||||
o.start(time);
|
||||
// add gain node to level down osc
|
||||
const g = new GainNode(ctx, { gain: 0.3 });
|
||||
// connect osc to gain
|
||||
const node = o.connect(g);
|
||||
// this function can be called from outside to stop the sound
|
||||
const stop = (time) => o.stop(time);
|
||||
// ended will be fired when stop has been fired
|
||||
o.addEventListener('ended', () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
},
|
||||
{ type: 'synth' },
|
||||
);
|
||||
// use the sound
|
||||
freq(220, 440, 330).s('mysaw');
|
||||
```
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
registerSound(
|
||||
'mysaw',
|
||||
(time, value, onended) => {
|
||||
let { freq } = value; // destructure control params
|
||||
const ctx = getAudioContext();
|
||||
// create oscillator
|
||||
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
|
||||
o.start(time);
|
||||
// add gain node to level down osc
|
||||
const g = new GainNode(ctx, { gain: 0.3 });
|
||||
// connect osc to gain
|
||||
const node = o.connect(g);
|
||||
// this function can be called from outside to stop the sound
|
||||
const stop = (time) => o.stop(time);
|
||||
// ended will be fired when stop has been fired
|
||||
o.addEventListener('ended', () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
},
|
||||
{ type: 'synth' },
|
||||
);
|
||||
// use the sound
|
||||
freq("220 440 330").s('mysaw');`}
|
||||
/>
|
||||
|
||||
You can actually use this code in the [REPL](https://strudel.cc/) and it'll work.
|
||||
After evaluating the code, you should see `mysaw` in listed in the sounds tab.
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -28,12 +29,12 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
if (!isNaN(date)) {
|
||||
title = date.toLocaleDateString();
|
||||
} else {
|
||||
title = 'unnamed';
|
||||
title = pattern.id || 'unnamed';
|
||||
}
|
||||
}
|
||||
|
||||
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
|
||||
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
@@ -79,73 +80,115 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
export function PatternsTab({ context }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
|
||||
const { userPatterns, patternAutoStart } = useSettings();
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
return (
|
||||
<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,text/x-markdown,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(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const visiblePatterns = useMemo(() => {
|
||||
if (!search) {
|
||||
return userPatterns;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||
const meta = getMetadata(pattern.code);
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
// Search for specific meta keys
|
||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||
if (searchLowercaseTrimmed.includes(':')) {
|
||||
const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/);
|
||||
if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) {
|
||||
const metaValues = meta[metaKey];
|
||||
if (Array.isArray(metaValues)) {
|
||||
return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch));
|
||||
} else if (typeof metaValues === 'string') {
|
||||
return metaValues.toLowerCase().includes(metaSearch);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
}
|
||||
}
|
||||
const title = meta.title ? meta.title : 'unnamed';
|
||||
const authors = meta.by ? meta.by : ['anonymous'];
|
||||
const tags = meta.tag ? meta.tag : [];
|
||||
return (
|
||||
title.toLowerCase().includes(searchLowercaseTrimmed) ||
|
||||
authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) ||
|
||||
tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed))
|
||||
);
|
||||
}),
|
||||
);
|
||||
}, [search, viewingPatternStore]);
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<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,text/x-markdown,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(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
}
|
||||
}}
|
||||
patterns={visiblePatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -232,28 +275,3 @@ function PublicPatterns({ 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">
|
||||
<UserPatterns context={context} />
|
||||
</div>
|
||||
);
|
||||
/* 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>
|
||||
); */
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Reference() {
|
||||
return (
|
||||
<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">
|
||||
<div className="w-full flex">
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user