mirror of
https://codeberg.org/uzu/strudel
synced 2026-08-15 17:47:17 -04:00
Merge branch 'main' into feature/592-sound-alias
This commit is contained in:
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
||||
Understand: [
|
||||
{ text: 'Coding syntax', link: 'learn/code' },
|
||||
{ text: 'Pitch', link: 'understand/pitch' },
|
||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Cycles', link: 'understand/cycles' },
|
||||
{ text: 'Voicings', link: 'understand/voicings' },
|
||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||
|
||||
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
||||
|
||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||
|
||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Xen Harmonic Functions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Xen Harmonic Functions
|
||||
|
||||
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||
|
||||
### tune(scale)
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
|
||||
Here's an example of how to configure a basic hexany scale:
|
||||
|
||||
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
||||
|
||||
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||
|
||||
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
|
||||
|
||||
You can set your root to be a particular note with `getFreq`
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.5).room(1)`}
|
||||
/>
|
||||
|
||||
Some tunings become more pronounced with a longer reverb decay:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||
/>
|
||||
|
||||
Additionally, you can combo this with `fmap` so that the base note changes:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"9 11 12 10 - - -".tune("gunkali")
|
||||
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||
/>
|
||||
|
||||
Combining this with various polyrhythm tricks can become very evocative:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||
.transpose(4)
|
||||
.tune("iraq")
|
||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||
.freq().clip(.5).room(1).rfade(9)`}
|
||||
/>
|
||||
|
||||
Another helpful trick when exploring new tunings is to strum them.
|
||||
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
|
||||
|
||||
Take the `sanza` tuning:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||
.mul(getFreq('c3'))
|
||||
.freq()`}
|
||||
/>
|
||||
|
||||
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||
See how close they are on the pitch wheel?
|
||||
|
||||
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||
|
||||
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||
.transpose("<2 5 8 1>")
|
||||
.tune("sanza")
|
||||
.mul(getFreq('c3')).freq()
|
||||
.legato("3").room(1).rfade(5)`}
|
||||
/>
|
||||
|
||||
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
|
||||
tones sound even more alive, too.
|
||||
|
||||
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
|
||||
|
||||
**selecting sample numbers separately**
|
||||
|
||||
Instead of using ":", we can also use the `n` function to select sample numbers:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
This is shorter and more readable than:
|
||||
Instead of selecting sample numbers one by one:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
||||
|
||||
We can also use the `n` function to make it shorter and more readable:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
## Recap
|
||||
|
||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
||||
|
||||
@@ -72,29 +72,39 @@
|
||||
|
||||
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
|
||||
.cm-tooltip.cm-completionInfo {
|
||||
padding: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid var(--foreground) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
max-width: 500px !important;
|
||||
min-width: 300px !important;
|
||||
max-height: 400px !important;
|
||||
white-space: normal !important;
|
||||
overflow: auto !important;
|
||||
background-color: var(--lineHighlight) !important;
|
||||
overflow: auto;
|
||||
background: var(--background) !important;
|
||||
}
|
||||
|
||||
/* Main tooltip container */
|
||||
.autocomplete-info-tooltip {
|
||||
.autocomplete-info-container {
|
||||
padding: 12px !important;
|
||||
border-radius: 4px !important;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
|
||||
font-size: var(--font-size, 13px);
|
||||
line-height: 1.4;
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
height: 100%;
|
||||
min-width: 400px;
|
||||
white-space: normal !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-tooltip {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-name {
|
||||
@@ -104,6 +114,13 @@
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-synonyms {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||
|
||||
const availableFunctions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
functions.push(doc);
|
||||
const synonyms = doc.synonyms || [];
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
||||
functions.push({
|
||||
...doc,
|
||||
name: s, // update names for the synonym
|
||||
longname: s,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
|
||||
@@ -312,7 +312,7 @@ export function SettingsTab({ started }) {
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
const { userPatterns } = settingsMap.get(); // keep current patterns
|
||||
settingsMap.set({...defaultSettings, userPatterns});
|
||||
settingsMap.set({ ...defaultSettings, userPatterns });
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user