Compare commits

..

7 Commits

10 changed files with 204 additions and 122 deletions
+3 -1
View File
@@ -3046,6 +3046,7 @@ registerSubControls('lfo', [
['skew', 'sk'],
['curve', 'cu'],
['sync', 's'],
['retrig', 'rt'],
['fxi'],
]);
registerSubControls('env', [
@@ -3132,13 +3133,14 @@ Pattern.prototype.modulate = function (type, config, idPat) {
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.retrig] If > 0.5, the LFO will retrigger on each event. Aliases: rt
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator
* @returns Pattern
+117 -89
View File
@@ -1055,91 +1055,88 @@ function _composeOp(a, b, func) {
return func(a, b);
}
// Make composers
(function () {
// pattern composers
const composers = {
set: [(a, b) => b],
keep: [(a) => a],
keepif: [(a, b) => (b ? a : undefined)],
// pattern composers
const COMPOSERS = {
set: [(a, b) => b],
keep: [(a) => a],
keepif: [(a, b) => (b ? a : undefined)],
// numerical functions
/**
*
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add
* @memberof Pattern
* @tags math
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
* @name sub
* @memberof Pattern
* @tags math
* @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
/**
*
* Multiplies each number by the given factor.
* @name mul
* @memberof Pattern
* @tags math
* @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
* @tags math
*/
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)],
blshift: [numeralArgs((a, b) => a << b)],
brshift: [numeralArgs((a, b) => a >> b)],
// numerical functions
/**
*
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add
* @memberof Pattern
* @tags math
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
* @name sub
* @memberof Pattern
* @tags math
* @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
/**
*
* Multiplies each number by the given factor.
* @name mul
* @memberof Pattern
* @tags math
* @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
* @tags math
*/
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)],
blshift: [numeralArgs((a, b) => a << b)],
brshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b],
gt: [(a, b) => a > b],
lte: [(a, b) => a <= b],
gte: [(a, b) => a >= b],
eq: [(a, b) => a == b],
eqt: [(a, b) => a === b],
ne: [(a, b) => a != b],
net: [(a, b) => a !== b],
and: [(a, b) => a && b],
or: [(a, b) => a || b],
// TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b],
gt: [(a, b) => a > b],
lte: [(a, b) => a <= b],
gte: [(a, b) => a >= b],
eq: [(a, b) => a == b],
eqt: [(a, b) => a === b],
ne: [(a, b) => a != b],
net: [(a, b) => a !== b],
and: [(a, b) => a && b],
or: [(a, b) => a || b],
// bitwise ops
func: [(a, b) => b(a)],
};
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
// bitwise ops
func: [(a, b) => b(a)],
};
const _setupAlignments = () => {
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
Pattern.prototype['_' + what] = function (value) {
@@ -1148,16 +1145,18 @@ function _composeOp(a, b, func) {
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
// Set to configurable so we can update if the default alignment changes
configurable: true,
// a getter that returns a function, so 'pat' can be
// accessed by closures that are methods of that function..
get: function () {
const pat = this;
// wrap the 'in' function as default behaviour
const wrapper = (...other) => pat[what]['in'](...other);
const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other);
// add methods to that function for each behaviour
for (const how of hows) {
for (const how of ALIGNMENTS) {
wrapper[how.toLowerCase()] = function (...other) {
var howpat = pat;
other = sequence(other);
@@ -1182,15 +1181,23 @@ function _composeOp(a, b, func) {
return wrapper;
},
});
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of hows) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
}
};
let DEFAULT_ALIGNMENT = 'in';
const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase());
// Make composers
(function () {
_setupAlignments();
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of ALIGNMENTS) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
// binary composers
/**
* Applies the given structure to the pattern:
@@ -1249,6 +1256,27 @@ function _composeOp(a, b, func) {
};
})();
/**
* Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel.
* The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right.
* By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings.
*
* Note that we say the _default_ method, because alignments can also be set explicitly with calls like
* 'add.mix', 'set.squeeze', etc.
*
* @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly'
* @example
* setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc.
* s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
*/
export const setDefaultJoin = (alignment) => {
alignment = alignment?.toLowerCase();
if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) {
DEFAULT_ALIGNMENT = alignment;
_setupAlignments();
}
};
// aliases
export const polyrhythm = stack;
export const pr = stack;
+2 -1
View File
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
fxi = 'main',
depth = 1,
depthabs,
retrig = 0,
...filteredParams
} = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
const modParams = {
...filteredParams,
frequency: sync !== undefined ? sync * cps : rate,
time: cycle / cps,
time: retrig > 0.5 ? 0 : cycle / cps,
depth: depthValue,
min,
max,
+29
View File
@@ -10874,6 +10874,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
]
`;
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
]
`;
exports[`runs examples > example "setGainCurve" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
+2
View File
@@ -1,7 +1,9 @@
import { afterEach } from 'vitest';
import { useRNG } from './packages/core/signal.mjs';
import { setDefaultJoin } from './packages/core/pattern.mjs';
afterEach(() => {
// Avoid bleed between tests
useRNG('legacy');
setDefaultJoin('in');
});
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { BottomPanel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<BottomPanel context={context} />
<HorizontalPanel context={context} />
</div>
);
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { Code } from '@src/repl/components/Code';
import Loader from '@src/repl/components/Loader';
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel, MainPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { useSettings } from '@src/settings.mjs';
@@ -21,13 +21,13 @@ export default function ReplEditor(Props) {
<div className="flex flex-col grow overflow-hidden">
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
<MainPanel context={context} isEmbedded={isEmbedded} />
<div className="flex overflow-hidden h-full">
<div className="flex overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <RightPanel context={context} />}
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
</div>
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <BottomPanel context={context} />}
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
</div>
);
@@ -2,21 +2,13 @@ import cx from '@src/cx.mjs';
import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger';
import { useEffect, useRef } from 'react';
export function ConsoleTab() {
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
const scrollRef = useRef();
// scroll to bottom when log changes
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [log]);
return (
<div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
<div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md" ref={scrollRef}>
<div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md">
{' '}
{/* bg-background */}
{log.map((l, i) => {
@@ -26,13 +18,12 @@ export function ConsoleTab() {
<div
key={l.id}
className={cx(
'whitespace-nowrap',
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline',
)}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} className="whitespace-nowrap" />
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
);
+42 -13
View File
@@ -48,8 +48,11 @@ export function MainPanel({ context, isEmbedded = false, className }) {
<nav
id="header"
className={cx(
//'border-t sm:border-b sm:border-t-0 border-muted',
'border-b border-muted',
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
!isZen && !isEmbedded && 'border-b border-muted bg-lineHighlight',
!isZen && !isEmbedded && 'bg-lineHighlight',
// isZen ? 'h-12 w-8 fixed top-0 left-0' : 'h-10 sticky top-0 w-full justify-between',
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
'flex items-center',
className,
@@ -57,6 +60,8 @@ export function MainPanel({ context, isEmbedded = false, className }) {
style={{ fontFamily }}
>
<div className={cx('flex w-full justify-between')}>
{' '}
{/*flex-wrap*/}
<div className="px-3 py-1 flex space-x-2 select-none">
<h1
onClick={() => {
@@ -72,6 +77,11 @@ export function MainPanel({ context, isEmbedded = false, className }) {
<div className="space-x-2 flex items-baseline">
<span className="hidden sm:block">strudel</span>
<span className="text-sm font-medium hidden sm:block">REPL</span>
{/* !isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
) */}
</div>
)}
</h1>
@@ -79,6 +89,7 @@ export function MainPanel({ context, isEmbedded = false, className }) {
{!isZen && (
<div className="flex grow justify-end">
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
{/* className="hidden sm:flex" */}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
</div>
)}
@@ -90,6 +101,7 @@ export function MainPanel({ context, isEmbedded = false, className }) {
export function Footer({ context, isEmbedded = false }) {
return (
<div className="border-t border-muted bg-lineHighlight block lg:hidden">
{/* block lg:hidden */}
<MainMenu context={context} isEmbedded={isEmbedded} />
</div>
);
@@ -105,10 +117,15 @@ function MainMenu({ context, isEmbedded = false, className }) {
title={started ? 'stop' : 'play'}
className={cx('px-2 hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
{/* {!pending ? ( */}
<span className={cx('flex items-center space-x-2')}>
{/* {started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />} */}
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
{!isEmbedded && <span>{pending ? '...' : started ? 'stop' : 'play'}</span>}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
{/* ) : (
<>loading...</>
)} */}
</button>
<button
onClick={handleEvaluate}
@@ -145,7 +162,7 @@ function PanelCloseButton() {
isPanelOpen && (
<button
onClick={() => setIsPanelOpened(false)}
className={cx('px-2 py-0 text-foreground hover:opacity-50')}
className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Close Menu"
>
<XMarkIcon className="w-6 h-6" />
@@ -154,7 +171,7 @@ function PanelCloseButton() {
);
}
export function BottomPanel({ context }) {
export function HorizontalPanel({ context }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return (
<PanelNav
@@ -165,7 +182,7 @@ export function BottomPanel({ context }) {
>
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className={cx(isPanelOpen && 'border-l border-muted')} />
<Tabs setTab={setTab} tab={tab} />
</div>
{isPanelOpen && (
<div className="w-full h-full overflow-auto border-t border-muted">
@@ -176,7 +193,7 @@ export function BottomPanel({ context }) {
);
}
export function RightPanel({ context }) {
export function VerticalPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
if (!isPanelOpen) {
@@ -186,16 +203,24 @@ export function RightPanel({ context }) {
<PanelNav
settings={settings}
className={cx(
'border-l border-muted shrink-0 h-full overflow-hidden',
isPanelOpen ? `min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12',
//'border-l border-muted shrink-0',
'border-0 border-muted shrink-0 h-full overflow-hidden',
isPanelOpen
? //? `min-w-full max-w-full lg:min-w-[min(600px,100vw)] lg:max-w-[min(600px,80vw)]`
`min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]`
: 'min-w-12 max-w-12',
)}
>
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
{/* <div className="block sm:hidden text-foreground px-3 py-2 border-l border-muted">
<LogoButton context={context} />
</div> */}
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className="border-l border-muted" />
<Tabs setTab={setTab} tab={tab} />
{/* <PanelCloseButton /> */}
</div>
<div className="overflow-auto h-full">
<div className="overflow-auto h-full border-l border-muted">
<PanelContent context={context} tab={tab} />
</div>
</div>
@@ -276,7 +301,7 @@ function Tabs({ className }) {
return (
<div
className={cx(
'px-2 w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
'px-2 border-l border-muted w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
className,
)}
>
@@ -295,10 +320,14 @@ export function PanelToggle({ isEmbedded, isZen }) {
!isZen &&
panelPosition === 'right' && (
<button
title="menu"
className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
title="share"
className={cx(
'border-l border-muted px-2 py-0 text-foreground hover:opacity-50' /* , isPanelOpen && 'hidden' */,
)}
onClick={() => setIsPanelOpened(!isPanelOpen)}
>
{/* <span>menu</span> */}
{/* isPanelOpen ? <XMarkIcon className="w-6 h-6" /> : <Bars3Icon className="w-6 h-6" /> */}
<Bars3Icon className="w-6 h-6" />
</button>
)
+1 -1
View File
@@ -21,7 +21,7 @@ function getUpdatedLog(log, event) {
} else {
log = log.concat([{ message, type, id, data }]);
}
return log.slice(-40);
return log.slice(-20);
}
export function useLogger() {