Compare commits

...

29 Commits

Author SHA1 Message Date
Felix Roos d0ce82e3cd Publish
- @strudel/codemirror@1.2.4
 - @strudel/core@1.2.4
 - @strudel/csound@1.2.5
 - @strudel/draw@1.2.4
 - @strudel/gamepad@1.2.4
 - @strudel/hydra@1.2.4
 - @strudel/midi@1.2.5
 - @strudel/mini@1.2.4
 - @strudel/mondo@1.1.2
 - @strudel/motion@1.2.4
 - @strudel/mqtt@1.2.4
 - @strudel/osc@1.2.4
 - @strudel/repl@1.2.5
 - @strudel/sampler@0.2.3
 - @strudel/serial@1.2.4
 - @strudel/soundfonts@1.2.5
 - superdough@1.2.5
 - @strudel/tonal@1.2.4
 - @strudel/transpiler@1.2.4
 - @strudel/web@1.2.5
 - @strudel/webaudio@1.2.5
 - @strudel/xen@1.2.4
2025-09-10 23:08:16 +02:00
Switch Angel AKA Jade Rose e82629de2e Merge pull request 'feat: add delete user samples button for convenience' (#1556) from delete_samples_button into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1556
2025-09-10 17:25:25 +02:00
Switch Angel AKA Jade Rose 8a91549a7b Merge branch 'main' into delete_samples_button 2025-09-09 06:42:38 +02:00
froos cc27d482cd Merge pull request 'Fix formatting of REPL footnote' (#1547) from ddbeck/strudel:fix-footnote into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1547
2025-09-08 08:54:00 +02:00
Switch Angel AKA Jade Rose 37e640470b Merge pull request 'fix: OSC' (#1557) from fixosc into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1557
2025-09-08 05:38:07 +02:00
Jade (Rose) Rowland 5e5e7730c1 rm errorlogger console 2025-09-07 23:29:24 -04:00
Jade (Rose) Rowland 89e7d52308 fixed 2025-09-07 23:28:18 -04:00
Jade (Rose) Rowland 99bb227cf4 format 2025-09-07 22:48:04 -04:00
Jade (Rose) Rowland 70e776e799 cleanup div 2025-09-07 22:44:09 -04:00
Jade (Rose) Rowland 66aad0004a fix formatting 2025-09-07 22:43:13 -04:00
Jade (Rose) Rowland a968545458 change sound background to distinguish action buttons 2025-09-07 22:41:03 -04:00
Jade (Rose) Rowland 5414bbe85d only show button on user samples 2025-09-07 22:27:48 -04:00
Jade (Rose) Rowland e7e80bfd83 working 2025-09-07 22:21:03 -04:00
froos c57f5bb429 Merge pull request 'Add examples for ? and | operators to documentation' (#1532) from james-collapse/strudel:mininotation-randomness-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1532
2025-09-07 22:47:10 +02:00
froos f2608e712b Merge pull request 'Signal flow documentation' (#1504) from glossing/strudel:glossing/signal-flow-documentation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1504
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-09-07 21:48:18 +02:00
froos 34b41fc26f Merge pull request 'Allow flattening of dirs with sample server' (#1529) from glossing/strudel:glossing/sample-server-with-flat into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1529
2025-09-07 21:27:28 +02:00
Switch Angel AKA Jade Rose f6e171fed8 Merge pull request 'fix: prevent orbit clicking by defining max number of channels in an orbit' (#1552) from orbitfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1552
2025-09-07 21:05:23 +02:00
Daniel D. Beck 977420e74d Fix formatting of REPL footnote 2025-09-06 15:33:08 +02:00
james-collapse 3e0e903d38 Merge branch 'main' into mininotation-randomness-docs 2025-09-03 20:19:54 +02:00
James Walker 591c3fe08f Add documentation for ?n in mini-notation 2025-09-03 19:16:47 +01:00
James Walker 142160d79a Add examples for ? and | operators 2025-09-01 21:08:06 +01:00
Aria 6a09f54b25 Add vibrato to list of continuous modulators 2025-08-31 01:03:09 -05:00
Aria 189e650a73 Add flat flag, automatic json saving; filter out non-audio at base dir 2025-08-29 12:18:28 -05:00
Aria a0fc52b1ec Final tweaks 2025-08-21 14:42:52 -05:00
Aria e490774294 Mention order of effects 2025-08-19 16:06:18 -05:00
Aria 34e8a57472 Typos grammar etc 2025-08-19 11:32:07 -05:00
Aria 59c8d70714 Code format 2025-08-19 11:27:56 -05:00
Aria ce7cff2c3b Clean up wording on orbit routing 2025-08-19 11:26:25 -05:00
Aria 32fe73aba2 Update effects documentation to include information on signal flow 2025-08-19 11:10:34 -05:00
34 changed files with 282 additions and 69 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.3",
"version": "1.2.4",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -6,7 +6,7 @@ let debounce = 1000,
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
console.error(e);
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.3",
"version": "1.2.4",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.4",
"version": "1.2.5",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.3",
"version": "1.2.4",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.3",
"version": "1.2.4",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.3",
"version": "1.2.4",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.4",
"version": "1.2.5",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.3",
"version": "1.2.4",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.1",
"version": "1.1.2",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.3",
"version": "1.2.4",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.3",
"version": "1.2.4",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.3",
"version": "1.2.4",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+1 -1
View File
@@ -6,5 +6,5 @@ const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
const currentTime = performance.now() / 1000;
return trigger(null, hap, currentTime, cps, targetTime);
return trigger(hap, currentTime, cps, targetTime);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.4",
"version": "1.2.5",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.2",
"version": "0.2.3",
"description": "",
"keywords": [
"tidalcycles",
+55 -19
View File
@@ -1,14 +1,20 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream, existsSync } from 'fs';
import { createReadStream, existsSync, writeFileSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, sep } from 'path';
import { join, resolve, sep } from 'path';
import readline from 'readline';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
const isAudioFile = (f) => {
const ext = f.split('.').slice(-1)[0].toLowerCase();
return VALID_AUDIO_EXTENSIONS.includes(ext);
};
async function getFilesInDirectory(directory) {
let files = [];
@@ -21,32 +27,32 @@ async function getFilesInDirectory(directory) {
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
isAudioFile(fullPath) && files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
async function getBanks(directory, flat = false) {
let files = await getFilesInDirectory(directory);
let banks = {};
directory = directory.split(sep).join('/');
files = files.map((path) => {
path = path.split(sep).join('/');
const [bank] = path.split('/').slice(-2);
const subDir = path.replace(directory, '');
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
banks[bank] = banks[bank] || [];
const relativeUrl = path.replace(directory, '');
banks[bank].push(relativeUrl);
return relativeUrl;
banks[bank].push(subDir);
return subDir;
});
banks._base = `http://localhost:5432`;
return { banks, files };
@@ -54,14 +60,44 @@ async function getBanks(directory) {
const args = process.argv.slice(2);
// eslint-disable-next-line
const directory = process.cwd();
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
function getInput(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(query, (response) => {
rl.close();
resolve(response);
}),
);
}
let directory = getArgValue('--dir') || process.cwd();
directory = resolve(directory);
if (args.includes('--json')) {
const { banks, files } = await getBanks(directory);
const { banks } = await getBanks(directory, getArgValue('--flat'));
const json = JSON.stringify(banks);
console.log(json);
process.exit(0);
const outFile = resolve(directory, 'strudel.json');
if (existsSync(outFile)) {
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
if (answer.toLowerCase() !== 'y') {
console.log('Aborted.');
process.exit(0);
}
}
writeFileSync(outFile, json, 'utf8');
console.log(`Wrote json to ${outFile}`);
}
console.log(
@@ -74,7 +110,7 @@ console.log(
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
@@ -82,7 +118,7 @@ const server = http.createServer(async (req, res) => {
let subpath = decodeURIComponent(req.url);
const filePath = join(directory, subpath.split('/').join(sep));
//console.log('GET:', filePath);
// console.log('GET:', filePath);
const isFound = existsSync(filePath);
if (!isFound) {
res.statusCode = 404;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.3",
"version": "1.2.4",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.4",
"version": "1.2.5",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.4",
"version": "1.2.5",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.3",
"version": "1.2.4",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.3",
"version": "1.2.4",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.4",
"version": "1.2.5",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.4",
"version": "1.2.5",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.3",
"version": "1.2.4",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+123
View File
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
# Signal chain
<img src="/img/strudel-signal-flow.png"></img>
The signal chain in Strudel is as follows:
- An sound-generating event is triggered by a pattern
- This has a start time and a duration, which is usually
controlled by the note length and ADSR parameters
- If we exceed the max polyphony, old sounds begin to die off
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
- A sound is produced (through, say, a sample or an oscillator)
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
again)
- Phase vocoder (`stretch`)
- Gain is applied (`gain`)
- This is where the main (volume) ADSR happens
- A lowpass filter (`lpf`)
- A highpass filter (`hpf`)
- A bandpass filter (`bandpass`)
- A vowel filter (`vowel`)
- Sample rate reduction (`coarse`)
- Bit crushing (`crush`)
- Waveshape distortion (`shape`)
- Normal distortion (`distort`)
- Tremolo (`tremolo`)
- Compressor (`compressor`)
- Panning (`pan`)
- Phaser (`phaser`)
- Postgain (`post`)
- The sound is then split into multiple destinations
- Dry output (amount controlled by `dry` parameter)
- The sends
- Analyzers
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
- Delay (amount controlled by `delay` parameter)
- Reverb (amount controlled by `room` parameter)
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
- The `duck` effect affects the volume of all signals in the orbit
- The orbit is then sent to the mixer
## Orbits
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
with a large reverb:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)`}
/>
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
distinct:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10).orbit(2)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
## Continuous changes
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
modified continuously in time. For example,
<MiniRepl
client:visible
tune={`
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
/>
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
<MiniRepl
client:visible
tune={`
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
/>
Some parameters _do_ induce continuous variations in time, though:
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
- The pitch envelope curve (governed by `penv` and its associated ADSR)
- The FM curve (`fmenv`)
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
- Tremolo (`tremolo`)
- Phaser (`phaser`)
- Vibrato (`vib`)
- Ducking (`duckorbit`)
# Filters
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
+16
View File
@@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
## Randomness
Events with a "?" placed after them will have a 50% chance of being removed from the pattern:
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?")`} punchcard />
Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed:
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?0.1")`} punchcard />
Events separated by a "|" will be chosen from at random:
<MiniRepl client:idle tune={`note("[g3,b3,e4] | [a3,c3,e4] | [b3,d3,f#4]")`} punchcard />
## Mini-notation review
To recap what we've learned so far, compare the following patterns:
@@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3|b3|e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
## Euclidian rhythms
+3 -1
View File
@@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl';
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
@@ -0,0 +1,10 @@
import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
return (
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
{labelIsHidden !== true && label}
{children}
</button>
);
}
@@ -12,8 +12,8 @@ import { useMemo } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { useSettings } from '../../../settings.mjs';
import { ActionButton } from '../button/action-button.jsx';
import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
@@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
);
}
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
);
}
const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
+34 -11
View File
@@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
import { ActionButton } from '../button/action-button.jsx';
import { confirmDialog } from '@src/repl/util.mjs';
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
import { prebake } from '@src/repl/prebake.mjs';
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const { BASE_URL } = import.meta.env;
@@ -27,18 +32,19 @@ export function SoundsTab() {
.sort((a, b) => a[0].localeCompare(b[0]))
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
if (soundsFilter === 'user') {
if (soundsFilter === soundFilterType.USER) {
return filtered.filter(([_, { data }]) => !data.prebake);
}
if (soundsFilter === 'drums') {
if (soundsFilter === soundFilterType.DRUMS) {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
}
if (soundsFilter === 'samples') {
if (soundsFilter === soundFilterType.SAMPLES) {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
}
if (soundsFilter === 'synths') {
if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
//TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') {
return [];
}
@@ -57,10 +63,10 @@ export function SoundsTab() {
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div className="pb-2 flex shrink-0 flex-wrap">
<div className=" flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
@@ -74,7 +80,26 @@ export function SoundsTab() {
></ButtonGroup>
</div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && (
<ActionButton
className="pl-2"
label="delete-all"
onClick={async () => {
try {
const confirmed = await confirmDialog('Delete all imported user samples?');
if (confirmed) {
clearIDB(userSamplesDBConfig.dbName);
soundMap.set({});
await prebake();
}
} catch (e) {
console.error(e);
}
}}
/>
)}
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -151,9 +176,7 @@ export function SoundsTab() {
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
</div>
</div>
);
+6 -2
View File
@@ -12,17 +12,21 @@ export const userSamplesDBConfig = {
};
// deletes all of the databases, useful for debugging
function clearIDB() {
function clearAllIDB() {
window.indexedDB
.databases()
.then((r) => {
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
for (var i = 0; i < r.length; i++) clearIDB(r[i].name);
})
.then(() => {
alert('All data cleared.');
});
}
export function clearIDB(dbName) {
return window.indexedDB.deleteDatabase(dbName);
}
// queries the DB, and registers the sounds so they can be played
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
openDB(config, (objectStore) => {
+9 -1
View File
@@ -8,6 +8,14 @@ export const audioEngineTargets = {
osc: 'osc',
};
export const soundFilterType = {
USER: 'user',
DRUMS: 'drums',
SAMPLES: 'samples',
SYNTHS: 'synths',
ALL: 'all',
};
export const defaultSettings = {
activeFooter: 'intro',
keybindings: 'codemirror',
@@ -28,7 +36,7 @@ export const defaultSettings = {
fontSize: 18,
latestCode: '',
isZen: false,
soundsFilter: 'all',
soundsFilter: soundFilterType.ALL,
patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right',