Merge branch 'main' into space-shell/helix-keybindings

This commit is contained in:
space-shell
2026-01-19 10:05:14 +01:00
95 changed files with 3810 additions and 695 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<base href={BASE_URL} />
<!-- Scrollable a11y code helper -->
<script {`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: -->
+1 -1
View File
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<base href={BASE_URL} />
<!-- Scrollable a11y code helper -->
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: -->
+1
View File
@@ -76,6 +76,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'MIDI & OSC', link: 'learn/input-output' },
],
More: [
{ text: 'FAQ', link: 'learn/faq' },
{ text: 'Recipes', link: 'recipes/recipes' },
{ text: 'Mini-Notation', link: 'learn/mini-notation' },
{ text: 'Visual Feedback', link: 'learn/visual-feedback' },
+1 -1
View File
@@ -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) {
+339
View File
@@ -0,0 +1,339 @@
---
title: Frequently Asked Questions
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Frequently Asked Questions
This page contains frequently asked questions, with answers. Usually, the topic is explained in more detail in a section which is linked in the answer.
## Is Strudel/Tidal free?
Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. However if you can, please make a one-off or regular donation to our [opencollective fund](https://opencollective.com/tidalcycles), that supports the software and cultural development of Strudel and other Uzu languages.
While there is no charge there are some caveats, e.g.:
- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission.
## How do I try out the latest features?
The main, stable strudel website is [strudel.cc](https://strudel.cc/). There is also [warm.strudel.cc](https://warm.strudel.cc), known as "warm strudel", which has the latest development features. You might find warm strudel has bug fixes and features that the main website doesn't, but it will often be less stable and probably not suitable for important performances.
Alternatively, you can run strudel locally to try out the latest features. You can find development-oriented [instructions for that here](https://codeberg.org/uzu/strudel/src/branch/main/CONTRIBUTING.md#project-setup).
You can see the [latest changes here](https://codeberg.org/uzu/strudel/pulls?q=&type=all&sort=recentupdate&state=closed&labels=&milestone=0&project=0&assignee=0&poster=0), as 'pull requests'.
## How to record or export audio?
Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel:
- Use the 'export' tab to render and download as an audio file.
- capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc.
- use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it.
- capture the audio/video stream using a capture tool such as [OBS](https://obsproject.com/fr), which is designed for live streaming, but also works very well for recording.
- don't record anything and code it again in front of your friends.
## Can I use strudel with my IDE?
Yes you can. There are experimental modes, made by community members, for several IDEs such as:
- VS Code: [Strudel VS](https://marketplace.visualstudio.com/items?itemName=cmillsdev.strudelvs): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel), which is defunct.
- nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim)
## How can I use my own samples?
There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians:
- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded.
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
## Can I use Strudel with AI/LLM tools?
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now,
including by people with very shady motives. Many in the community are very wary of people training models
on their tunes that they've poured their love into. So please keep discussion and questions around AI and
LLMs to channels dedicated to the topic and be fully respectful of other people's work.
Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those
answers for you, as generally they will be timewasting nonsense.
Human questions are always welcome!
## Where can I download loads of patterns to train my LLM?
You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools)
## How to run offline?
Strudel works offline just fine! There are multiple techniques for this, see [this explanation](learn/pwa/#using-strudel-offline).
## How to change tempo? How do I translate BPM to cpm?
Strudel works in cycles, rather than beats, but if you assume a certain number of beats per cycle, you can convert between them.
For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)`
where BPM is your beats per minute.
If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or
two bars into one cycle), adjust accordingly.
## Where can I see all the functions?
If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "reference" which lists all the functions of strudel.
## Where can I see all the samples and synths?
If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "sounds" which lists all the drum machines, samples and synths currently loaded.
## How do I use this exactly like a DAW?
Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards
sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and
transforming patterns in ways that can be hard to predict.
If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations
executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these
operations. For example in Strudel, the 'arrange' and 'pick' methods are useful for sequencing patterns over time (see question on these later in this document).
You might still find that the typical DAW workflow is not really adapted to live coding because, despite
both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding
to the medium of code, which might mean leaving more place to serendipity and writing code that you don't predict the output of.
## Why doesn't everyone just use a DAW?
There is no easy answer to this question. Here are some thoughts:
- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox.
- Live coding has developed over decades as a distinct creative practice. For example, live coding artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody.
- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model, no user tracking or hidden features. We need open tools in the arts! - Live coders don't all shy away from using DAWs. Many use them all of the time, especially when it makes their life easier for... live coding!
- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc.
- There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming.
## How can I interface Strudel with my favorite music software? What can I do with it?
Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for communicating musical information.
Other music software (or hardware!) can then listen to these messages and process them according to its capabilities.
A simple example would be to send livecoded audio to a DAW like Ardour on different tracks and then use it to mix them.
You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score.
You could also send MIDI to your hardware synths, if you like their sound.
## How do I use this in my closed source webgame or other software?
You don't. You need to license your game to a free/open source license fulfill the [AGPLv3 license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) Strudel is distributed under.
## How to play different patterns simultaneously?
Using the $ operator, several patterns can be played at once:
<MiniRepl
client:idle
tune={`$: s("bd*4").bank("tr707")
$: s("- sd").bank("tr909")`}
/>
See also [stack](intro/#combining-patterns)
## Is it possible to mute a pattern?
With an additional underscore, a pattern can be muted.
<MiniRepl
client:idle
tune={`$: s("bd*4").bank("tr707")
_$: s("- sd").bank("tr909")`}
/>
See also [hush](/learn/conditional-modifiers/#hush)
## How can I arrange in Strudel using `mask`?
With mini-notation, using the `<>` and `!` operators, you can try something like
```
.mask("<0!24 1!40>")
```
It mutes a pattern for 24 cycles and plays it for 40. You would gain 64 cycles total, a multiple of 2/4/8 commonly used in western music.
If each cycle is a bar, as a starting point, you could write a mask like that for any pattern:
```
.mask("<0!16 0!16 0!16 0!16 0!16 0!10>")
```
It mutes it throughout.
For arranging, you could add the same mask to each part and replace some zeroes with ones in your different masks to make parts play.
If you use `.mask()` on different patterns mess up your counting, then patterns do not align anymore.
On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel.
You can make things quite lively and more organic with a little (controlled) interference, according to your own taste.
And you are free to arrange in cycles like 3, 6 or 9 too.
To modify everything at once, you could try all and when, for example:
```
all(x=>x.when("<0!7 1>", x=>x.lpf(saw.range(200, 2000))))
```
This would lowpass filter sweep everything every 8 cycles.
## How can I arrange in Strudel using `arrange` or `pick`?
Take [Pachelbel's Canon in D](https://en.wikipedia.org/wiki/Pachelbel%27s_Canon#Analysis) as an example which has 4 voices (one cello and 3 violins) which have repeating patterns, as seen in the link above.
The following snipped defines the patterns as constants which can then be used for the different voices. `arrange` takes multiple arguments, which are each a number of cycles and a pattern which is played for the number of cycles, wrapped in `[]` If the pattern is shorter than the number, it is repeated.
<MiniRepl client:idle tune={`const cello = note(
"<[d3 a2 b2 f#2] [g2 d3 g2 a2]>")
.color("grey").sound("gm_tremolo_strings:3")
const violin_p1 = note(
"<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>")
.color("blue")
const violin_p2 = note(
"<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>")
.color("green")
const violin_p3 = note(
"<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>")
.color("purple")
const violin_p4 = note(
"<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>")
.color("red")
cello$: arrange(
[2, silence],
[18,cello])
violin1$: arrange(
[4,silence],
[2,violin_p1], [2,violin_p2],
[2,violin_p3], [2,violin_p4],
[2,violin_p1], [2,violin_p2],
[2,violin_p3], [2,violin_p4]
).sound("gm_tremolo_strings:0")
violin2$: arrange(
[6,silence], [2,violin_p1],
[2,violin_p2], [2,violin_p3],
[2,violin_p4], [2,violin_p1],
[2,violin_p2], [2,violin_p3]
).sound("gm_tremolo_strings:1")
violin3$: arrange(
[8,silence],
[2,violin_p1], [2,violin_p2],
[2,violin_p3], [2,violin_p4],
[2,violin_p1], [2,violin_p2]
).sound("gm_tremolo_strings:2")
all(x => x.release(.2))
`} />
Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles.
`pick` has better highlighting than `arrange`:
<MiniRepl client:idle tune={`const cello = note(
"<[d3 a2 b2 f#2] [g2 d3 g2 a2]>")
.color("grey").sound("gm_tremolo_strings:3")
const violin_p1 = note(
"<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>")
.color("blue")
const violin_p2 = note(
"<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>")
.color("green")
const violin_p3 = note(
"<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>")
.color("purple")
const violin_p4 = note(
"<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>")
.color("red")
const violins = [violin_p1, violin_p2, violin_p3, violin_p4]
cello$: "<~@2 0@18>".pick([cello])
violin1$: "<~@4 0@2 1@2 2@2 3@2 0@2 1@2 2@2 3@2>".pick(violins)
.sound("gm_tremolo_strings:0")
violin2$: "<~@6 0@2 1@2 2@2 3@2 0@2 1@2 2@2>".pick(violins)
.sound("gm_tremolo_strings:1")
violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins)
.sound("gm_tremolo_strings:2")
all(x => x.release(.2))
`} />
The `pick` method also works with jsons which have named elements, which makes it easier to read, see the [here](/learn/conditional-modifiers/#pick). `pickRestart` restarts the pattern upon picking it which can make a difference if the duration of the pick indexes doesn't line up with the patterns which are picked - which is not the case here.
Try adding `.punchcard()` after the `release(.2)` for a visualization.
## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work?
Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel.
They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts)
You can find the instructions how to use that script in the readme.md there.
## Is there difference between `n` and `note`?
They are not aliases of each other, in contrast to `s` and `sound`.
The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`).
On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) .
The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`.
<MiniRepl
client:idle
tune={`
n("<[0 1 2 3@3 -@2] [3 2 1 0@3 -@2] >")
.scale("A:minor:pentatonic")
.s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`}
/>
Note that `n` is not the only way that functions use indices, some take numbered patterns instead.
## Is there a cheat sheet for all symbols?
Yes!
```
' marks start and end of strings, is different from "
" marks start and end of single line patterns in mini notation, is different from '
` marks start and end of patterns with line breaks in mini notation, is different from '
[] used for patterns in mini notation, each item in it has the same length
<> used for patterns, alternates between items each cycle
{} historically used for polyrhythmic patterns. {a b c}%4 is the same as <a b c>*4.
@3 elongates the item by a factor of 3 (other numbers work too, even non-integer, but for numbers between 0 and 1 you need a leading zero like this: @0.5)
@ after an item: elongates the item once (multiple @ work too c @ @ is the same as c@3)
_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3), see below for a different usage.
. this divides equal parts of a pattern and is called a foot. Can be used instead of [] like this: "1 6 7 8 . 2 . 3 . 4" is the same as "[1 6 7 8] 2 3 4"
- silence
~ also silence
x not silence (for the use in struct, any non-silence symbol works there)
b decrease by one semitone, i.e. flat, works for steps of scales, note names (but not midi numbers) and chord names
s increase by one semitone, i.e. sharp, works for steps of scales, note names (but not midi numbers) but not chord names
# increase by one semitone, i.e. sharp works for steps of scales, note names (but not midi numbers) and chord names
# also used in mondo notation
*3 play the sample or pattern at thrice the speed, fast(3)
!3 play the sample or pattern three times
/2 play the sample or pattern at half speed, slow(2)
? play the pattern sometimes
| once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles()
, play all items separated by it at the same time, i.e. stack()
: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects.
$: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names
_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage.
```
+5 -1
View File
@@ -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.
+19
View File
@@ -53,6 +53,7 @@ Available tags are:
- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.)
- `@genre`: music genre(s) (pop, jazz, etc.)
- `@album`: music album name
- `@tag`: custom tag
Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values
@@ -92,3 +93,21 @@ If a tag doesn't accept a list, it can take multi-line values:
the sofa in the living room.
*/
```
# Searching meta-data in the online repl
Meta-data can be used in the search field of the patterns tab in the online repl.
For example to search for all patterns by a specific author use the search term
```
by: Ada L
```
or search for patterns with a specific genre like
```
genre: unicorns
```
Hint: If no meta-data property is provided in the search all patterns with a `@title`, `@by` or `@tag` matching the search term will be shown.
+31 -29
View File
@@ -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.
@@ -66,7 +66,7 @@ export default function ExportTab(Props) {
<div className="flex flex-row gap-4 w-full">
<FormItem label="Start cycle" disabled={exporting}>
<Textbox
min={1}
min={0}
max={Infinity}
onBlur={(e) => {
let v = parseInt(e.target.value);
@@ -1,6 +1,7 @@
import { errorLogger } from '@strudel/core';
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
import { SpecialActionInput } from '../button/action-button';
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
async function importScript(script) {
const reader = new FileReader();
@@ -23,7 +24,19 @@ export function ImportPrebakeScriptButton() {
type="file"
label="import prebake script"
accept=".strudel"
onChange={(e) => importScript(e.target.files[0])}
onChange={async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
if (!confirmed) {
return;
}
try {
await importScript(file);
window.location.reload();
} catch (e) {
errorLogger(e);
}
}}
/>
);
}
+105 -87
View File
@@ -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>
); */
}
+136 -26
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
@@ -10,8 +10,16 @@ const availableFunctions = (() => {
const functions = [];
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
if (seen.has(doc.name)) continue;
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
// @something is. We only want data from comments like `@tags fx, superdough` here.
// If nothing is specified, we default to "untagged" for debugging
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
functions.push(doc);
const synonyms = doc.synonyms || [];
seen.add(doc.name);
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
seen.add(s);
@@ -35,11 +43,27 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
export function Reference() {
export const Reference = memo(function Reference() {
const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const [selectedFunction, setSelectedFunction] = useState(null);
const visibleFunctions = useMemo(() => {
const toggleTag = (tag) => {
if (selectedTag === tag) {
setSelectedTag(null);
} else {
setSelectedTag(tag);
}
};
const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (selectedTag) {
if (!(entry.tags || ['untagged']).includes(selectedTag)) {
return false;
}
}
if (!search) {
return true;
}
@@ -50,43 +74,128 @@ export function Reference() {
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search]);
}, [search, selectedTag]);
const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => {
if (selectedTag === null) {
if (search) {
return true;
}
return x.name === selectedFunction;
} else {
return true;
}
});
}, [searchVisibleFunctions, selectedFunction, selectedTag]);
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
}
});
}
const onSearchTagFilterClick = () => {
setSelectedTag(null);
setSelectedFunction(null);
};
useEffect(() => {
if (selectedFunction) {
const el = document.getElementById(`doc-${selectedFunction}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}
}, [selectedFunction]);
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">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<div className="h-full text-foreground flex flex-col gap-3 w-1/3 ">
<div className="w-full flex">
<Textbox
className="w-full"
placeholder="Search"
value={search}
onChange={(e) => {
setSelectedFunction(null);
setSearch(e);
}}
/>
</div>
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
{selectedTag && (
<div className="w-72">
<span
className="text-foreground border-2 border-gray-500 px-1 py-0.5 my-2 rounded-md cursor-pointer font-sans"
onClick={onSearchTagFilterClick}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
{selectedTag}
</span>
</div>
)}
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{searchVisibleFunctions.map((entry, i) => (
<Fragment key={`entry-${entry.name}`}>
<a
className={
'cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis ' +
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
}
onClick={() => {
if (entry.name === selectedFunction) {
setSelectedFunction(null);
} else {
setSelectedFunction(entry.name);
}
}}
>
{entry.name}
</a>{' '}
</Fragment>
))}
</div>
</div>
<div
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
className="break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<h2>API Reference</h2>
<p>
<p className="font-sans text-md">
This is the long list of functions you can use. Remember that you don't need to remember all of those and
that you can already make music with a small set of functions!
</p>
{visibleFunctions.map((entry, i) => (
<section key={i}>
<h3 id={`doc-${i}`}>{entry.name}</h3>
<div>
{Object.entries(tagCounts)
.sort(([a], [b]) => a.localeCompare(b))
.map(([t, count]) => (
<span key={t}>
<a
className={[
'select-none text-white border-2 border-gray-500 px-1 py-0.5 my-2 cursor-pointer text-sm/8 rounded-md no-underline font-sans',
`${selectedTag === t ? 'bg-gray-500 text-black' : ''}`,
].join(' ')}
onClick={() => toggleTag(t)}
>
{t}&nbsp;({count})
</a>{' '}
</span>
))}
</div>
{detailVisibleFunctions.map((entry, i) => (
<section key={i} className="font-sans">
<div className="flex flex-row items-center mt-8 justify-between">
<h3 className="font-mono my-0" id={`doc-${entry.name}`}>
{entry.name}
</h3>
{entry.tags && (
<span className="ml-2 text-xs text-gray-400 border-2 border-gray-500 rounded-md px-1 py-0.5">
{entry.tags.join(', ')}
</span>
)}
</div>
{!!entry.synonyms_text && (
<p>
Synonyms: <code>{entry.synonyms_text}</code>
@@ -107,9 +216,10 @@ export function Reference() {
</pre>
))}
</section>
))}
)) || <p className="font-sans">Searcb or select a tag to get started.</p>}
{detailVisibleFunctions.length > 0 && <div className="h-screen" />}
</div>
</div>
</div>
);
}
});
@@ -1,13 +1,13 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { isUdels } from '../../util.mjs';
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
import { SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
function Checkbox({ label, value, onChange, disabled = false }) {
@@ -86,8 +86,6 @@ const fontFamilyOptions = {
galactico: 'galactico',
};
const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
export function SettingsTab({ started }) {
const {
theme,
@@ -127,11 +125,8 @@ export function SettingsTab({ started }) {
isDisabled={started}
audioDeviceName={audioDeviceName}
onChange={(audioDeviceName) => {
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('audioDeviceName', audioDeviceName);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('audioDeviceName', audioDeviceName);
});
}}
/>
@@ -141,11 +136,8 @@ export function SettingsTab({ started }) {
<AudioEngineTargetSelector
target={audioEngineTarget}
onChange={(target) => {
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('audioEngineTarget', target);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('audioEngineTarget', target);
});
}}
/>
@@ -175,12 +167,9 @@ export function SettingsTab({ started }) {
label="Multi Channel Orbits"
onChange={(cbEvent) => {
const val = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
});
}}
value={multiChannelOrbits}
@@ -297,11 +286,8 @@ export function SettingsTab({ started }) {
label="Sync across Browser Tabs / Windows"
onChange={(cbEvent) => {
const newVal = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r) {
settingsMap.setKey('isSyncEnabled', newVal);
window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('isSyncEnabled', newVal);
});
}}
disabled={shouldAlwaysSync}
@@ -210,7 +210,11 @@ export function SoundsTab() {
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
{!soundEntries.length && soundsFilter !== 'importSounds'
? search == ''
? 'No sounds loaded'
: 'No sounds found'
: ''}
</div>
</div>
);
+14 -2
View File
@@ -1,4 +1,4 @@
import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns } from '../settings.mjs';
import { setVersionDefaults } from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser';
@@ -107,7 +107,19 @@ export function confirmDialog(msg) {
resolve(confirmed);
});
}
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
export function confirmAndReloadPage(onSuccess) {
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
if (r == true) {
try {
onSuccess();
return window.location.reload();
} catch (e) {
errorLogger(e);
}
}
});
}
//RIP due to SPAM
// let lastShared;
// export async function shareCode(codeToShare) {